Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115351
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
10 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index bbae919..fef20db 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,217 +1,224 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
alias OpenApiSpex.{
OpenApi,
Operation,
Operation2,
Reference,
Schema,
SchemaException,
- SchemaResolver
+ 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
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()}
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()
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()}
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()
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
@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 """
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)
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
end
diff --git a/lib/open_api_spex/schema_consistency.ex b/lib/open_api_spex/schema_consistency.ex
new file mode 100644
index 0000000..a4ebc9e
--- /dev/null
+++ b/lib/open_api_spex/schema_consistency.ex
@@ -0,0 +1,37 @@
+defmodule OpenApiSpex.SchemaConsistency do
+ @moduledoc """
+ Rules of API Schema consistency.
+ If these rules are not followed, tricky run-time bugs may appear.
+ Related issues: #144
+ """
+
+ alias OpenApiSpex.Schema
+
+ @doc "Returns a list of compile-time warnings"
+ @spec warnings(Schema.t()) :: list(binary())
+ def warnings(schema) do
+ [
+ &validate_type_key/1
+ ]
+ |> Enum.reduce([], fn validator, acc ->
+ case apply(validator, [schema]) do
+ :ok -> acc
+ {:error, message} -> [message | acc]
+ end
+ end)
+ end
+
+ ## Specific validations
+
+ # if :type is missing or nil, :properties are ignored and the nested schemas are not validated
+
+ defp validate_type_key(%Schema{properties: %{}, type: type}) do
+ case type do
+ :object -> :ok
+ nil -> {:error, ":properties provided, but :type is missing or nil (expected :object)"}
+ type -> {:error, ":properties provided, but :type is #{inspect(type)} (expected :object)"}
+ end
+ end
+
+ defp validate_type_key(_), do: :ok
+end
diff --git a/test/schema_consistency_test.exs b/test/schema_consistency_test.exs
new file mode 100644
index 0000000..16a7edf
--- /dev/null
+++ b/test/schema_consistency_test.exs
@@ -0,0 +1,35 @@
+defmodule OpenApiSpex.SchemaConsistencyTest do
+ use ExUnit.Case
+ alias OpenApiSpex.{Schema, SchemaConsistency}
+
+ @valid_schema %Schema{
+ title: "Size",
+ description: "A size of a pet",
+ type: :object,
+ properties: %{
+ unit: %Schema{type: :string, description: "SI unit name", default: "cm"}
+ },
+ required: [:unit]
+ }
+
+ describe "warnings/1" do
+ test "valid schema -> []" do
+ assert SchemaConsistency.warnings(@valid_schema) == []
+ end
+
+ test "missing :type -> [error]" do
+ %{@valid_schema | type: nil}
+ |> assert_validation_error(~r/:type is missing or nil/)
+ end
+
+ test "invalid :type -> [error]" do
+ %{@valid_schema | type: :integer}
+ |> assert_validation_error(~r/:type is :integer/)
+ end
+ end
+
+ defp assert_validation_error(body, pattern) do
+ assert [reason] = SchemaConsistency.warnings(body)
+ assert reason =~ pattern
+ end
+end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Nov 27, 10:56 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40767
Default Alt Text
(10 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment