defmodule PhoenixSwagger.Schema do @moduledoc """ Struct and helpers for swagger schema. Fields should reflect the swagger SchemaObject specification. See http://swagger.io/specification/#schemaObject """ alias PhoenixSwagger.Schema @basic_types [:null, :boolean, :integer, :number, :string, :array, :object] defstruct [ :"$ref", :format, :title, :description, :default, :multipleOf, :maximum, :exclusiveMaximum, :minimum, :exclusiveMinimum, :maxLength, :minLength, :pattern, :maxItems, :minItems, :uniqueItems, :enum, :maxProperties, :minProperties, :required, :type, :items, :allOf, :properties, :additionalProperties, :discriminator, :example, :"x-nullable", :readOnly ] @doc """ Construct a new %Schema{} struct using the schema DSL. This macro is similar to PhoenixSwagger.swagger_schema, except that it produces a Schema struct instead of a plain map with string keys. ## Examples iex> require PhoenixSwagger.Schema, as: Schema ...> Schema.new do ...> type :object ...> properties do ...> name :string, "user name", required: true ...> date_of_birth :string, "date of birth", format: :datetime ...> end ...> end %Schema{ type: :object, properties: %{ name: %Schema { type: :string, description: "user name" }, date_of_birth: %Schema { type: :string, format: :datetime, description: "date of birth" } }, required: [:name] } """ defmacro new(block) do exprs = case block do [do: {:__block__, _, exprs}] -> exprs [do: expr] -> [expr] end body = Enum.reduce(exprs, Macro.escape(%Schema{type: :object}), fn expr, acc -> quote do unquote(acc) |> unquote(expr) end end) quote do (fn -> import PhoenixSwagger.Schema unquote(body) end).() end end @doc """ Construct a schema reference, using name of definition in this swagger document, or a complete path. ## Examples iex> PhoenixSwagger.Schema.ref(:User) %PhoenixSwagger.Schema{"$ref": "#/definitions/User"} iex> PhoenixSwagger.Schema.ref("../common/Error.json") %PhoenixSwagger.Schema{"$ref": "../common/Error.json"} """ def ref(name) when is_atom(name) do %Schema{"$ref": "#/definitions/#{name}"} end def ref(path) when is_binary(path) do %Schema{"$ref": path} end @doc """ Construct an array schema, where the array items schema is a ref to the given name. ## Examples iex> PhoenixSwagger.Schema.array(:User) %PhoenixSwagger.Schema{ items: %PhoenixSwagger.Schema{"$ref": "#/definitions/User"}, type: :array } iex> PhoenixSwagger.Schema.array(:string) %PhoenixSwagger.Schema{ items: %PhoenixSwagger.Schema{type: :string}, type: :array } """ def array(name) when name in @basic_types do %Schema{ type: :array, items: %Schema{ type: name } } end def array(name) when is_atom(name) do %Schema{ type: :array, items: ref(name) } end @doc """ Sets a property of the Schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.property(:name, :string, "Full name", required: true, maxLength: 256) ...> |> Schema.property(:address, [:string, "null"], "Street Address") ...> |> Schema.property(:friends, Schema.array(:User), "Friends list", required: true) %PhoenixSwagger.Schema{ type: :object, properties: %{ friends: %PhoenixSwagger.Schema{ type: :array, description: "Friends list", items: %PhoenixSwagger.Schema{"$ref": "#/definitions/User"} }, address: %PhoenixSwagger.Schema{ type: [:string, "null"], description: "Street Address" }, name: %PhoenixSwagger.Schema{ type: :string, description: "Full name", maxLength: 256 } }, required: [:friends, :name] } """ def property(model, name, type_or_schema, description \\ nil, opts \\ []) def property(model = %Schema{type: :object}, name, type, description, opts) when is_atom(type) or is_list(type) do property(model, name, %Schema{type: type}, description, opts) end def property(model = %Schema{type: :object}, name, type = %Schema{}, description, opts) do {required?, opts} = Keyword.pop(opts, :required) {nullable?, opts} = Keyword.pop(opts, :nullable) property_schema = struct!(type, [description: type.description || description] ++ opts) property_schema = if nullable?, do: %{property_schema | :"x-nullable" => true}, else: property_schema properties = (model.properties || %{}) |> Map.put(name, property_schema) model = %{model | properties: properties} if required?, do: required(model, name), else: model end @doc """ Defines multiple properties for a schema with a custom DSL syntax. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.properties do ...> name :string, "Full Name", required: true ...> dateOfBirth :string, "Date of birth", format: :datetime ...> end %PhoenixSwagger.Schema{ type: :object, properties: %{ name: %PhoenixSwagger.Schema{ type: :string, description: "Full Name", }, dateOfBirth: %PhoenixSwagger.Schema{ type: :string, description: "Date of birth", format: :datetime } }, required: [:name] } """ defmacro properties(model, block) do exprs = case block do [do: {:__block__, _, exprs}] -> exprs [do: expr] -> [expr] end body = exprs |> Enum.map(fn {:-, line, _args} = ast -> {:property, line, [get_dash_name(ast) | get_args(ast)]} {name, line, args} -> {:property, line, [name | args]} end) |> Enum.reduce(model, fn expr, acc -> quote do unquote(acc) |> unquote(expr) end end) quote do (fn -> import PhoenixSwagger.Schema unquote(body) end).() end end defp get_dash_name({:-, _line, [first, second]}), do: get_dash_name(first) <> "-" <> get_dash_name(second) defp get_dash_name({atom_name, _, _}), do: to_string(atom_name) defp get_args({:-, _line, [_first, second]}), do: get_args(second) defp get_args({_, _line, args}), do: args @doc """ Sets the format of a Schema with `type: :string`. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} |> Schema.format(:datetime) %PhoenixSwagger.Schema{type: :string, format: :datetime} """ def format(model = %Schema{type: :string}, format) do %{model | format: format} end @doc """ Sets the title of the schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{} |> Schema.title("User") %PhoenixSwagger.Schema{title: "User"} """ def title(model = %Schema{}, title) when is_binary(title) do %{model | title: title} end @doc """ Sets the description for the schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{} |> Schema.description("A user") %PhoenixSwagger.Schema{description: "A user"} """ def description(model = %Schema{}, desc) when is_binary(desc) do %{model | description: desc} end @doc """ Sets the default value for the schema. The value provided should validate against the schema sucessfully. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string, format: :datetime} ...> |> Schema.default("2017-01-01T00:00:00Z") %PhoenixSwagger.Schema{ type: :string, format: :datetime, default: "2017-01-01T00:00:00Z" } """ def default(model = %Schema{}, default) do %{model | default: default} end @doc """ Limits a values of numeric type (:integer or :number) to multiples of the given amount. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :number} ...> |> Schema.multiple_of(7) %PhoenixSwagger.Schema{type: :number, multipleOf: 7} """ def multiple_of(model = %Schema{}, number) when is_number(number) do %{model | multipleOf: number} end @doc """ Specifies a maximum numeric value for :integer or :number types. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :integer} ...> |> Schema.maximum(100) %PhoenixSwagger.Schema{type: :integer, maximum: 100} """ def maximum(model = %Schema{}, maximum) when is_number(maximum) do %{model | maximum: maximum} end @doc """ Boolean indicating that value for `maximum` is excluded from the valid range. When true: `x < maximum`, when false: `x <= maximum` ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :number} ...> |> Schema.maximum(128) ...> |> Schema.exclusive_maximum(true) %PhoenixSwagger.Schema{ type: :number, maximum: 128, exclusiveMaximum: true } """ def exclusive_maximum(model = %Schema{}, exclusive?) when is_boolean(exclusive?) do %{model | exclusiveMaximum: exclusive?} end @doc """ Specifies a minimum numeric value for `:integer` or `:number` types. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :integer} ...> |> Schema.minimum(10) %PhoenixSwagger.Schema{type: :integer, minimum: 10} """ def minimum(model = %Schema{}, minimum) when is_number(minimum) do %{model | minimum: minimum} end @doc """ Boolean indicating that value for `minimum` is excluded from the valid range. When true: `x > minimum`, when false: `x => minimum`. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :number} ...> |> Schema.minimum(16) ...> |> Schema.exclusive_minimum(true) %PhoenixSwagger.Schema{ type: :number, minimum: 16, exclusiveMinimum: true } """ def exclusive_minimum(model = %Schema{}, exclusive?) when is_boolean(exclusive?) do %{model | exclusiveMinimum: exclusive?} end @doc """ Constrains the maximum length of a string to the given value. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} ...> |> Schema.max_length(255) %PhoenixSwagger.Schema{type: :string, maxLength: 255} """ def max_length(model = %Schema{type: :string}, value) when is_number(value) and value >= 0 do %{model | maxLength: value} end @doc """ Constrains the minimum length of a string to the given value. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} ...> |> Schema.min_length(32) %PhoenixSwagger.Schema{type: :string, minLength: 32} """ def min_length(model = %Schema{type: :string}, value) when is_number(value) and value >= 0 do %{model | minLength: value} end @doc """ Restricts a string schema to match a regular expression, given as a string. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} ...> |> Schema.pattern(~S"^(\([0-9]{2}\))?[0-9]{4}-[0-9]{4}$") %PhoenixSwagger.Schema{type: :string, pattern: ~S"^(\([0-9]{2}\))?[0-9]{4}-[0-9]{4}$"} """ def pattern(model = %Schema{type: :string}, regex_pattern) when is_binary(regex_pattern) do %{model | pattern: regex_pattern} end @doc """ Restricts the maximimum number of items in an `array` schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :array} ...> |> Schema.max_items(25) %PhoenixSwagger.Schema{type: :array, maxItems: 25} """ def max_items(model = %Schema{type: :array}, value) when is_number(value) and value >= 0 do %{model | maxItems: value} end @doc """ Restricts the minimum number of items in an `array` schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :array} ...> |> Schema.min_items(1) %PhoenixSwagger.Schema{type: :array, minItems: 1} """ def min_items(model = %Schema{type: :array}, value) when is_number(value) and value >= 0 do %{model | minItems: value} end @doc """ Boolean that when true, requires each item of an `array` schema to be unique. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :array} ...> |> Schema.unique_items(true) %PhoenixSwagger.Schema{type: :array, uniqueItems: true} """ def unique_items(model = %Schema{type: :array}, unique?) when is_boolean(unique?) do %{model | uniqueItems: unique?} end @doc """ Restricts the schema to a fixed set of values. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} ...> |> Schema.enum(["red", "yellow", "green"]) %PhoenixSwagger.Schema{type: :string, enum: ["red", "yellow", "green"]} """ def enum(model = %Schema{}, values) when is_list(values) do %{model | enum: values} end @doc """ Limits the maximum number of properties an `object` schema may contain. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.max_properties(10) %PhoenixSwagger.Schema{type: :object, maxProperties: 10} """ def max_properties(model = %Schema{}, value) when is_number(value) and value >= 0 do %{model | maxProperties: value} end @doc """ Limits the minimum number of properties an `object` schema may contain. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.min_properties(1) %PhoenixSwagger.Schema{type: :object, minProperties: 1} """ def min_properties(model = %Schema{}, value) when is_number(value) and value >= 0 do %{model | minProperties: value} end @doc """ Sets the type of for the schema. Valid values are `:string`, `:integer`, `:number`, `:object`, `:array`, `:boolean`, `:null`, or a list of those basic types. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{} ...> |> Schema.type(:string) %PhoenixSwagger.Schema{type: :string} iex> alias PhoenixSwagger.Schema ...> %Schema{} ...> |> Schema.type([:string, :integer]) %PhoenixSwagger.Schema{type: [:string, :integer]} """ def type(model = %Schema{}, type) when is_atom(type) or is_list(type) do %{model | type: type} end @doc """ Sets the schema/s for the items of an array. Use a single schema for arrays when each item should have the same schema. Use a list of schemas when the array represents a tuple, each element will be validated against the corresponding schema in the `items` list. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :array} ...> |> Schema.items(%Schema{type: :string}) %PhoenixSwagger.Schema{type: :array, items: %PhoenixSwagger.Schema{type: :string}} iex> alias PhoenixSwagger.Schema ...> %Schema{type: :array} ...> |> Schema.items([%Schema{type: :string}, %Schema{type: :number}]) %PhoenixSwagger.Schema{ type: :array, items: [%PhoenixSwagger.Schema{type: :string}, %PhoenixSwagger.Schema{type: :number}] } """ def items(model = %Schema{type: :array}, item_schema) when is_map(item_schema) or is_list(item_schema) do %{model | items: item_schema} end @doc """ Used to combine multiple schemas, requiring a value to conform to all schemas. Can be used in conjunction with `discriminator` to define polymorphic inheritance relationships. See http://swagger.io/specification/#composition-and-inheritance--polymorphism--83 ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{} ...> |> Schema.all_of([Schema.ref("#definitions/Contact"), Schema.ref("#definitions/CreditHistory")]) %PhoenixSwagger.Schema{ allOf: [ %PhoenixSwagger.Schema{'$ref': "#definitions/Contact"}, %PhoenixSwagger.Schema{'$ref': "#definitions/CreditHistory"}, ] } """ def all_of(model = %Schema{}, schemas) when is_list(schemas) do %{model | allOf: schemas} end @doc """ Boolean indicating that additional properties are allowed, or a schema to be used for validating any additional properties not listed in `properties`. Default behaviour is to allow additional properties. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.property(:name, :string, "Full name", maxLength: 255) ...> |> Schema.additional_properties(false) %PhoenixSwagger.Schema{ type: :object, properties: %{ name: %PhoenixSwagger.Schema{ type: :string, description: "Full name", maxLength: 255 } }, additionalProperties: false } iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object} ...> |> Schema.property(:name, :string, "Full name", maxLength: 255) ...> |> Schema.additional_properties(%Schema{type: :number}) %PhoenixSwagger.Schema{ type: :object, properties: %{ name: %PhoenixSwagger.Schema{ type: :string, description: "Full name", maxLength: 255 } }, additionalProperties: %PhoenixSwagger.Schema{ type: :number } } """ def additional_properties(model = %Schema{type: :object}, bool_or_schema) do %{model | additionalProperties: bool_or_schema} end @doc """ Makes one or more properties required in an object schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object, properties: %{phone_number: %Schema{type: :string}}} ...> |> Schema.required(:phone_number) %PhoenixSwagger.Schema{ type: :object, properties: %{ phone_number: %PhoenixSwagger.Schema{ type: :string } }, required: [:phone_number] } iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object, properties: %{phone_number: %Schema{type: :string}, address: %Schema{type: :string}}} ...> |> Schema.required([:phone_number, :address]) %PhoenixSwagger.Schema{ type: :object, properties: %{ phone_number: %PhoenixSwagger.Schema{ type: :string }, address: %PhoenixSwagger.Schema{ type: :string } }, required: [:phone_number, :address] } """ def required(model = %Schema{type: :object}, name) when is_atom(name) or is_binary(name) do required(model, [name]) end def required(model = %Schema{type: :object}, names) when is_list(names) do %{model | required: names ++ (model.required || [])} end @doc """ Adds an example of the schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :object, properties: %{phone_number: %Schema{type: :string}}} ...> |> Schema.example(%{phone_number: "555-123-456"}) %PhoenixSwagger.Schema{ type: :object, properties: %{ phone_number: %PhoenixSwagger.Schema{ type: :string } }, example: %{phone_number: "555-123-456"} } """ def example(model = %Schema{}, example) do %{model | example: example} end @doc """ Specifies the name of a property that identifies the polymorphic schema for a JSON object. The value of this property should be the name of this schema, or another schema that inherits from this schema using `all_of`. See http://swagger.io/specification/#composition-and-inheritance--polymorphism--83 ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{} ...> |> Schema.type(:object) ...> |> Schema.title("Pet") ...> |> Schema.property(:pet_type, :string, "polymorphic pet schema type", example: "Dog") ...> |> Schema.discriminator(:pet_type) %Schema{ type: :object, title: "Pet", properties: %{ pet_type: %Schema{ type: :string, description: "polymorphic pet schema type", example: "Dog" } }, discriminator: :pet_type, required: [:pet_type] } """ def discriminator(model = %Schema{}, property_name) do model |> Map.put(:discriminator, property_name) |> required(property_name) end @doc """ Sets the `x-nullable` vendor extension property for the schema. ## Examples iex> alias PhoenixSwagger.Schema ...> %Schema{type: :string} |> Schema.nullable(true) %Schema{ type: :string, 'x-nullable': true } """ @spec nullable(%Schema{}, maybe_boolean) :: %Schema{} when maybe_boolean: true | false | nil def nullable(model = %Schema{}, value) when value in [true, false, nil] do %{model | "x-nullable": value} end end