Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115364
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/open_api.ex b/lib/open_api_spex/open_api.ex
index ce2681f..0e076a4 100644
--- a/lib/open_api_spex/open_api.ex
+++ b/lib/open_api_spex/open_api.ex
@@ -1,101 +1,149 @@
-defmodule OpenApiSpex.OpenApi do
+defmodule OpenApiSpex.OpenApi do
@moduledoc """
Defines the `OpenApiSpex.OpenApi.t` type and the behaviour for application modules that
construct an `OpenApiSpex.OpenApi.t` at runtime.
"""
alias OpenApiSpex.{
- Extendable, Info, Server, Paths, Components,
- SecurityRequirement, Tag, ExternalDocumentation,
- OpenApi
+ Extendable,
+ Info,
+ Server,
+ Paths,
+ Components,
+ SecurityRequirement,
+ Tag,
+ ExternalDocumentation,
+ OpenApi,
+ MediaType,
+ Schema,
+ Example
}
+
@enforce_keys [:info, :paths]
- defstruct [
- openapi: "3.0.0",
- info: nil,
- servers: [],
- paths: nil,
- components: nil,
- security: [],
- tags: [],
- externalDocs: nil,
- extensions: nil
- ]
+ defstruct openapi: "3.0.0",
+ info: nil,
+ servers: [],
+ paths: nil,
+ components: nil,
+ security: [],
+ tags: [],
+ externalDocs: nil,
+ extensions: nil
@typedoc """
[OpenAPI Object](https://swagger.io/specification/#oasObject)
This is the root document object of the OpenAPI document.
"""
@type t :: %OpenApi{
- openapi: String.t,
- info: Info.t,
- servers: [Server.t] | nil,
- paths: Paths.t,
- components: Components.t | nil,
- security: [SecurityRequirement.t] | nil,
- tags: [Tag.t] | nil,
- externalDocs: ExternalDocumentation.t | nil,
- extensions: %{String.t() => any()} | nil,
- }
+ openapi: String.t(),
+ info: Info.t(),
+ servers: [Server.t()] | nil,
+ paths: Paths.t(),
+ components: Components.t() | nil,
+ security: [SecurityRequirement.t()] | nil,
+ tags: [Tag.t()] | nil,
+ externalDocs: ExternalDocumentation.t() | nil,
+ extensions: %{String.t() => any()} | nil
+ }
@doc """
A spec/0 callback function is required for use with the `OpenApiSpex.Plug.PutApiSpec` plug.
## Example
@impl OpenApiSpex.OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(MyAppWeb.Endpoint)
],
info: %Info{
title: "My App",
version: "1.0"
},
# populate the paths from a phoenix router
paths: Paths.from_router(MyAppWeb.Router)
}
|> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
end
"""
@callback spec() :: t
@json_encoder Enum.find([Jason, Poison], &Code.ensure_loaded?/1)
def json_encoder, do: @json_encoder
for encoder <- [Poison.Encoder, Jason.Encoder] do
if Code.ensure_loaded?(encoder) do
defimpl encoder do
def encode(api_spec = %OpenApi{}, options) do
api_spec
|> to_json()
|> unquote(encoder).encode(options)
end
defp to_json(%Regex{source: source}), do: source
+
+ defp to_json(%object{} = value) when object in [MediaType, Schema, Example] do
+ value
+ |> Extendable.to_map()
+ |> Stream.map(fn
+ {:value, v} when object == Example -> {"value", to_json_example(v)}
+ {:example, v} -> {"example", to_json_example(v)}
+ {k, v} -> {to_string(k), to_json(v)}
+ end)
+ |> Stream.filter(fn
+ {_, nil} -> false
+ _ -> true
+ end)
+ |> Enum.into(%{})
+ end
+
defp to_json(value = %{__struct__: _}) do
value
|> Extendable.to_map()
|> to_json()
end
+
defp to_json(value) when is_map(value) do
value
- |> Stream.map(fn {k,v} -> {to_string(k), to_json(v)} end)
- |> Stream.filter(fn {_, nil} -> false; _ -> true end)
+ |> Stream.map(fn {k, v} -> {to_string(k), to_json(v)} end)
+ |> Stream.filter(fn
+ {_, nil} -> false
+ _ -> true
+ end)
|> Enum.into(%{})
end
+
defp to_json(value) when is_list(value) do
Enum.map(value, &to_json/1)
end
+
defp to_json(nil), do: nil
defp to_json(true), do: true
defp to_json(false), do: false
defp to_json(value) when is_atom(value), do: to_string(value)
defp to_json(value), do: value
+
+ defp to_json_example(value = %{__struct__: _}) do
+ value
+ |> Extendable.to_map()
+ |> to_json_example()
+ end
+
+ defp to_json_example(value) when is_map(value) do
+ value
+ |> Stream.map(fn {k, v} -> {to_string(k), to_json_example(v)} end)
+ |> Enum.into(%{})
+ end
+
+ defp to_json_example(value) when is_list(value) do
+ Enum.map(value, &to_json_example/1)
+ end
+
+ defp to_json_example(value), do: to_json(value)
end
end
end
end
diff --git a/test/encode_test.exs b/test/encode_test.exs
index 8bb72f6..2a68305 100644
--- a/test/encode_test.exs
+++ b/test/encode_test.exs
@@ -1,68 +1,203 @@
defmodule OpenApiSpex.EncodeTest do
use ExUnit.Case
alias OpenApiSpex.{
Info,
- OpenApi
+ OpenApi,
+ Schema
}
test "Vendor extensions x-logo properly encoded" do
spec = %OpenApi{
info: %Info{
title: "Test",
version: "1.0.0",
extensions: %{
"x-logo" => %{
"url" => "https://example.com/logo.png",
"backgroundColor" => "#FFFFFF",
"altText" => "Example logo"
}
}
},
paths: %{}
}
decoded =
OpenApiSpex.resolve_schema_modules(spec)
|> Jason.encode!()
|> Jason.decode!()
assert decoded["info"]["x-logo"]["url"] == "https://example.com/logo.png"
assert decoded["info"]["x-logo"]["backgroundColor"] == "#FFFFFF"
assert decoded["info"]["x-logo"]["altText"] == "Example logo"
assert is_nil(decoded["info"]["extensions"])
end
test "Vendor extensions x-tagGroups properly encoded" do
spec = %OpenApi{
info: %Info{
title: "Test",
version: "1.0.0"
},
extensions: %{
"x-tagGroups" => [
%{
"name" => "Methods",
"tags" => [
"Search",
"Fetch",
"Delete"
]
}
]
},
paths: %{}
}
decoded =
OpenApiSpex.resolve_schema_modules(spec)
|> Jason.encode!()
|> Jason.decode!()
assert hd(decoded["x-tagGroups"])["name"] == "Methods"
assert hd(decoded["x-tagGroups"])["tags"] == ["Search", "Fetch", "Delete"]
assert is_nil(decoded["extensions"])
end
+
+ test "Example field properly encoded (MediaType, Schema)" do
+ spec = %OpenApi{
+ info: %Info{
+ title: "Test",
+ version: "1.0.0"
+ },
+ paths: %{
+ "/example" => %OpenApiSpex.PathItem{
+ get: %OpenApiSpex.Operation{
+ responses: %{
+ 200 => %OpenApiSpex.Response{
+ description: "An example",
+ content: %{
+ "application/json" => %OpenApiSpex.MediaType{
+ example: %{
+ "id" => 678,
+ "first_name" => "John",
+ "last_name" => "Doe",
+ "phone_number" => nil
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ components: %OpenApiSpex.Components{
+ schemas: %{
+ "User" => %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer},
+ first_name: %Schema{type: :string},
+ last_name: %Schema{type: :string},
+ phone_number: %Schema{type: :string, nullable: true}
+ },
+ example: %{
+ "id" => 42,
+ "first_name" => "Jane",
+ "last_name" => "Doe",
+ "phone_number" => nil
+ }
+ }
+ }
+ }
+ }
+
+ decoded =
+ OpenApiSpex.resolve_schema_modules(spec)
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert Map.has_key?(
+ get_in(decoded, [
+ "paths",
+ "/example",
+ "get",
+ "responses",
+ "200",
+ "content",
+ "application/json",
+ "example"
+ ]),
+ "phone_number"
+ )
+
+ assert Map.has_key?(
+ get_in(decoded, [
+ "components",
+ "schemas",
+ "User",
+ "example"
+ ]),
+ "phone_number"
+ )
+ end
+
+ test "Value field from Example object properly encoded" do
+ spec = %OpenApi{
+ info: %Info{
+ title: "Test",
+ version: "1.0.0"
+ },
+ paths: %{
+ "/example" => %OpenApiSpex.PathItem{
+ get: %OpenApiSpex.Operation{
+ responses: %{
+ 200 => %OpenApiSpex.Response{
+ description: "An example",
+ content: %{
+ "application/json" => %OpenApiSpex.MediaType{
+ examples: %{
+ "John" => %OpenApiSpex.Example{
+ summary: "Its John",
+ value: %{
+ "id" => 678,
+ "first_name" => "John",
+ "last_name" => "Doe",
+ "phone_number" => nil
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ decoded =
+ OpenApiSpex.resolve_schema_modules(spec)
+ |> Jason.encode!()
+ |> Jason.decode!()
+
+ assert Map.has_key?(
+ get_in(decoded, [
+ "paths",
+ "/example",
+ "get",
+ "responses",
+ "200",
+ "content",
+ "application/json",
+ "examples",
+ "John",
+ "value"
+ ]),
+ "phone_number"
+ )
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Nov 28, 12:31 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40777
Default Alt Text
(10 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment