Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115147
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index b1ca5a1..bcd1142 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,274 +1,274 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
alias OpenApiSpex.{
Components,
OpenApi,
Operation,
Operation2,
Reference,
Schema,
SchemaException,
SchemaResolver,
SchemaConsistency
}
alias OpenApiSpex.Cast.Error
@doc """
Adds schemas to the api spec from the modules specified in the Operations.
Eg, if the response schema for an operation is defined with:
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
Then the `UserResponse.schema()` function will be called to load the schema, and
a `Reference` to the loaded schema will be used in the operation response.
See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules.
"""
@spec resolve_schema_modules(OpenApi.t()) :: OpenApi.t()
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
@doc """
Cast and validate a value against a given Schema.
"""
def cast_value(value, schema = %schema_mod{}) when schema_mod in [Schema, Reference] do
OpenApiSpex.Cast.cast(schema, value)
end
@doc """
Cast and validate a value against a given Schema belonging to a given OpenApi spec.
"""
def cast_value(value, schema = %schema_mod{}, spec = %OpenApi{})
when schema_mod in [Schema, Reference] do
OpenApiSpex.Cast.cast(schema, value, spec.components.schemas)
end
def cast_and_validate(
spec = %OpenApi{},
operation = %Operation{},
conn = %Plug.Conn{},
content_type \\ nil
) do
Operation2.cast(operation, conn, content_type, spec.components)
end
@doc """
Cast params to conform to a `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.cast/3` for additional examples and details.
"""
@spec cast(OpenApi.t(), Schema.t() | Reference.t(), any) :: {:ok, any} | {:error, String.t()}
@deprecated "Use OpenApiSpex.cast_value/3 or cast_value/2 instead"
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
@doc """
Cast all params in `Plug.Conn` to conform to the schemas for `OpenApiSpex.Operation`.
Returns `{:ok, Plug.Conn.t}` with `params` and `body_params` fields updated if successful,
or `{:error, reason}` if casting fails.
`content_type` may optionally be supplied to select the `requestBody` schema.
"""
@spec cast(OpenApi.t(), Operation.t(), Plug.Conn.t(), content_type | nil) ::
{:ok, Plug.Conn.t()} | {:error, String.t()}
when content_type: String.t()
@deprecated "Use OpenApiSpex.cast_and_validate/3 instead"
def cast(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.cast(operation, conn, content_type, spec.components.schemas)
end
@doc """
Validate params against `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.validate/3` for examples of error messages.
"""
@spec validate(OpenApi.t(), Schema.t() | Reference.t(), any) :: :ok | {:error, String.t()}
@deprecated "Use OpenApiSpex.cast_value/3 or cast_value/2 instead"
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
@doc """
Validate all params in `Plug.Conn` against `OpenApiSpex.Operation` `parameter` and `requestBody` schemas.
`content_type` may be optionally supplied to select the `requestBody` schema.
"""
@spec validate(OpenApi.t(), Operation.t(), Plug.Conn.t(), content_type | nil) ::
:ok | {:error, String.t()}
when content_type: String.t()
@deprecated "Use OpenApiSpex.cast_and_validate/4 instead"
def validate(
spec = %OpenApi{},
operation = %Operation{},
conn = %Plug.Conn{},
content_type \\ nil
) do
Operation.validate(operation, conn, content_type, spec.components.schemas)
end
def path_to_string(%Error{} = error) do
Error.path_to_string(error)
end
def error_message(%Error{} = error) do
Error.message(error)
end
@doc """
Declares a struct based `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 `Jason.Encoder` and/or `Poison.Encoder` for the struct
See `OpenApiSpex.Schema` for additional examples and details.
## 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
@compile {:report_warnings, false}
@behaviour OpenApiSpex.Schema
@schema struct(
OpenApiSpex.Schema,
Map.put(Map.delete(unquote(body), :__struct__), :"x-struct", __MODULE__)
)
def schema, do: @schema
@derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1)
defstruct Schema.properties(@schema)
@type t :: %__MODULE__{}
Map.from_struct(@schema) |> OpenApiSpex.validate_compiled_schema()
# Throwing warnings to prevent runtime bugs (like in issue #144)
@schema
|> SchemaConsistency.warnings()
|> Enum.each(&IO.warn("Inconsistent schema: #{&1}", Macro.Env.stacktrace(__ENV__)))
end
end
@doc """
Creates an `%OpenApi{}` struct from a map.
This is useful when importing existing JSON or YAML encoded schemas.
## Example
# Importing an existing JSON encoded schema
open_api_spec_from_json = "encoded_schema.json"
|> File.read!()
|> Jason.decode!()
|> OpenApiSpex.OpenApi.Decode.decode()
# Importing an existing YAML encoded schema
open_api_spec_from_yaml = "encoded_schema.yaml"
|> YamlElixir.read_all_from_file!()
|> OpenApiSpex.OpenApi.Decode.decode()
"""
def schema_from_map(map), do: OpenApiSpex.OpenApi.from_map(map)
@doc """
Validate the compiled schema's properties to ensure the schema is not improperly
defined. Only errors which would cause a given schema to _always_ fail should be
raised here.
"""
def validate_compiled_schema(schema) do
Enum.each(schema, fn prop_and_val ->
:ok = validate_compiled_schema(prop_and_val, schema)
end)
end
def validate_compiled_schema({_, %Schema{} = schema}, _parent) do
- validate_compiled_schema(schema)
+ validate_compiled_schema(Map.from_struct(schema))
end
@doc """
Used for validating the schema at compile time, otherwise we're forced
to raise errors for improperly defined schemas at runtime.
"""
def validate_compiled_schema({:discriminator, %{propertyName: property, mapping: _}}, %{
anyOf: schemas
})
when is_list(schemas) do
Enum.each(schemas, fn schema ->
case schema do
%Schema{title: title} when is_binary(title) -> :ok
_ -> error!(:discriminator_schema_missing_title, schema, property_name: property)
end
end)
end
def validate_compiled_schema({:discriminator, %{propertyName: _, mapping: _}}, schema) do
case {schema.anyOf, schema.allOf, schema.oneOf} do
{nil, nil, nil} ->
error!(:discriminator_missing_composite_key, schema)
_ ->
:ok
end
end
def validate_compiled_schema({_property, _value}, _schema), do: :ok
@doc """
Raises compile time errors for improperly defined schemas.
"""
@spec error!(atom(), Schema.t(), keyword()) :: no_return()
@spec error!(atom(), Schema.t()) :: no_return()
def error!(error, schema, details \\ []) do
raise SchemaException, %{error: error, schema: schema, details: details}
end
@doc """
Resolve a schema or reference to a schema.
"""
@spec resolve_schema(Schema.t() | Reference.t(), Components.schemas_map()) :: Schema.t()
def resolve_schema(%Schema{} = schema, _), do: schema
def resolve_schema(%Reference{} = ref, schemas), do: Reference.resolve_schema(ref, schemas)
end
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 0a585cf..5ecfd7e 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,308 +1,319 @@
defmodule OpenApiSpexTest.Schemas do
require OpenApiSpex
alias OpenApiSpex.Schema
defmodule Size do
OpenApiSpex.schema(%{
title: "Size",
description: "A size of a pet",
type: :object,
properties: %{
unit: %Schema{type: :string, description: "SI unit name", default: "cm"},
value: %Schema{type: :integer, description: "Size in given unit", default: 100}
},
required: [:unit, :value]
})
end
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],
additionalProperties: false,
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
defmodule ContactInfo do
OpenApiSpex.schema(%{
title: "ContactInfo",
description: "A users contact information",
type: :object,
properties: %{
phone_number: %Schema{type: :string, description: "Phone number"},
postal_address: %Schema{type: :string, description: "Postal address"}
},
required: [:phone_number],
additionalProperties: false,
example: %{
"phone_number" => "555-123-456",
"postal_address" => "123 Evergreen Tce"
}
})
end
defmodule CreditCardPaymentDetails do
OpenApiSpex.schema(%{
title: "CreditCardPaymentDetails",
description: "Payment details when using credit-card method",
type: :object,
properties: %{
credit_card_number: %Schema{type: :string, description: "Credit card number"},
name_on_card: %Schema{type: :string, description: "Name as appears on card"},
expiry: %Schema{type: :string, description: "4 digit expiry MMYY"}
},
required: [:credit_card_number, :name_on_card, :expiry],
example: %{
"credit_card_number" => "1234-5678-1234-6789",
"name_on_card" => "Joe User",
"expiry" => "1234"
}
})
end
defmodule DirectDebitPaymentDetails do
OpenApiSpex.schema(%{
title: "DirectDebitPaymentDetails",
description: "Payment details when using direct-debit method",
type: :object,
properties: %{
account_number: %Schema{type: :string, description: "Bank account number"},
account_name: %Schema{type: :string, description: "Name of account"},
bsb: %Schema{type: :string, description: "Branch identifier"}
},
required: [:account_number, :account_name, :bsb],
example: %{
"account_number" => "12349876",
"account_name" => "Joes Savings Account",
"bsb" => "123-4567"
}
})
end
defmodule PaymentDetails do
OpenApiSpex.schema(%{
title: "PaymentDetails",
description: "Abstract Payment details type",
type: :object,
oneOf: [
CreditCardPaymentDetails,
DirectDebitPaymentDetails
]
})
end
defmodule UserRequest do
OpenApiSpex.schema(%{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
},
example: %{
"user" => %{
"name" => "Joe User",
"email" => "joe@gmail.com"
}
}
})
end
defmodule UserResponse do
OpenApiSpex.schema(%{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
},
example: %{
"data" => %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
})
end
defmodule UsersResponse do
OpenApiSpex.schema(%{
title: "UsersResponse",
description: "Response schema for multiple users",
type: :object,
properties: %{
data: %Schema{description: "The users details", type: :array, items: User}
},
example: %{
"data" => [
%{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com"
},
%{
"id" => 456,
"name" => "Jay Consumer",
"email" => "jay@yahoo.com"
}
]
}
})
end
defmodule EntityWithDict do
OpenApiSpex.schema(%{
title: "EntityWithDict",
description: "Entity with a dictionary defined via additionalProperties",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "Entity ID"},
stringDict: %Schema{
type: :object,
description: "String valued dict",
additionalProperties: %Schema{type: :string}
},
anyTypeDict: %Schema{
type: :object,
description: "Untyped valued dict",
additionalProperties: true
}
},
example: %{
"id" => 123,
"stringDict" => %{"key1" => "value1", "key2" => "value2"},
"anyTypeDict" => %{"key1" => 42, "key2" => %{"foo" => "bar"}}
}
})
end
defmodule Pet do
require OpenApiSpex
alias OpenApiSpex.{Schema, Discriminator}
pet_schemas = [
%Schema{
title: "Dog",
type: :object,
properties: %{
bark: %Schema{type: :string}
}
},
%Schema{
title: "Cat",
properties: %{
meow: %Schema{type: :string}
}
}
]
OpenApiSpex.schema(%{
title: "Pet",
type: :object,
properties: %{
pet_type: %Schema{type: :string}
},
required: [:pet_type],
anyOf: pet_schemas,
discriminator: %Discriminator{
propertyName: "pet_type"
}
})
end
defmodule Cat do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Cat",
type: :object,
allOf: [
Pet,
%Schema{
type: :object,
properties: %{
meow: %Schema{type: :string}
},
required: [:meow]
}
]
})
end
defmodule Dog do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "Dog",
type: :object,
allOf: [
Pet,
%Schema{
type: :object,
properties: %{
bark: %Schema{type: :string}
},
required: [:bark]
}
]
})
end
defmodule CatOrDog do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CatOrDog",
oneOf: [Cat, Dog]
})
end
defmodule Array do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Array",
type: :array,
items: Dog,
example: [%{pet_type: "Dog", bark: "A lot"}]
})
end
defmodule Primitive do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Primitive",
type: :integer,
example: 1
})
end
+
+ defmodule PrimitiveArray do
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "PrimitiveArray",
+ type: :array,
+ items: %Schema{type: "string"},
+ example: ["Foo"]
+ })
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Nov 27, 10:27 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40609
Default Alt Text
(17 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment