diff --git a/lib/parameters.ex b/lib/parameters.ex index de6da63..f6593b9 100644 --- a/lib/parameters.ex +++ b/lib/parameters.ex @@ -1,7 +1,7 @@ defmodule Parameters do alias Parameters.{ - ParamsNode, - FieldNode + Params, + Field } defmacro __using__(_) do @@ -17,18 +17,15 @@ defmodule Parameters do def __on_definition__(%{module: module}, _kind, name, _args, _guards, _body) do if ast = Module.get_attribute(module, :parameters_block) do - Module.put_attribute(module, :parameters, ParamsNode.parse(name, ast)) + Module.put_attribute(module, :parameters, Params.parse(name, ast)) Module.delete_attribute(module, :parameters_block) end end defmacro __before_compile__(env) do - for schema <- Module.get_attribute(env.module, :parameters) do - parent = Module.concat(Parameters, env.module) - - quote do - unquote(define_schema(parent, schema)) - end + quote do + unquote(define_schemas(env.module)) + unquote(define_reflections()) end end @@ -69,8 +66,27 @@ defmodule Parameters do changeset_for(module, fun, params) end - defp define_schema(parent, node) do - module = Module.concat(parent, Macro.camelize("#{node.id}")) + defp define_reflections() do + quote do + def __parameters__ do + @parameters + end + end + end + + defp define_schemas(module) do + parameters = Module.get_attribute(module, :parameters) + parent = Module.concat(Parameters, module) + + for schema <- parameters do + quote do + unquote(define_schema(parent, schema)) + end + end + end + + defp define_schema(parent, params) do + module = Module.concat(parent, Macro.camelize("#{params.id}")) quote do defmodule unquote(module) do @@ -79,12 +95,12 @@ defmodule Parameters do @primary_key false embedded_schema do - unquote(define_fields(node.fields)) + unquote(define_fields(params.fields)) end - unquote(define_changeset(node.fields)) + unquote(define_changeset(params.fields)) - unquote(define_embeds(module, node.fields)) + unquote(define_embeds(module, params.fields)) end end end @@ -97,26 +113,26 @@ defmodule Parameters do defp define_fields(fields) do for field <- fields do - options = Keyword.take(field.options, [:default]) + opts = Keyword.take(field.opts, [:default]) case field do - %FieldNode{type: :map, fields: fields} when is_list(fields) -> + %Field{type: :map, fields: fields} when is_list(fields) -> quote do embeds_one unquote(field.id), Module.concat(__MODULE__, Macro.camelize("#{unquote(field.id)}")), - unquote(options) + unquote(opts) end - %FieldNode{type: :array, fields: fields} when is_list(fields) -> + %Field{type: :array, fields: fields} when is_list(fields) -> quote do embeds_many unquote(field.id), Module.concat(__MODULE__, Macro.camelize("#{unquote(field.id)}")), - unquote(options) + unquote(opts) end - %FieldNode{fields: nil} -> + %Field{fields: nil} -> quote do - field unquote(field.id), unquote(field.type), unquote(options) + field unquote(field.id), unquote(field.type), unquote(opts) end end end @@ -131,21 +147,21 @@ defmodule Parameters do required_fields = fields |> Enum.filter(fn field -> - is_nil(field.fields) and Keyword.fetch!(field.options, :required) + is_nil(field.fields) and Keyword.fetch!(field.opts, :required) end) |> Enum.map(fn field -> field.id end) optional_embeds = fields |> Enum.filter(fn field -> - not is_nil(field.fields) and not Keyword.fetch!(field.options, :required) + not is_nil(field.fields) and not Keyword.fetch!(field.opts, :required) end) |> Enum.map(fn field -> field.id end) required_embeds = fields |> Enum.filter(fn field -> - not is_nil(field.fields) and Keyword.fetch!(field.options, :required) + not is_nil(field.fields) and Keyword.fetch!(field.opts, :required) end) |> Enum.map(fn field -> field.id end) diff --git a/lib/parameters/field_node.ex b/lib/parameters/field.ex similarity index 69% rename from lib/parameters/field_node.ex rename to lib/parameters/field.ex index d72dd31..b081843 100644 --- a/lib/parameters/field_node.ex +++ b/lib/parameters/field.ex @@ -1,9 +1,9 @@ -defmodule Parameters.FieldNode do +defmodule Parameters.Field do defstruct [ :id, :type, :fields, - options: [] + opts: [] ] def parse({:__block__, _metadata, ast}), do: parse(ast) @@ -15,15 +15,15 @@ defmodule Parameters.FieldNode do opts = [required: name == :requires] fields = parse(ast) fields = if is_list(fields), do: fields, else: [fields] - struct(__MODULE__, id: field, type: type, options: opts, fields: fields) + struct(__MODULE__, id: field, type: type, opts: opts, fields: fields) [field, type, opts] -> opts = Keyword.put(opts, :required, name == :requires) - struct(__MODULE__, id: field, type: type, options: opts) + struct(__MODULE__, id: field, type: type, opts: opts) [field, type] -> opts = [required: name == :requires] - struct(__MODULE__, id: field, type: type, options: opts) + struct(__MODULE__, id: field, type: type, opts: opts) end end end diff --git a/lib/parameters/oas3.ex b/lib/parameters/oas3.ex new file mode 100644 index 0000000..d523496 --- /dev/null +++ b/lib/parameters/oas3.ex @@ -0,0 +1,131 @@ +defmodule Parameters.OAS3 do + def type_mapper(:map), do: :object + def type_mapper(:integer), do: :number + def type_mapper(:float), do: :number + def type_mapper(any), do: any + + def render_paths(routes, content_types) do + routes + |> Enum.group_by(& &1.path, &render_operation(&1, content_types)) + |> Enum.map(fn {key, val} -> {key, Map.new(val)} end) + |> Map.new() + end + + def render_operation(route, content_types) do + params = Enum.find(route.plug.__parameters__(), &(&1.id == route.plug_opts)) + fields = params && params.fields + + operation = + Map.new() + |> put_parameters(route, fields || [], content_types) + |> put_default_response(route, content_types) + + {route.verb, operation} + end + + def put_parameters(operation, %{verb: :get}, fields, _content_types) do + parameters = + for field <- fields do + Map.new( + in: "query", + name: field.id, + required: Keyword.get(field.opts, :required, false), + schema: Map.new(type: type_mapper(field.type)), + description: Keyword.get(field.opts, :description, "") + ) + end + + Map.put(operation, :parameters, parameters) + end + + def put_parameters(operation, route, fields, content_types) do + req_body = + Map.new( + required: true, + description: "" + ) + + content = + for pipeline <- route.pipe_through, + Map.has_key?(content_types, pipeline), + into: Map.new() do + content_type = Map.get(content_types, pipeline) + + required_fields = + for field <- fields, Keyword.get(field.opts, :required, false) do + field.id + end + + properties = + for field <- fields, into: Map.new() do + {field.id, Map.new(type: type_mapper(field.type))} + end + + example = + for field <- fields, into: Map.new() do + {field.id, type_mapper(field.type)} + end + + content = + Map.new( + schema: + Map.new( + required: required_fields, + properties: properties, + example: example + ) + ) + + {content_type, content} + end + + req_body = Map.put(req_body, :content, content) + + Map.put(operation, :requestBody, req_body) + end + + def put_default_response(operation, _route, _content_types) do + responses = Map.new(default: Map.new(description: "OK")) + + Map.put(operation, :responses, responses) + end + + defmacro __using__(opts) do + content_types = Keyword.fetch!(opts, :content_types) + accepts = Keyword.fetch!(opts, :accepts) + otp_app = Keyword.fetch!(opts, :otp_app) + info = Keyword.fetch!(opts, :info) + + quote do + @content_types Map.new(unquote(content_types)) + @accepts Map.new(unquote(accepts)) + @info Map.new(unquote(info)) + @otp_app unquote(otp_app) + + def render do + routes = __parameters__(:routes) + + Map.new( + openapi: "3.0.0", + info: __parameters__(:info), + paths: Parameters.OAS3.render_paths(routes, __parameters__(:content_types)) + ) + end + + def __parameters__(:info), do: @info + def __parameters__(:accepts), do: @accepts + def __parameters__(:content_types), do: @content_types + + def __parameters__(:routes) do + config = Application.get_env(@otp_app, __MODULE__) + + config + |> Keyword.fetch!(:router) + |> apply(:__routes__, []) + |> Enum.filter(fn route -> + function_exported?(route.plug, :__parameters__, 0) + end) + end + end + end +end diff --git a/lib/parameters/params_node.ex b/lib/parameters/params.ex similarity index 83% rename from lib/parameters/params_node.ex rename to lib/parameters/params.ex index 032ee48..52babf0 100644 --- a/lib/parameters/params_node.ex +++ b/lib/parameters/params.ex @@ -1,4 +1,4 @@ -defmodule Parameters.ParamsNode do +defmodule Parameters.Params do defstruct [ :id, fields: [] @@ -10,7 +10,7 @@ defmodule Parameters.ParamsNode do def parse(name, ast) when is_list(ast) do Enum.reduce(ast, struct(__MODULE__), fn field, schema -> Map.update!(schema, :fields, fn fields -> - [Parameters.FieldNode.parse(field) | fields] + [Parameters.Field.parse(field) | fields] end) end) |> Map.put(:id, name)