Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index 428ae2d..f4cc5a5 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,11 +1,40 @@
defmodule OpenApiSpex do
- alias OpenApiSpex.{OpenApi, SchemaResolver}
+ alias OpenApiSpex.{OpenApi, RequestBody, Schema, SchemaResolver}
+ alias Plug.Conn
@moduledoc """
"""
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
+ def cast_parameters(conn = %Conn{}, operation_id) do
+ operation = conn.private.open_api_spex.operation_lookup[operation_id]
+ spec = conn.private.open_api_spex.spec
+ schemas = spec.components.schemas
+ params =
+ operation.parameters
+ |> Enum.filter(fn parameter -> Map.has_key?(conn.params, Atom.to_string(parameter.name)) end)
+ |> Enum.map(fn %{schema: schema, name: name} -> {name, Schema.cast(schema, conn.params[name], schemas)} end)
+ |> Enum.reduce({:ok, %{}}, fn
+ {name, {:ok, val}}, {:ok, acc} -> {:ok, Map.put(acc, name, val)}
+ _, {:error, reason} -> {:error, reason}
+ {_name, {:error, reason}}, _ -> {:error, reason}
+ end)
+
+ body = case operation.requestBody do
+ nil -> {:ok, %{}}
+ %RequestBody{content: content} ->
+ [content_type] = Conn.get_req_header(conn, "content-type")
+ schema = content[content_type].schema
+ Schema.cast(schema, conn.params, spec.components.schemas)
+ end
+
+ with {:ok, cast_params} <- params,
+ {:ok, cast_body} <- body do
+ params = Map.merge(cast_params, cast_body)
+ {:ok, %{conn | params: params}}
+ end
+ end
end
diff --git a/lib/open_api_spex/plug/cast.ex b/lib/open_api_spex/plug/cast.ex
new file mode 100644
index 0000000..7daab11
--- /dev/null
+++ b/lib/open_api_spex/plug/cast.ex
@@ -0,0 +1,15 @@
+defmodule OpenApiSpex.Plug.Cast do
+ def init(opts), do: opts
+ def call(conn, operation_id: operation_id) do
+ case OpenApiSpex.cast_parameters(conn, operation_id) do
+ {:ok, conn} -> conn
+ {:error, reason} ->
+ conn
+ |> Plug.Conn.send_resp(422, "#{reason}")
+ |> Plug.Conn.halt()
+ end
+ end
+ def call(conn = %{private: %{phoenix_controller: controller, phoenix_action: action}}, _opts) do
+ call(conn, operation_id: controller.open_api_operation(action).operationId)
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/plug/put_api_spec.ex b/lib/open_api_spex/plug/put_api_spec.ex
new file mode 100644
index 0000000..371c1ea
--- /dev/null
+++ b/lib/open_api_spex/plug/put_api_spec.ex
@@ -0,0 +1,23 @@
+defmodule OpenApiSpex.Plug.PutApiSpec do
+ def init(opts = [module: _mod]), do: opts
+ def call(conn, module: mod) do
+ spec = %OpenApiSpex.OpenApi{} = mod.spec()
+ private_data =
+ conn
+ |> Map.get(:private)
+ |> Map.get(:open_api_spex, %{})
+ |> Map.put(:spec, spec)
+ |> Map.put(:operation_lookup, build_operation_lookup(spec))
+
+ Plug.Conn.put_private(conn, :open_api_spex, private_data)
+ end
+
+ defp build_operation_lookup(spec = %OpenApiSpex.OpenApi{}) do
+ spec
+ |> Map.get(:paths)
+ |> Stream.flat_map(fn {_name, item} -> Map.values(item) end)
+ |> Stream.filter(fn x -> match?(%OpenApiSpex.Operation{}, x) end)
+ |> Stream.map(fn operation -> {operation.operationId, operation} end)
+ |> Enum.into(%{})
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 78ea548..1617be3 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,79 +1,131 @@
defmodule OpenApiSpex.Schema do
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,
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
:deprecated
]
@type t :: %__MODULE__{
title: String.t,
multipleOf: number,
maximum: number,
exclusiveMaximum: number,
minimum: number,
exclusiveMinimum: number,
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,
description: String.t,
format: String.t,
default: any,
nullable: boolean,
discriminator: Discriminator.t,
readOnly: boolean,
writeOnly: boolean,
xml: Xml.t,
externalDocs: ExternalDocumentation.t,
example: any,
deprecated: boolean
}
+
+ 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: :object, properties: properties}, value, schemas) when is_map(value) do
+ properties
+ |> Stream.filter(fn {name, _} -> Map.has_key?(value, name) || Map.has_key?(value, Atom.to_string(name)) end)
+ |> Stream.map(fn {name, schema} -> {name, resolve_schema(schema, schemas)} end)
+ |> Stream.map(fn {name, schema} -> {name, schema, Map.get(value, name, value[Atom.to_string(name)])} end)
+ |> Stream.map(fn {name, schema, property_val} -> cast_property(name, schema, property_val, schemas) end)
+ |> Enum.reduce({:ok, %{}}, fn
+ _, {:error, reason} -> {:error, reason}
+ {:error, reason}, _ -> {:error, reason}
+ {:ok, {name, property_val}}, {:ok, acc} -> {:ok, Map.put(acc, name, property_val)}
+ end)
+ end
+ def cast(ref = %Reference{}, val, schemas), do: cast(resolve_schema(ref, schemas), val, schemas)
+
+ defp resolve_schema(schema = %Schema{}, _), do: schema
+ defp resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
+
+ defp cast_property(name, schema, value, schemas) do
+ casted = cast(schema, value, schemas)
+ case casted do
+ {:ok, new_value} -> {:ok, {name, new_value}}
+ {:error, reason} -> {:error, reason}
+ end
+ end
end
\ No newline at end of file
diff --git a/mix.exs b/mix.exs
index 8e11943..7ccb5b5 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,32 +1,32 @@
defmodule OpenApiSpex.Mixfile do
use Mix.Project
def project do
[
app: :open_api_spex,
version: "0.1.0",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env),
start_permanent: Mix.env == :prod,
deps: deps()
]
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"},
- {:phoenix, "~> 1.3", only: :test, runtime: false}
+ {:phoenix, "~> 1.3", only: :test}
]
end
end
diff --git a/test/open_api_spex_test.exs b/test/open_api_spex_test.exs
index 1f49b47..387ed3c 100644
--- a/test/open_api_spex_test.exs
+++ b/test/open_api_spex_test.exs
@@ -1,11 +1,37 @@
defmodule OpenApiSpexTest do
use ExUnit.Case
alias OpenApiSpexTest.ApiSpec
describe "OpenApi" do
test "compete" do
spec = ApiSpec.spec()
assert spec
end
+
+ test "asdfafd" do
+ request_body = %{
+ "user" => %{
+ "id" => 123,
+ "name" => "asdf",
+ "email" => "foo@bar.com",
+ "updated_at" => "2017-09-12T14:44:55Z"
+ }
+ }
+
+ conn =
+ :post
+ |> Plug.Test.conn("/api/users", Poison.encode!(request_body))
+ |> Plug.Conn.put_req_header("content-type", "application/json")
+
+ conn = OpenApiSpexTest.Router.call(conn, [])
+ assert conn.params == %{
+ user: %{
+ id: 123,
+ name: "asdf",
+ email: "foo@bar.com",
+ updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
+ }
+ }
+ end
end
end
\ No newline at end of file
diff --git a/test/schema_test.exs b/test/schema_test.exs
new file mode 100644
index 0000000..abf688e
--- /dev/null
+++ b/test/schema_test.exs
@@ -0,0 +1,31 @@
+defmodule OpenApiSpex.SchemaTest do
+ use ExUnit.Case
+ alias OpenApiSpex.Schema
+ alias OpenApiSpexTest.ApiSpec
+
+ test "cast request schema" do
+ api_spec = ApiSpec.spec()
+ schemas = api_spec.components.schemas
+ user_request_schema = schemas["UserRequest"]
+
+ input = %{
+ "user" => %{
+ "id" => 123,
+ "name" => "asdf",
+ "email" => "foo@bar.com",
+ "updated_at" => "2017-09-12T14:44:55Z"
+ }
+ }
+
+ {:ok, output} = Schema.cast(user_request_schema, input, schemas)
+
+ assert output == %{
+ user: %{
+ id: 123,
+ name: "asdf",
+ email: "foo@bar.com",
+ updated_at: DateTime.from_naive!(~N[2017-09-12T14:44:55], "Etc/UTC")
+ }
+ }
+ end
+end
\ No newline at end of file
diff --git a/test/support/router.ex b/test/support/router.ex
index 666dfe9..7ffe161 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,8 +1,14 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
+ pipeline :api do
+ plug Plug.Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Poison
+ plug OpenApiSpex.Plug.PutApiSpec, module: OpenApiSpexTest.ApiSpec
+ end
+
scope "/api", OpenApiSpexTest do
+ pipe_through :api
resources "/users", UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpecController, :show
end
end
\ No newline at end of file
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 88eb6fe..19e542f 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,59 +1,59 @@
defmodule OpenApiSpexTest.Schemas do
alias OpenApiSpex.Schema
defmodule User do
def schema do
%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"},
email: %Schema{type: :string, description: "Email address", format: :email},
- inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
- updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
+ inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :'date-time'},
+ updated_at: %Schema{type: :string, description: "Update timestamp", format: :'date-time'}
}
}
end
end
defmodule UserRequest do
def schema do
%Schema{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
}
}
end
end
defmodule UserResponse do
def schema do
%Schema{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
end
end
defmodule UsersResponse do
def schema do
%Schema{
title: "UsersReponse",
description: "Response schema for multiple users",
type: :object,
properties: %{
data: %Schema{description: "The users details", type: :array, items: User}
}
}
end
end
end
\ No newline at end of file
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
index 1b66f44..69c5ee9 100644
--- a/test/support/user_controller.ex
+++ b/test/support/user_controller.ex
@@ -1,66 +1,69 @@
defmodule OpenApiSpexTest.UserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
+ alias Plug.Conn
+
+ plug OpenApiSpex.Plug.Cast
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def show_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def show(conn, _params) do
conn
- |> Plug.Conn.send_resp(200, "HELLO")
+ |> Conn.send_resp(200, "HELLO")
end
def index_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
parameters: [],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
conn
- |> Plug.Conn.send_resp(200, "HELLO")
+ |> Conn.send_resp(200, "HELLO")
end
def create_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [],
requestBody: request_body("The user attributes", "application/json", Schemas.UserRequest),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def create(conn, _params) do
conn
- |> Plug.Conn.send_resp(201, "DONE")
+ |> Conn.send_resp(201, "DONE")
end
end
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 24, 7:55 AM (1 d, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39374
Default Alt Text
(15 KB)

Event Timeline