Page MenuHomePhorge

No OneTemporary

Size
39 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index a28badf..220bf77 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,82 +1,82 @@
defmodule OpenApiSpex do
alias OpenApiSpex.{OpenApi, Operation, Reference, Schema, SchemaResolver}
@moduledoc """
"""
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
@doc """
Cast params to conform to a Schema or Operation spec.
"""
+ @spec cast(OpenApi.t, Schema.t | Reference.t, any) :: {:ok, any} | {:error, String.t}
def cast(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, operation = %Operation{}, params, content_type \\ nil) do
Operation.cast(operation, params, content_type, spec.components.schemas)
end
-
@doc """
Validate params against a Schema or Operation spec.
"""
def validate(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, operation = %Operation{}, params = %{}, content_type \\ nil) do
Operation.validate(operation, params, content_type, spec.components.schemas)
end
@doc """
Declares a OpenApiSpex Schema
- defines the schema/0 callback
- ensures the schema is linked to the module by "x-struct" extension property
- defines a struct with keys matching the schema properties
- defines a @type `t` for the struct
- derives a `Poison.Encoder` for the struct
## Example
require OpenApiSpex
defmodule User do
OpenApiSpex.schema %{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :'date-time'},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :'date-time'}
},
required: [:name, :email],
example: %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
end
"""
defmacro schema(body) do
quote do
@schema struct(OpenApiSpex.Schema, Map.put(unquote(body), :"x-struct", __MODULE__))
def schema, do: @schema
@derive [Poison.Encoder]
defstruct Map.keys(@schema.properties)
@type t :: %__MODULE__{}
end
end
end
diff --git a/lib/open_api_spex/components.ex b/lib/open_api_spex/components.ex
index 40b7c62..a19c199 100644
--- a/lib/open_api_spex/components.ex
+++ b/lib/open_api_spex/components.ex
@@ -1,28 +1,29 @@
defmodule OpenApiSpex.Components do
alias OpenApiSpex.{
Schema, Reference, Response, Parameter, Example,
- RequestBody, Header, SecurityScheme, Link, Callback
+ RequestBody, Header, SecurityScheme, Link, Callback,
+ Components
}
defstruct [
:schemas,
:responses,
:parameters,
:examples,
:requestBodies,
:headers,
:securitySchemes,
:links,
:callbacks,
]
- @type t :: %{
+ @type t :: %Components{
schemas: %{String.t => Schema.t | Reference.t},
responses: %{String.t => Response.t | Reference.t},
parameters: %{String.t => Parameter.t | Reference.t},
examples: %{String.t => Example.t | Reference.t},
requestBodies: %{String.t => RequestBody.t | Reference.t},
headers: %{String.t => Header.t | Reference.t},
securitySchemes: %{String.t => SecurityScheme.t | Reference.t},
links: %{String.t => Link.t | Reference.t},
callbacks: %{String.t => Callback.t | Reference.t}
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/example.ex b/lib/open_api_spex/example.ex
index a36e0eb..d03eee7 100644
--- a/lib/open_api_spex/example.ex
+++ b/lib/open_api_spex/example.ex
@@ -1,14 +1,14 @@
defmodule OpenApiSpex.Example do
defstruct [
:summary,
:description,
:value,
:externalValue
]
- @type t :: %{
+ @type t :: %__MODULE__{
summary: String.t,
description: String.t,
value: any,
externalValue: String.t
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/link.ex b/lib/open_api_spex/link.ex
index 9b2aa15..a8220d8 100644
--- a/lib/open_api_spex/link.ex
+++ b/lib/open_api_spex/link.ex
@@ -1,19 +1,19 @@
defmodule OpenApiSpex.Link do
alias OpenApiSpex.Server
defstruct [
:operationRef,
:operationId,
:parameters,
:requestBody,
:description,
:server
]
- @type t :: %{
+ @type t :: %__MODULE__{
operationRef: String.t,
operationId: String.t,
parameters: %{String.t => any},
requestBody: any,
description: String.t,
server: Server.t
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/operation.ex b/lib/open_api_spex/operation.ex
index c204785..2bc4f96 100644
--- a/lib/open_api_spex/operation.ex
+++ b/lib/open_api_spex/operation.ex
@@ -1,184 +1,184 @@
defmodule OpenApiSpex.Operation do
alias OpenApiSpex.{
Callback,
ExternalDocumentation,
MediaType,
Operation,
Parameter,
Reference,
RequestBody,
Response,
Responses,
Schema,
SecurityRequirement,
Server,
}
defstruct [
:tags,
:summary,
:description,
:externalDocs,
:operationId,
:parameters,
:requestBody,
:responses,
:callbacks,
:deprecated,
:security,
:servers
]
@type t :: %__MODULE__{
tags: [String.t],
summary: String.t,
description: String.t,
externalDocs: ExternalDocumentation.t,
operationId: String.t,
parameters: [Parameter.t | Reference.t],
requestBody: [RequestBody.t | Reference.t],
responses: Responses.t,
callbacks: %{
String.t => Callback.t | Reference.t
},
deprecated: boolean,
security: [SecurityRequirement.t],
servers: [Server.t]
}
@doc """
Constructs an Operation struct from the plug and opts specified in the given route
"""
@spec from_route(PathItem.route) :: t
def from_route(route) do
from_plug(route.plug, route.opts)
end
@doc """
Constructs an Operation struct from plug module and opts
"""
@spec from_plug(module, any) :: t
def from_plug(plug, opts) do
plug.open_api_operation(opts)
end
@doc """
Shorthand for constructing a Parameter name, location, type, description and optional examples
"""
- @spec parameter(atom, atom, atom, String.t, keyword) :: RequestBody.t
+ @spec parameter(atom, Parameter.location, Reference.t | Schema.t | atom, String.t, keyword) :: Parameter.t
def parameter(name, location, type, description, opts \\ []) do
params =
[name: name, in: location, description: description, required: location == :path]
|> Keyword.merge(opts)
Parameter
|> struct(params)
|> Parameter.put_schema(type)
end
@doc """
Shorthand for constructing a RequestBody with description, media_type, schema and optional examples
"""
@spec request_body(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: RequestBody.t
def request_body(description, media_type, schema_ref, opts \\ []) do
%RequestBody{
description: description,
content: %{
media_type => %MediaType{
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
},
required: opts[:required] || false
}
end
@doc """
Shorthand for constructing a Response with description, media_type, schema and optional examples
"""
@spec response(String.t, String.t, (Schema.t | Reference.t | module), keyword) :: Response.t
def response(description, media_type, schema_ref, opts \\ []) do
%Response{
description: description,
content: %{
media_type => %MediaType {
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}
}
}
end
@doc """
Cast params to the types defined by the schemas of the operation parameters and requestBody
"""
@spec cast(Operation.t, map, String.t | nil, %{String.t => Schema.t}) :: {:ok, map} | {:error, String.t}
def cast(operation = %Operation{}, params = %{}, content_type, schemas) do
parameters = Enum.filter(operation.parameters, fn p -> Map.has_key?(params, Atom.to_string(p.name)) end)
with {:ok, parameter_values} <- cast_parameters(parameters, params, schemas),
{:ok, body} <- cast_request_body(operation.requestBody, params, content_type, schemas) do
{:ok, Map.merge(parameter_values, body)}
end
end
def cast_parameters([], _params, _schemas), do: {:ok, %{}}
def cast_parameters([p | rest], params = %{}, schemas) do
with {:ok, cast_val} <- Schema.cast(Parameter.schema(p), params[Atom.to_string(p.name)], schemas),
{:ok, cast_tail} <- cast_parameters(rest, params, schemas) do
{:ok, Map.put_new(cast_tail, p.name, cast_val)}
end
end
def cast_request_body(nil, _, _, _), do: {:ok, %{}}
def cast_request_body(%RequestBody{content: content}, params, content_type, schemas) do
schema = content[content_type].schema
Schema.cast(schema, params, schemas)
end
@doc """
Validate params against the schemas of the operation parameters and requestBody
"""
@spec validate(Operation.t, map, String.t | nil, %{String.t => Schema.t}) :: :ok | {:error, String.t}
def validate(operation = %Operation{}, params = %{}, content_type, schemas) do
with :ok <- validate_required_parameters(operation.parameters, params),
parameters <- Enum.filter(operation.parameters, &Map.has_key?(params, &1.name)),
{:ok, remaining} <- validate_parameter_schemas(parameters, params, schemas),
:ok <- validate_body_schema(operation.requestBody, remaining, content_type, schemas) do
:ok
end
end
def validate_required_parameters(parameter_list, params = %{}) do
required =
parameter_list
|> Stream.filter(fn parameter -> parameter.required end)
|> Enum.map(fn parameter -> parameter.name end)
missing = required -- Map.keys(params)
case missing do
[] -> :ok
_ -> {:error, "Missing required parameters: #{inspect(missing)}"}
end
end
def validate_parameter_schemas([], params, _schemas), do: {:ok, params}
def validate_parameter_schemas([p | rest], params, schemas) do
with :ok <- Schema.validate(Parameter.schema(p), params[p.name], schemas),
{:ok, remaining} <- validate_parameter_schemas(rest, params, schemas) do
{:ok, Map.delete(remaining, p.name)}
end
end
def validate_body_schema(nil, _, _, _), do: :ok
def validate_body_schema(%RequestBody{required: false}, params, _content_type, _schemas) when map_size(params) == 0 do
:ok
end
def validate_body_schema(%RequestBody{content: content}, params, content_type, schemas) do
content
|> Map.get(content_type)
|> Map.get(:schema)
|> Schema.validate(params, schemas)
end
end
\ No newline at end of file
diff --git a/lib/open_api_spex/parameter.ex b/lib/open_api_spex/parameter.ex
index 821f8ea..c9a78e8 100644
--- a/lib/open_api_spex/parameter.ex
+++ b/lib/open_api_spex/parameter.ex
@@ -1,64 +1,62 @@
defmodule OpenApiSpex.Parameter do
alias OpenApiSpex.{
Schema, Reference, Example, MediaType, Parameter
}
defstruct [
:name,
:in,
:description,
:required,
:deprecated,
:allowEmptyValue,
:style,
:explode,
:allowReserved,
:schema,
:example,
:examples,
:content,
]
+ @type location :: :query | :header | :path | :cookie
@type style :: :matrix | :label | :form | :simple | :spaceDelimited | :pipeDelimited | :deep
@type t :: %__MODULE__{
- name: String.t,
- in: :query | :header | :path | :cookie,
+ name: atom,
+ in: location,
description: String.t,
required: boolean,
deprecated: boolean,
allowEmptyValue: boolean,
style: style,
explode: boolean,
allowReserved: boolean,
- schema: Schema.t | Reference.t,
+ schema: Schema.t | Reference.t | atom,
example: any,
examples: %{String.t => Example.t | Reference.t},
content: %{String.t => MediaType.t}
}
@doc """
Sets the schema for a parameter from a simple type, reference or Schema
"""
- @spec put_schema(t, Reference.t | Schema.t | atom | String.t) :: t
+ @spec put_schema(t, Reference.t | Schema.t | atom) :: t
def put_schema(parameter = %Parameter{}, type = %Reference{}) do
%{parameter | schema: type}
end
def put_schema(parameter = %Parameter{}, type = %Schema{}) do
%{parameter | schema: type}
end
- def put_schema(parameter = %Parameter{}, type) when is_binary(type) do
- %{parameter | schema: %Schema{type: type}}
- end
def put_schema(parameter = %Parameter{}, type) when type in [:boolean, :integer, :number, :string, :array, :object] do
%{parameter | schema: %Schema{type: type}}
end
def put_schema(parameter = %Parameter{}, type) when is_atom(type) do
%{parameter | schema: type}
end
def schema(%Parameter{schema: schema = %{}}) do
schema
end
def schema(%Parameter{content: content = %{}}) do
{_type, %MediaType{schema: schema}} = Enum.at(content, 0)
schema
end
end
diff --git a/lib/open_api_spex/reference.ex b/lib/open_api_spex/reference.ex
index 9441dcb..16d712a 100644
--- a/lib/open_api_spex/reference.ex
+++ b/lib/open_api_spex/reference.ex
@@ -1,34 +1,34 @@
defmodule OpenApiSpex.Reference do
@moduledoc """
A simple object to allow referencing other components in the specification, internally and externally.
The Reference Object is defined by JSON Reference and follows the same structure, behavior and rules.
See https://swagger.io/specification/#referenceObject
## Example
ref = %OpenApiSpex.Reference{"$ref": "#/components/schemas/user"}
"""
alias OpenApiSpex.Reference
defstruct [
:"$ref"
]
- @type t :: %{
+ @type t :: %Reference{
"$ref": String.t
}
@doc """
Resolve a `Reference` to the `Schema` it refers to.
## Examples
iex> alias OpenApiSpex.{Reference, Schema}
...> schemas = %{"user" => %Schema{title: "user", type: :object}}
...> Reference.resolve_schema(%Reference{"$ref": "#/components/schemas/user"}, schemas)
%OpenApiSpex.Schema{type: :object, title: "user"}
"""
@spec resolve_schema(Reference.t, %{String.t => Schema.t}) :: Schema.t | nil
def resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
end
\ No newline at end of file
diff --git a/lib/open_api_spex/response.ex b/lib/open_api_spex/response.ex
index b9da420..24c121c 100644
--- a/lib/open_api_spex/response.ex
+++ b/lib/open_api_spex/response.ex
@@ -1,15 +1,15 @@
defmodule OpenApiSpex.Response do
alias OpenApiSpex.{Header, Reference, MediaType, Link}
defstruct [
:description,
:headers,
:content,
:links
]
@type t :: %__MODULE__{
description: String.t,
- headers: %{String.t => Header.t | Reference.t},
+ headers: %{String.t => Header.t | Reference.t} | nil,
content: %{String.t => MediaType.t},
- links: %{String.t => Link.t | Reference.t}
+ links: %{String.t => Link.t | Reference.t} | nil
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 7bccef6..dd7c171 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,372 +1,373 @@
defmodule OpenApiSpex.Schema do
@moduledoc """
The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays.
See https://swagger.io/specification/#schemaObject
## Example
alias OpenApiSpex.Schema
%Schema{
title: "User",
type: :object,
properties: %{
id: %Schema{type: :integer, minimum: 1},
name: %Schema{type: :string, pattern: "[a-zA-Z][a-zA-Z0-9_]+"},
email: %Scheam{type: :string, format: :email},
last_login: %Schema{type: :string, format: :"date-time"}
},
required: [:name, :email],
example: %{
"name" => "joe",
"email" => "joe@gmail.com"
}
}
"""
alias OpenApiSpex.{
Schema, Reference, Discriminator, Xml, ExternalDocumentation
}
defstruct [
:title,
:multipleOf,
:maximum,
:exclusiveMaximum,
:minimum,
:exclusiveMinimum,
:maxLength,
:minLength,
:pattern,
:maxItems,
:minItems,
:uniqueItems,
:maxProperties,
:minProperties,
:required,
:enum,
:type,
:allOf,
:oneOf,
:anyOf,
:not,
:items,
:properties,
{:additionalProperties, true},
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
:deprecated,
:"x-struct"
]
@type t :: %__MODULE__{
title: String.t,
- multipleOf: number,
- maximum: number,
- exclusiveMaximum: boolean,
- minimum: number,
- exclusiveMinimum: boolean,
- maxLength: integer,
- minLength: integer,
- pattern: String.t,
- maxItems: integer,
- minItems: integer,
- uniqueItems: boolean,
- maxProperties: integer,
- minProperties: integer,
- required: [String.t],
- enum: [String.t],
- type: String.t,
- allOf: [Schema.t | Reference.t],
- oneOf: [Schema.t | Reference.t],
- anyOf: [Schema.t | Reference.t],
- not: Schema.t | Reference.t,
- items: Schema.t | Reference.t,
- properties: %{String.t => Schema.t | Reference.t},
- additionalProperties: boolean | Schema.t | Reference.t,
+ multipleOf: number | nil,
+ maximum: number | nil,
+ exclusiveMaximum: boolean | nil,
+ minimum: number | nil,
+ exclusiveMinimum: boolean | nil,
+ maxLength: integer | nil,
+ minLength: integer | nil,
+ pattern: String.t | Regex.t | nil,
+ maxItems: integer | nil,
+ minItems: integer | nil,
+ uniqueItems: boolean | nil,
+ maxProperties: integer | nil,
+ minProperties: integer | nil,
+ required: [String.t] | nil,
+ enum: [String.t] | nil,
+ type: atom,
+ allOf: [Schema.t | Reference.t] | nil,
+ oneOf: [Schema.t | Reference.t] | nil,
+ anyOf: [Schema.t | Reference.t] | nil,
+ not: Schema.t | Reference.t | nil,
+ items: Schema.t | Reference.t | nil,
+ properties: %{atom => Schema.t | Reference.t} | nil,
+ additionalProperties: boolean | Schema.t | Reference.t | nil,
description: String.t,
- format: String.t,
- default: any,
- nullable: boolean,
- discriminator: Discriminator.t,
- readOnly: boolean,
- writeOnly: boolean,
- xml: Xml.t,
- externalDocs: ExternalDocumentation.t,
+ format: String.t | nil,
+ default: any | nil,
+ nullable: boolean | nil,
+ discriminator: Discriminator.t | nil,
+ readOnly: boolean | nil,
+ writeOnly: boolean | nil,
+ xml: Xml.t | nil,
+ externalDocs: ExternalDocumentation.t | nil,
example: any,
- deprecated: boolean,
- "x-struct": module
+ deprecated: boolean | nil,
+ "x-struct": module | nil
}
@doc """
Cast a simple value to the elixir type defined by a schema.
By default, object types are cast to maps, however if the "x-struct" attribute is set in the schema,
the result will be constructed as an instance of the given struct type.
## Examples
iex> OpenApiSpex.Schema.cast(%Schema{type: :integer}, "123", %{})
{:ok, 123}
iex> {:ok, dt = %DateTime{}} = OpenApiSpex.Schema.cast(%Schema{type: :string, format: :"date-time"}, "2018-04-02T13:44:55Z", %{})
...> dt |> DateTime.to_iso8601()
"2018-04-02T13:44:55Z"
"""
- @spec cast(Schema.t | Reference.t, any, %{String.t => Schema.t}) :: {:ok, any} | {:error, String.t}
+ @spec cast(Schema.t | Reference.t, any, %{String.t => Schema.t | Reference.t}) :: {:ok, any} | {:error, String.t}
def cast(schema = %Schema{"x-struct": mod}, value, schemas) when not is_nil(mod) do
with {:ok, data} <- cast(%{schema | "x-struct": nil}, value, schemas) do
{:ok, struct(mod, data)}
end
end
def cast(%Schema{type: :boolean}, value, _schemas) when is_boolean(value), do: {:ok, value}
def cast(%Schema{type: :boolean}, value, _schemas) when is_binary(value) do
case value do
"true" -> true
"false" -> false
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def cast(%Schema{type: :integer}, value, _schemas) when is_integer(value), do: {:ok, value}
def cast(%Schema{type: :integer}, value, _schemas) when is_binary(value) do
case Integer.parse(value) do
{i, ""} -> {:ok, i}
_ -> {:error, :bad_integer}
end
end
def cast(%Schema{type: :number}, value, _schemas) when is_number(value), do: {:ok, value}
def cast(%Schema{type: :number}, value, _schemas) when is_binary(value) do
case Float.parse(value) do
{x, ""} -> {:ok, x}
_ -> {:error, :bad_float}
end
end
def cast(%Schema{type: :string, format: :"date-time"}, value, _schemas) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, datetime = %DateTime{}, _offset} -> {:ok, datetime}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string, format: :date}, value, _schemas) when is_binary(value) do
case Date.from_iso8601(value) do
{:ok, date = %Date{}} -> {:ok, date}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string}, value, _schemas) when is_binary(value), do: {:ok, value}
def cast(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: {:ok, value}
def cast(%Schema{type: :array}, [], _schemas), do: {:ok, []}
def cast(schema = %Schema{type: :array, items: items_schema}, [x | rest], schemas) do
with {:ok, x_cast} <- cast(items_schema, x, schemas),
{:ok, rest_cast} <- cast(schema, rest, schemas) do
{:ok, [x_cast | rest_cast]}
end
end
def cast(schema = %Schema{type: :object}, value, schemas) when is_map(value) do
with {:ok, props} <- cast_properties(schema, Enum.to_list(value), schemas) do
{:ok, Map.new(props)}
end
end
def cast(ref = %Reference{}, val, schemas), do: cast(Reference.resolve_schema(ref, schemas), val, schemas)
def cast(additionalProperties, val, _schemas) when additionalProperties in [true, false, nil], do: val
@spec cast_properties(Schema.t, list, %{String.t => Schema.t}) :: {:ok, list} | {:error, String.t}
defp cast_properties(%Schema{}, [], _schemas), do: {:ok, []}
defp cast_properties(object_schema = %Schema{}, [{key, value} | rest], schemas) do
{name, schema} = Enum.find(
object_schema.properties,
{key, object_schema.additionalProperties},
fn {name, _schema} -> to_string(name) == to_string(key) end)
with {:ok, new_value} <- cast(schema, value, schemas),
{:ok, cast_tail} <- cast_properties(object_schema, rest, schemas) do
{:ok, [{name, new_value} | cast_tail]}
end
end
@doc """
Validate a value against a Schema.
This expects that the value has already been `cast` to the appropriate data type.
## Examples
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :integer, minimum: 5}, 3, %{})
{:error, "3 is smaller than minimum 5"}
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :string, pattern: "(.*)@(.*)"}, "joe@gmail.com", %{})
:ok
iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :string, pattern: "(.*)@(.*)"}, "joegmail.com", %{})
{:error, "Value does not match pattern: (.*)@(.*)"}
"""
- @spec validate(Schema.t | Reference.t, any, %{String.t => Schema.t}) :: :ok | {:error, String.t}
+ @spec validate(Schema.t | Reference.t, any, %{String.t => Schema.t | Reference.t}) :: :ok | {:error, String.t}
def validate(ref = %Reference{}, val, schemas), do: validate(Reference.resolve_schema(ref, schemas), val, schemas)
def validate(schema = %Schema{type: type}, value, _schemas) when type in [:integer, :number] do
with :ok <- validate_multiple(schema, value),
:ok <- validate_maximum(schema, value),
:ok <- validate_minimum(schema, value) do
:ok
end
end
def validate(schema = %Schema{type: :string}, value, _schemas) do
with :ok <- validate_max_length(schema, value),
:ok <- validate_min_length(schema, value),
:ok <- validate_pattern(schema, value) do
:ok
end
end
def validate(%Schema{type: :boolean}, value, _schemas) do
case is_boolean(value) do
true -> :ok
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def validate(schema = %Schema{type: :array}, value, schemas) do
with :ok <- validate_max_items(schema, value),
:ok <- validate_min_items(schema, value),
:ok <- validate_unique_items(schema, value),
:ok <- validate_array_items(schema, value, schemas) do
:ok
end
end
def validate(schema = %Schema{type: :object, properties: properties}, value, schemas) do
with :ok <- validate_required_properties(schema, value),
:ok <- validate_max_properties(schema, value),
:ok <- validate_min_properties(schema, value),
:ok <- validate_object_properties(properties, value, schemas) do
:ok
end
end
@spec validate_multiple(Schema.t, number) :: :ok | {:error, String.t}
defp validate_multiple(%{multipleOf: nil}, _), do: :ok
defp validate_multiple(%{multipleOf: n}, value) when (round(value / n) * n == value), do: :ok
defp validate_multiple(%{multipleOf: n}, value), do: {:error, "#{value} is not a multiple of #{n}"}
@spec validate_maximum(Schema.t, number) :: :ok | {:error, String.t}
defp validate_maximum(%{maximum: nil}, _), do: :ok
defp validate_maximum(%{maximum: n, exclusiveMaximum: true}, value) when value < n, do: :ok
defp validate_maximum(%{maximum: n}, value) when value <= n, do: :ok
defp validate_maximum(%{maximum: n}, value), do: {:error, "#{value} is larger than maximum #{n}"}
@spec validate_minimum(Schema.t, number) :: :ok | {:error, String.t}
defp validate_minimum(%{minimum: nil}, _), do: :ok
defp validate_minimum(%{minimum: n, exclusiveMinimum: true}, value) when value > n, do: :ok
defp validate_minimum(%{minimum: n}, value) when value >= n, do: :ok
defp validate_minimum(%{minimum: n}, value), do: {:error, "#{value} is smaller than minimum #{n}"}
@spec validate_max_length(Schema.t, String.t) :: :ok | {:error, String.t}
defp validate_max_length(%{maxLength: nil}, _), do: :ok
defp validate_max_length(%{maxLength: n}, value) do
case String.length(value) <= n do
true -> :ok
_ -> {:error, "String length is larger than maxLength: #{n}"}
end
end
@spec validate_min_length(Schema.t, String.t) :: :ok | {:error, String.t}
defp validate_min_length(%{minLength: nil}, _), do: :ok
defp validate_min_length(%{minLength: n}, value) do
case String.length(value) >= n do
true -> :ok
_ -> {:error, "String length is smaller than minLength: #{n}"}
end
end
- @spec validate_max_length(Schema.t, String.t) :: :ok | {:error, String.t}
+ @spec validate_pattern(Schema.t, String.t) :: :ok | {:error, String.t}
defp validate_pattern(%{pattern: nil}, _), do: :ok
defp validate_pattern(schema = %{pattern: regex}, val) when is_binary(regex) do
with {:ok, regex} <- Regex.compile(regex) do
validate_pattern(%{schema | pattern: regex}, val)
end
end
defp validate_pattern(%{pattern: regex = %Regex{}}, val) do
case Regex.match?(regex, val) do
true -> :ok
_ -> {:error, "Value does not match pattern: #{regex.source}"}
end
end
- @spec validate_max_length(Schema.t, list) :: :ok | {:error, String.t}
+ @spec validate_max_items(Schema.t, list) :: :ok | {:error, String.t}
defp validate_max_items(%Schema{maxItems: nil}, _), do: :ok
defp validate_max_items(%Schema{maxItems: n}, value) when length(value) <= n, do: :ok
defp validate_max_items(%Schema{maxItems: n}, value) do
{:error, "Array length #{length(value)} is larger than maxItems: #{n}"}
end
@spec validate_min_items(Schema.t, list) :: :ok | {:error, String.t}
defp validate_min_items(%Schema{minItems: nil}, _), do: :ok
defp validate_min_items(%Schema{minItems: n}, value) when length(value) >= n, do: :ok
defp validate_min_items(%Schema{minItems: n}, value) do
{:error, "Array length #{length(value)} is smaller than minItems: #{n}"}
end
@spec validate_unique_items(Schema.t, list) :: :ok | {:error, String.t}
defp validate_unique_items(%Schema{uniqueItems: true}, value) do
unique_size =
value
|> MapSet.new()
|> MapSet.size()
case unique_size == length(value) do
true -> :ok
_ -> {:error, "Array items must be unique"}
end
end
defp validate_unique_items(_, _), do: :ok
@spec validate_array_items(Schema.t, list, %{String.t => Schema.t}) :: :ok | {:error, String.t}
defp validate_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: :ok
defp validate_array_items(%Schema{type: :array}, [], _schemas), do: :ok
defp validate_array_items(schema = %Schema{type: :array, items: item_schema}, [x | rest], schemas) do
with :ok <- validate(item_schema, x, schemas) do
validate(schema, rest, schemas)
end
end
@spec validate_required_properties(Schema.t, %{}) :: :ok | {:error, String.t}
defp validate_required_properties(%Schema{type: :object, required: nil}, _), do: :ok
defp validate_required_properties(%Schema{type: :object, required: required}, value) do
missing = required -- Map.keys(value)
case missing do
[] -> :ok
_ -> {:error, "Missing required properties: #{inspect(missing)}"}
end
end
@spec validate_max_properties(Schema.t, %{}) :: :ok | {:error, String.t}
defp validate_max_properties(%Schema{type: :object, maxProperties: nil}, _), do: :ok
defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val) when map_size(val) <= n, do: :ok
defp validate_max_properties(%Schema{type: :object, maxProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is greater than maxProperties: #{n}"}
end
@spec validate_min_properties(Schema.t, %{}) :: :ok | {:error, String.t}
defp validate_min_properties(%Schema{type: :object, minProperties: nil}, _), do: :ok
defp validate_min_properties(%Schema{type: :object, minProperties: n}, val) when map_size(val) >= n, do: :ok
defp validate_min_properties(%Schema{type: :object, minProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is less than minProperties: #{n}"}
end
- @spec validate_min_properties(Schema.t, %{}) :: :ok | {:error, String.t}
- defp validate_object_properties(properties = %{}, value, schemas) do
+ @spec validate_object_properties(Enumerable.t, %{}, %{String.t => Schema.t | Reference.t}) :: :ok | {:error, String.t}
+ defp validate_object_properties(properties = %{}, value = %{}, schemas = %{}) do
properties
|> Enum.filter(fn {name, _schema} -> Map.has_key?(value, name) end)
|> validate_object_properties(value, schemas)
end
defp validate_object_properties([], _, _), do: :ok
- defp validate_object_properties([{name, schema} | rest], value, schemas) do
- case validate(schema, Map.fetch!(value, name), schemas) do
+ defp validate_object_properties([{name, schema} | rest], value, schemas = %{}) do
+ %{^name => property_value} = value
+ case validate(schema, property_value, schemas) do
:ok -> validate_object_properties(rest, value, schemas)
error -> error
end
end
end
\ No newline at end of file
diff --git a/lib/open_api_spex/server.ex b/lib/open_api_spex/server.ex
index c8cb5be..3a6358d 100644
--- a/lib/open_api_spex/server.ex
+++ b/lib/open_api_spex/server.ex
@@ -1,28 +1,29 @@
defmodule OpenApiSpex.Server do
alias OpenApiSpex.{Server, ServerVariable}
defstruct [
:url,
:description,
variables: %{}
]
@type t :: %Server{
url: String.t,
- description: String.t,
+ description: String.t | nil,
variables: %{String.t => ServerVariable.t}
}
@doc """
Builds a Server from a phoenix Endpoint module
"""
- @spec from_endpoint(module, keyword) :: t
- def from_endpoint(endpoint, otp_app: app) do
+ @spec from_endpoint(module, [otp_app: atom]) :: t
+ def from_endpoint(endpoint, opts) do
+ app = opts[:otp_app]
url_config = Application.get_env(app, endpoint, []) |> Keyword.get(:url, [])
scheme = Keyword.get(url_config, :scheme, "http")
host = Keyword.get(url_config, :host, "localhost")
port = Keyword.get(url_config, :port, "80")
path = Keyword.get(url_config, :path, "/")
%Server{
url: "#{scheme}://#{host}:#{port}#{path}"
}
end
end
\ No newline at end of file
diff --git a/lib/open_api_spex/servier_variable.ex b/lib/open_api_spex/servier_variable.ex
index 20426c0..4924287 100644
--- a/lib/open_api_spex/servier_variable.ex
+++ b/lib/open_api_spex/servier_variable.ex
@@ -1,12 +1,12 @@
defmodule OpenApiSpex.ServerVariable do
defstruct [
:enum,
:default,
:description
]
- @type t :: %{
+ @type t :: %__MODULE__{
enum: [String.t],
default: String.t,
description: String.t
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/tag.ex b/lib/open_api_spex/tag.ex
index 80a964c..ca75c0e 100644
--- a/lib/open_api_spex/tag.ex
+++ b/lib/open_api_spex/tag.ex
@@ -1,13 +1,13 @@
defmodule OpenApiSpex.Tag do
alias OpenApiSpex.ExternalDocumentation
defstruct [
:name,
:description,
:externalDocs
]
- @type t :: %{
+ @type t :: %__MODULE__{
name: String.t,
description: String.t,
externalDocs: ExternalDocumentation.t
}
end
\ No newline at end of file
diff --git a/lib/open_api_spex/test/assertions.ex b/lib/open_api_spex/test/assertions.ex
index eaf9fec..dfa2986 100644
--- a/lib/open_api_spex/test/assertions.ex
+++ b/lib/open_api_spex/test/assertions.ex
@@ -1,15 +1,17 @@
defmodule OpenApiSpex.Test.Assertions do
alias OpenApiSpex.OpenApi
import ExUnit.Assertions
+ @dialyzer {:no_match, assert_schema: 3}
+
def assert_schema(value = %{}, schema_title, api_spec = %OpenApi{}) do
schemas = api_spec.components.schemas
schema = schemas[schema_title]
if !schema do
flunk("Schema: #{schema_title} not found in #{inspect(Map.keys(schemas))}")
end
- assert {:ok, data} = OpenApiSpex.cast(api_spec, schema, value)
- assert :ok = OpenApiSpex.validate(api_spec, schema, data)
+ _ = assert {:ok, data} = OpenApiSpex.cast(api_spec, schema, value)
+ _ = assert :ok = OpenApiSpex.validate(api_spec, schema, data)
end
end
\ No newline at end of file
diff --git a/mix.exs b/mix.exs
index 55a5a4d..a41f0d8 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,39 +1,46 @@
defmodule OpenApiSpex.Mixfile do
use Mix.Project
@version "1.0.0"
def project do
[
app: :open_api_spex,
version: "1.0.0",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env),
start_permanent: Mix.env == :prod,
deps: deps(),
source_url: "https://github.com/mbuhot/open_api_spex",
homepage_url: "https://github.com/mbuhot/open_api_spex",
- docs: [extras: ["README.md"], main: "readme", source_ref: "v#{@version}"]
+ docs: [extras: ["README.md"], main: "readme", source_ref: "v#{@version}"],
+ dialyzer: [
+ plt_add_apps: [:mix],
+ plt_add_deps: :apps_direct,
+ flags: ["-Werror_handling", "-Wno_unused", "-Wunmatched_returns", "-Wunderspecs"],
+ remove_defaults: [:unknown]
+ ]
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:poison, ">= 0.0.0"},
{:plug, ">= 0.0.0"},
{:phoenix, "~> 1.3", only: :test},
- {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
+ {:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
+ {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}
]
end
end
diff --git a/mix.lock b/mix.lock
index 8cf5fe9..d3a003b 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,7 +1,8 @@
-%{"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"},
+%{"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"},
+ "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.16.4", "4bf6b82d4f0a643b500366ed7134896e8cccdbab4d1a7a35524951b25b1ec9f0", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"},
"plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}}
diff --git a/test/path_item_test.exs b/test/path_item_test.exs
index 16ffb62..fc221b3 100644
--- a/test/path_item_test.exs
+++ b/test/path_item_test.exs
@@ -1,21 +1,20 @@
defmodule OpenApiSpex.PathItemTest do
use ExUnit.Case
alias OpenApiSpex.PathItem
alias OpenApiSpexTest.{Router, UserController}
describe "PathItem" do
- @tag :focus
test "from_routes" do
routes =
for route <- Router.__routes__(),
route.path == "/api/users",
do: route
path_item = PathItem.from_routes(routes)
assert path_item == %PathItem{
get: UserController.index_operation(),
post: UserController.create_operation()
}
end
end
end
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 6:49 PM (21 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39052
Default Alt Text
(39 KB)

Event Timeline