Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113063
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment