defmodule PhoenixSwagger.JsonApi do @moduledoc """ This module defines a DSL for defining swagger definitions in a JSON-API conformant format. ## Examples use PhoenixSwagger def swagger_definitions do %{ UserResource: JsonApi.resource do description "A user that may have one or more supporter pages." attributes do phone :string, "Users phone number" full_name :string, "Full name" user_updated_at :string, "Last update timestamp UTC", format: "ISO-8601" user_created_at :string, "First created timestamp UTC" email :string, "Email", required: true birthday :string, "Birthday in YYYY-MM-DD format" address Schema.ref(:Address), "Users address" end link :self, "The link to this user resource" relationship :posts end, Users: JsonApi.page(:UserResource), User: JsonApi.single(:UserResource) } end swagger_path :index do get "/api/v1/users" paging size: "page[size]", number: "page[number]" response 200, "OK", Schema.ref(:Users) end """ alias PhoenixSwagger.Schema require PhoenixSwagger @doc """ Defines a schema for a top level json-api document with an array of resources as primary data. The given `resource` should be the name of a JSON-API resource defined with the `resource/1` macro """ def page(resource) do %Schema{ type: :object, description: "A page of [#{resource}](##{resource |> to_string |> String.downcase()}) results", properties: %{ meta: %Schema{ type: :object, properties: %{ "total-pages": %Schema{ type: :integer, description: "The total number of pages available" }, "total-count": %Schema{ type: :integer, description: "The total number of items available" } } }, links: %Schema{ type: :object, properties: %{ self: %Schema{ type: :string, description: "Link to this page of results" }, prev: %Schema{ type: :string, description: "Link to the previous page of results" }, next: %Schema{ type: :string, description: "Link to the next page of results" }, last: %Schema{ type: :string, description: "Link to the last page of results" }, first: %Schema{ type: :string, description: "Link to the first page of results" } } }, data: %Schema{ type: :array, description: "Content with [#{resource}](##{resource |> to_string |> String.downcase()}) objects", items: %Schema{ "$ref": "#/definitions/#{resource}" } } }, required: [:data] } |> PhoenixSwagger.to_json() end @doc """ Defines a schema for a top level json-api document with a single primary data resource. The given `resource` should be the name of a JSON-API resource defined with the `resource/1` macro """ def single(resource) do %Schema{ type: :object, description: "A JSON-API document with a single [#{resource}](##{ resource |> to_string |> String.downcase() }) resource", properties: %{ links: %Schema{ type: :object, properties: %{ self: %Schema{ type: :string, description: "the link that generated the current response document." } } }, data: %Schema{ "$ref": "#/definitions/#{resource}" }, included: %Schema{ type: :array, description: "Included resources", items: %Schema{ type: :object, properties: %{ type: %Schema{type: :string, description: "The JSON-API resource type"}, id: %Schema{type: :string, description: "The JSON-API resource ID"} } } } }, required: [:data] } |> PhoenixSwagger.to_json() end @doc """ Defines a schema for a JSON-API resource, without the enclosing top-level document. """ defmacro resource(do: {:__block__, _, exprs}) do schema = quote do %Schema{ type: :object, properties: %{ type: %Schema{type: :string, description: "The JSON-API resource type"}, id: %Schema{type: :string, description: "The JSON-API resource ID"}, relationships: %Schema{type: :object, properties: %{}}, links: %Schema{type: :object, properties: %{}}, attributes: %Schema{ type: :object, properties: %{} } } } end body = Enum.reduce(exprs, schema, fn expr, acc -> quote do unquote(acc) |> unquote(expr) end end) quote do (fn -> import PhoenixSwagger.JsonApi import PhoenixSwagger.Schema unquote(body) |> PhoenixSwagger.to_json() end).() end end @doc """ Defines a block of attributes for a JSON-API resource. Within this block, each function call will be translated into a call to the `PhoenixSwagger.JsonApi.attribute` function. ## Examples description("A User") attributes do name :string, "Full name of the user", required: true dateOfBirth :string, "Date of Birth", format: "ISO-8601", required: false end Translates to: description("A User") |> attribute(:name, :string, "Full name of the user", required: true) |> attribute(:dateOfBirth, :string, "Date of Birth", format: "ISO-8601", required: false) """ defmacro attributes(model, block) do attrs = case block do [do: {:__block__, _, attrs}] -> attrs [do: attr] -> [attr] end attrs |> Enum.map(fn {name, line, args} -> {:attribute, line, [name | args]} end) |> Enum.reduce(model, fn next, pipeline -> quote do unquote(pipeline) |> unquote(next) end end) end @doc """ Defines an attribute in a JSON-API schema. Name, type and description are accepted as positional arguments, but any other schema properties can be set through the trailing keyword arguments list. As a convenience, required: true can be passed in the keyword args, causing the name of this attribute to be added to the "required" list of the attributes schema. """ def attribute(model, name, type, description, opts \\ []) def attribute(model, name, type, description, opts) when is_atom(type) or is_list(type) do attribute(model, name, %Schema{type: type}, description, opts) end def attribute(model = %Schema{}, name, type = %Schema{}, description, opts) do {required?, opts} = Keyword.pop(opts, :required) attr_schema = struct!(type, [description: description] ++ opts) model = put_in(model.properties.attributes.properties[name], attr_schema) required = case {model.properties.attributes.required, required?} do {nil, true} -> [name] {r, true} -> r ++ [name] {r, _} -> r end put_in(model.properties.attributes.required, required) end @doc """ Defines a link with name and description. """ def link(model = %Schema{}, name, description) do put_in( model.properties.links.properties[name], %Schema{type: :string, description: description} ) end @doc """ Defines a relationship. Optionally can pass `type: :has_many` or `type: :has_one` to determine whether to structure the relationship as an object or array. Defaults to `:has_one` """ @spec relationship(%Schema{}, name :: atom, [option]) :: %Schema{} when option: {:type, relationship_type} | {:nullable, boolean}, relationship_type: :has_one | :has_many def relationship(model = %Schema{}, name, opts \\ []) do type = opts[:type] || :has_one put_in(model.properties.relationships.properties[name], %Schema{ type: :object, properties: %{ links: %Schema{ type: :object, properties: %{ self: %Schema{type: :string, description: "Relationship link for #{name}"}, related: %Schema{type: :string, description: "Related #{name} link"} } }, data: relationship_data(type, name) |> Schema.nullable(opts[:nullable]) } }) end defp relationship_data(:has_one, name) do %Schema{ type: :object, properties: %{ id: %Schema{type: :string, description: "Related #{name} resource id"}, type: %Schema{type: :string, description: "Type of related #{name} resource"} } } end defp relationship_data(:has_many, name) do %Schema{ type: :array, items: %Schema{ type: :object, properties: %{ id: %Schema{type: :string, description: "Related #{name} resource id"}, type: %Schema{type: :string, description: "Type of related #{name} resource"} } } } end end