Page MenuHomePhorge

No OneTemporary

Size
10 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index f57c3dc..1a42fef 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,225 +1,232 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
alias OpenApiSpex.{
Components,
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
@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/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

Mime Type
text/x-diff
Expires
Wed, Nov 27, 8:34 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40743
Default Alt Text
(10 KB)

Event Timeline