Packages

An elixir library for Phoenix that generates Swagger documentation and performs parameter validation.

Current section

Files

Jump to
valspec lib utils.ex
Raw

lib/utils.ex

defmodule Valspec.Utils do
@schema_keys Map.keys(%OpenApiSpex.Schema{})
@doc """
Generates a OpenApiSpex.Schema.t() from a valspec definition.
"""
def generate_swagger_schema({:__block__, _, contents}) do
%{
type: :object,
properties:
Enum.reduce(contents, %{}, fn function, acc ->
Map.merge(acc, generate_swagger_schema(function))
end)
}
end
# Support embedded_schema definitions within Swagger
def generate_swagger_schema({:embeds_many, _lines, [field, {:__aliases__, _, _} = als]}) do
schema = expand_nested_module_alias(als, Macro.Env.location(__ENV__))
opts = [type: :array, items: schema.__schema__()]
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({:embeds_one, _lines, [field, {:__aliases__, _, _} = als]}) do
schema = expand_nested_module_alias(als, Macro.Env.location(__ENV__))
%{field => schema.__schema__()}
end
# Support Goals :uuid type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :uuid, opts]}) do
opts = maybe_update_opt_value_for_ast(opts, :example)
opts =
Enum.into(opts, %{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:format, "uuid")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({maybe_required, _lines, [field, :uuid]}) do
opts =
%{}
|> Map.put(:type, :string)
|> Map.put(:format, "uuid")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Ectos :utc_datetime type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :utc_datetime, opts]}) do
opts =
opts
# date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z
|> Keyword.put_new(:example, "2024-08-12T21:00:39")
|> maybe_update_opt_value_for_ast(:example)
# ensure example is always a string
|> Keyword.update!(:example, fn example -> to_string(example) end)
|> Enum.into(%{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:format, "date-time")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({maybe_required, _lines, [field, :utc_datetime]}) do
opts =
%{}
|> Map.put(:type, :string)
|> Map.put(:format, "date-time")
# date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z
|> Map.put(:example, "2024-08-12T21:00:39")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Ectos :naive_datetime type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :naive_datetime, opts]}) do
opts =
opts
# date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z
|> Keyword.put_new(:example, "2024-08-12T21:00:39")
|> maybe_update_opt_value_for_ast(:example)
# ensure example is always a string
|> Keyword.update!(:example, fn example -> to_string(example) end)
|> Enum.into(%{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:format, "date-time")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({maybe_required, _lines, [field, :naive_datetime]}) do
opts =
%{}
|> Map.put(:type, :string)
|> Map.put(:format, "date-time")
# date-time notation as defined by RFC 3339, section 5.6, for example, 2017-07-21T17:32:28Z
|> Map.put(:example, "2024-08-12T21:00:39")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Ectos :date type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :date, opts]}) do
opts = maybe_update_opt_value_for_ast(opts, :example)
opts =
opts
|> then(fn opts ->
if Keyword.has_key?(opts, :example) do
Keyword.replace(opts, :example, to_string(opts[:example]))
else
opts
end
end)
|> Keyword.replace(:example, to_string(opts[:example] || ""))
|> Enum.into(%{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:format, "date")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({maybe_required, _lines, [field, :date]}) do
opts =
%{}
|> Map.put(:type, :string)
|> Map.put(:format, "date")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Goals :enum type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :enum, opts]}) do
opts = maybe_update_opt_value_for_ast(opts, :values)
opts =
opts
|> Keyword.put_new(:enum, Enum.map(opts[:values], &to_string/1))
|> Enum.into(%{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Goals :decimal type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :decimal]}) do
opts = %{type: :number, format: "double", required: maybe_required == :required}
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({maybe_required, _lines, [field, :decimal, opts]}) do
opts =
opts
|> maybe_update_opt_value_for_ast(:example)
|> maybe_update_opt_value_for_ast(:minimum)
|> maybe_update_opt_value_for_ast(:maximum)
|> then(fn opts ->
example = Keyword.get(opts, :example)
minimum = Keyword.get(opts, :minimum)
maximum = Keyword.get(opts, :maximum)
[example, minimum, maximum]
|> Enum.filter(& &1)
|> Enum.reduce(opts, fn value, acc ->
case value do
%Decimal{} ->
raise """
Do not use Decimal module with valspec. Value: #{inspect(value)}
"""
_ ->
nil
end
acc
end)
end)
|> Enum.into(%{})
|> Map.take(@schema_keys)
|> Map.put(:type, :number)
|> Map.put(:format, "double")
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Goals {:array, type} type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, {:array, subtype}, opts]}) do
# Case where subtype == :map
if do_block = Keyword.get(opts, :do) do
nested_schema = generate_swagger_schema(do_block)
nested_schema =
if Map.has_key?(nested_schema, :properties),
do: nested_schema,
else: %{properties: nested_schema}
opts = [
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: nested_schema.properties,
required: maybe_required == :required
}
]
%{field => struct(OpenApiSpex.Schema, opts)}
else
if subtype not in [:string, :integer], do: "Unsuported subtype #{inspect(subtype)} in array"
base_opts = maybe_update_opt_value_for_ast(opts, :example)
opts =
[
type: :array,
items: %OpenApiSpex.Schema{type: subtype, required: maybe_required == :required}
]
|> Keyword.merge(base_opts)
|> Enum.into(%{})
|> Map.take(@schema_keys)
%{field => struct(OpenApiSpex.Schema, opts)}
end
end
def generate_swagger_schema({maybe_required, _lines, [field, {:array, subtype}]}) do
if subtype not in [:string, :integer], do: "Unsuported subtype #{inspect(subtype)} in array"
opts =
[
type: :array,
items: %OpenApiSpex.Schema{type: subtype, required: maybe_required == :required}
]
|> Enum.into(%{})
|> Map.take(@schema_keys)
%{field => struct(OpenApiSpex.Schema, opts)}
end
# Support Goals :map type within Swagger
def generate_swagger_schema({maybe_required, _lines, [field, :map, opts]}) do
# Case where subtype == :map
if do_block = Keyword.get(opts, :do) do
nested_schema = generate_swagger_schema(do_block)
opts = [
type: :object,
properties: nested_schema.properties,
required: maybe_required == :required
]
%{field => struct(OpenApiSpex.Schema, opts)}
else
%{field => struct(OpenApiSpex.Schema, type: :object, required: maybe_required == :required)}
end
end
def generate_swagger_schema({maybe_required, _lines, [field, type]}) do
opts = %{type: type, required: maybe_required == :required}
%{field => struct(OpenApiSpex.Schema, opts)}
end
# maybe translate Goal string w/ includes into Swagger enum
def generate_swagger_schema({maybe_required, _lines, [field, :string, opts]}) do
if Keyword.has_key?(opts, :included) do
# handle possible included passed as AST during compile
{updated_included, _} = Code.eval_quoted(opts[:included])
opts = Keyword.put(opts, :values, updated_included)
generate_swagger_schema({maybe_required, nil, [field, :enum, opts]})
else
opts = maybe_update_opt_value_for_ast(opts, :example)
opts =
Enum.into(opts, %{})
|> Map.take(@schema_keys)
|> Map.put(:type, :string)
|> Map.put(:required, maybe_required == :required)
%{field => struct(OpenApiSpex.Schema, opts)}
end
end
def generate_swagger_schema({maybe_required, _lines, [field, type, opts]}) do
opts =
Enum.into(opts, %{})
|> Map.take(@schema_keys)
|> Map.put(:type, type)
opts = if maybe_required == :required, do: Map.put(opts, :required, true), else: opts
%{field => struct(OpenApiSpex.Schema, opts)}
end
def generate_swagger_schema({_type, _lines, _contents}) do
%{}
end
defp maybe_update_opt_value_for_ast(opts, key) do
if value = Keyword.get(opts, key) do
# handle possible value passed as AST during compile
{updated_value, _} = Code.eval_quoted(value)
Keyword.replace(opts, key, updated_value)
else
opts
end
end
# inspired by how ecto schema handles aliases within a schema.
defp expand_nested_module_alias({:__aliases__, _, [Elixir, _ | _] = alias}, _env),
do: Module.concat(alias)
defp expand_nested_module_alias({:__aliases__, _, [h | t]}, _env) when is_atom(h),
do: Module.concat([h | t])
defp expand_nested_module_alias(other, _env), do: other
@doc """
Generates a Ecto.Schema.t() from a valspec definition.
"""
def generate_embedded_schema({:__block__, _lines, contents}) do
Enum.reduce(contents, [], fn function, acc ->
[generate_embedded_schema(function) | acc]
end)
end
# translate swagger uuid -> ecto string
def generate_embedded_schema({:field, lines, [field, :uuid]}),
do: {:field, lines, [field, :string]}
def generate_embedded_schema({:field, lines, [field, :uuid, _opts]}),
do: {:field, lines, [field, :string]}
# translate swagger enum -> ecto string
def generate_embedded_schema({:field, lines, [field, :enum]}),
do: {:field, lines, [field, :string]}
def generate_embedded_schema({:field, lines, [field, :enum, _opts]}),
do: {:field, lines, [field, :string]}
def generate_embedded_schema({:field, lines, [field, type]}),
do: {:field, lines, [field, type]}
def generate_embedded_schema({:field, lines, [field, type, _opts]}),
do: {:field, lines, [field, type]}
def generate_embedded_schema(anything), do: anything
def add_callback_schema(callback_schema_module) do
schema = callback_schema_module.__schema__()
req_body = %OpenApiSpex.Operation{
requestBody: %OpenApiSpex.RequestBody{
content: %{
"application/json" => %OpenApiSpex.MediaType{
schema: schema
}
}
},
responses: %{
200 => %OpenApiSpex.Response{
description: "Your server returns this code if it accepts the callback"
}
}
}
%{
"#{schema.title}" => %{
"" => %OpenApiSpex.PathItem{post: req_body}
}
}
end
@doc """
Helper to possibly add the response schema opt to the Swagger operation
"""
def maybe_add_response_schema(base_opts, opts, default \\ nil) do
if response_schema = Keyword.get(opts, :response_schema) || default do
Keyword.merge(base_opts,
responses: %{201 => {"", "application/json", response_schema.__schema__()}}
)
else
base_opts
end
end
def maybe_add_callback_schemas(base_opts, opts, default \\ nil) do
if callback_schema_modules = Keyword.get(opts, :callback_schemas) || default do
callbacks =
[callback_schema_modules]
|> List.flatten()
|> Enum.reduce(%{}, fn callback_schema_module, acc ->
val = add_callback_schema(callback_schema_module)
Map.merge(acc, val)
end)
Keyword.merge(base_opts, callbacks: callbacks)
else
base_opts
end
end
def maybe_add_open_api_opts(base_opts, []), do: base_opts
def maybe_add_open_api_opts(base_opts, opts) do
Enum.reduce(opts, base_opts, fn opt, acc ->
Keyword.merge(acc, List.wrap(opt))
end)
end
end