From 01d12f9531bb494f60d091f9609225ee06d80c02 Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Tue, 9 Jul 2019 01:04:38 +0800 Subject: [PATCH 1/5] introduce reflection function --- lib/parameters.ex | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/parameters.ex b/lib/parameters.ex index de6da63..fd4d738 100644 --- a/lib/parameters.ex +++ b/lib/parameters.ex @@ -23,12 +23,9 @@ defmodule Parameters do 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,6 +66,25 @@ defmodule Parameters do changeset_for(module, fun, params) end + 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, node) do module = Module.concat(parent, Macro.camelize("#{node.id}")) From 8834deaf3f0eba40724ee46c224ee7d3058e057c Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Tue, 9 Jul 2019 02:50:13 +0800 Subject: [PATCH 2/5] Proof of concept --- lib/parameters/oas3.ex | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 lib/parameters/oas3.ex diff --git a/lib/parameters/oas3.ex b/lib/parameters/oas3.ex new file mode 100644 index 0000000..294f429 --- /dev/null +++ b/lib/parameters/oas3.ex @@ -0,0 +1,89 @@ +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 + + defmacro __using__(opts) do + version = Keyword.fetch!(opts, :version) + title = Keyword.fetch!(opts, :title) + content_types = Keyword.fetch!(opts, :content_types) + accepts = Keyword.fetch!(opts, :accepts) + + quote do + def generate do + config = Application.get_env(unquote(opts[:otp_app]), __MODULE__) + router = Keyword.fetch!(config, :router) + routes = router.__routes__() + accepts = unquote(accepts) + content_types = unquote(content_types) + + spec = %{ + openapi: "3.0.0", + info: %{ + title: unquote(title), + version: unquote(version) + } + } + + key_fn = fn route -> route.path end + val_fn = fn route -> + parameters = route.plug.__parameters__() + node = Enum.find(parameters, fn node -> node.id == route.plug_opts end) + + key = "#{route.verb}" + val = %{ + requestBody: %{ + required: true, + description: "", + content: for pipeline <- route.pipe_through, into: %{} do + key = Keyword.fetch!(content_types, pipeline) + + val = %{ + schema: %{ + required: for field <- node.fields, field.options[:required] do + field.id + end, + properties: for field <- node.fields, into: %{} do + {field.id, %{ + type: Parameters.OpenAPI.type_mapper(field.type) + }} + end, + } + } + + {key, val} + end, + }, + responses: %{ + default: %{ + description: "", + content: for pipeline <- route.pipe_through, into: %{} do + key = Keyword.fetch!(content_types, pipeline) + + val = %{ + schema: %{ + type: :object + } + } + + {key, val} + end, + } + }, + } + + {key, val} + end + + paths = + routes + |> Enum.group_by(key_fn, val_fn) + |> Enum.map(fn {key, val} -> {key, Enum.into(val, %{})} end) + |> Enum.into(%{}) + + Map.put(spec, :paths, paths) + end + end + end +end \ No newline at end of file From 9c12598208127037136983c5e10058c6792dba52 Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Sat, 3 Aug 2019 12:10:53 +0800 Subject: [PATCH 3/5] WIP --- lib/parameters.ex | 36 ++-- lib/parameters/{field_node.ex => field.ex} | 10 +- lib/parameters/oas3.ex | 205 ++++++++++++------- lib/parameters/{params_node.ex => params.ex} | 4 +- 4 files changed, 156 insertions(+), 99 deletions(-) rename lib/parameters/{field_node.ex => field.ex} (69%) rename lib/parameters/{params_node.ex => params.ex} (83%) diff --git a/lib/parameters.ex b/lib/parameters.ex index fd4d738..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,7 +17,7 @@ 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 @@ -85,8 +85,8 @@ defmodule Parameters do end end - defp define_schema(parent, node) do - module = Module.concat(parent, Macro.camelize("#{node.id}")) + defp define_schema(parent, params) do + module = Module.concat(parent, Macro.camelize("#{params.id}")) quote do defmodule unquote(module) do @@ -95,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 @@ -113,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 @@ -147,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 index 294f429..fdf43fa 100644 --- a/lib/parameters/oas3.ex +++ b/lib/parameters/oas3.ex @@ -4,85 +4,142 @@ defmodule Parameters.OAS3 do 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)) + + operation = + Map.new() + |> put_parameters(route, params.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 + content = + for pipeline <- route.pipe_through, Map.has_key?(content_types, pipeline), into: Map.new() do + content_type = Map.get(content_types, pipeline) + content = Map.new( + schema: Map.new( + type: :object + ) + ) + + {content_type, content} + end + + responses = Map.new( + default: Map.new( + description: "", + content: content + ) + ) + + Map.put(operation, :responses, responses) + end + + defmacro __using__(opts) do - version = Keyword.fetch!(opts, :version) - title = Keyword.fetch!(opts, :title) 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 - def generate do - config = Application.get_env(unquote(opts[:otp_app]), __MODULE__) - router = Keyword.fetch!(config, :router) - routes = router.__routes__() - accepts = unquote(accepts) - content_types = unquote(content_types) - - spec = %{ - openapi: "3.0.0", - info: %{ - title: unquote(title), - version: unquote(version) - } - } - - key_fn = fn route -> route.path end - val_fn = fn route -> - parameters = route.plug.__parameters__() - node = Enum.find(parameters, fn node -> node.id == route.plug_opts end) - - key = "#{route.verb}" - val = %{ - requestBody: %{ - required: true, - description: "", - content: for pipeline <- route.pipe_through, into: %{} do - key = Keyword.fetch!(content_types, pipeline) - - val = %{ - schema: %{ - required: for field <- node.fields, field.options[:required] do - field.id - end, - properties: for field <- node.fields, into: %{} do - {field.id, %{ - type: Parameters.OpenAPI.type_mapper(field.type) - }} - end, - } - } - - {key, val} - end, - }, - responses: %{ - default: %{ - description: "", - content: for pipeline <- route.pipe_through, into: %{} do - key = Keyword.fetch!(content_types, pipeline) - - val = %{ - schema: %{ - type: :object - } - } - - {key, val} - end, - } - }, - } - - {key, val} - end - - paths = - routes - |> Enum.group_by(key_fn, val_fn) - |> Enum.map(fn {key, val} -> {key, Enum.into(val, %{})} end) - |> Enum.into(%{}) - - Map.put(spec, :paths, paths) + @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( + 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 -> + route.plug + |> apply(:__parameters__, []) + |> Enum.any?(fn params -> params.id == route.plug_opts 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) From e6a1c0bfe07898a3d9d25a6af7aa3846827c2b4a Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Sat, 3 Aug 2019 12:28:42 +0800 Subject: [PATCH 4/5] Filter out modules without parameters --- lib/parameters/oas3.ex | 66 ++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/parameters/oas3.ex b/lib/parameters/oas3.ex index fdf43fa..806c1ce 100644 --- a/lib/parameters/oas3.ex +++ b/lib/parameters/oas3.ex @@ -6,7 +6,7 @@ defmodule Parameters.OAS3 do def render_paths(routes, content_types) do routes - |> Enum.group_by(&(&1.path), &render_operation(&1, content_types)) + |> Enum.group_by(& &1.path, &render_operation(&1, content_types)) |> Enum.map(fn {key, val} -> {key, Map.new(val)} end) |> Map.new() end @@ -29,9 +29,7 @@ defmodule Parameters.OAS3 do in: "query", name: field.id, required: Keyword.get(field.opts, :required, false), - schema: Map.new( - type: type_mapper(field.type) - ), + schema: Map.new(type: type_mapper(field.type)), description: Keyword.get(field.opts, :description, "") ) end @@ -40,13 +38,16 @@ defmodule Parameters.OAS3 do end def put_parameters(operation, route, fields, content_types) do - req_body = Map.new( - required: true, - description: "" - ) + req_body = + Map.new( + required: true, + description: "" + ) content = - for pipeline <- route.pipe_through, Map.has_key?(content_types, pipeline), into: Map.new() do + for pipeline <- route.pipe_through, + Map.has_key?(content_types, pipeline), + into: Map.new() do content_type = Map.get(content_types, pipeline) required_fields = @@ -55,22 +56,24 @@ defmodule Parameters.OAS3 do end properties = - for field <- fields, into: Map.new do + 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 + 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 = + Map.new( + schema: + Map.new( + required: required_fields, + properties: properties, + example: example + ) + ) {content_type, content} end @@ -81,29 +84,11 @@ defmodule Parameters.OAS3 do end def put_default_response(operation, route, content_types) do - content = - for pipeline <- route.pipe_through, Map.has_key?(content_types, pipeline), into: Map.new() do - content_type = Map.get(content_types, pipeline) - content = Map.new( - schema: Map.new( - type: :object - ) - ) - - {content_type, content} - end - - responses = Map.new( - default: Map.new( - description: "", - content: content - ) - ) + 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) @@ -120,6 +105,7 @@ defmodule Parameters.OAS3 do routes = __parameters__(:routes) Map.new( + openapi: "3.0.0", info: __parameters__(:info), paths: Parameters.OAS3.render_paths(routes, __parameters__(:content_types)) ) @@ -136,11 +122,9 @@ defmodule Parameters.OAS3 do |> Keyword.fetch!(:router) |> apply(:__routes__, []) |> Enum.filter(fn route -> - route.plug - |> apply(:__parameters__, []) - |> Enum.any?(fn params -> params.id == route.plug_opts end) + function_exported?(route.plug, :__parameters__, 0) end) end end end -end \ No newline at end of file +end From 09a05cae5588f226bfb6083d2f12455cf4ef88ba Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Tue, 20 Aug 2019 03:14:49 +0800 Subject: [PATCH 5/5] Handle empty fields --- lib/parameters/oas3.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/parameters/oas3.ex b/lib/parameters/oas3.ex index 806c1ce..d523496 100644 --- a/lib/parameters/oas3.ex +++ b/lib/parameters/oas3.ex @@ -13,10 +13,11 @@ defmodule Parameters.OAS3 do 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, params.fields, content_types) + |> put_parameters(route, fields || [], content_types) |> put_default_response(route, content_types) {route.verb, operation} @@ -83,7 +84,7 @@ defmodule Parameters.OAS3 do Map.put(operation, :requestBody, req_body) end - def put_default_response(operation, route, content_types) do + def put_default_response(operation, _route, _content_types) do responses = Map.new(default: Map.new(description: "OK")) Map.put(operation, :responses, responses)