defmodule OpenApiSpex.OpenApi.Decode do
@moduledoc "This module exposes functionality to convert an arbitrary map into a OpenApi struct."
alias OpenApiSpex.{
Components,
Contact,
Discriminator,
Encoding,
Example,
ExternalDocumentation,
Header,
Info,
License,
Link,
MediaType,
OAuthFlow,
OAuthFlows,
OpenApi,
Operation,
Parameter,
PathItem,
Reference,
RequestBody,
Response,
Schema,
SecurityScheme,
Server,
ServerVariable,
Tag,
Xml
}
@open_api_spex_extensions ["x-struct", "x-validate"]
def decode(%{"openapi" => _openapi, "info" => _info, "paths" => _paths} = map) do
map
|> to_struct(OpenApi)
|> prop_to_struct(:info, Info)
|> prop_to_struct(:paths, PathItems)
|> prop_to_struct(:servers, Servers)
|> prop_to_struct(:components, Components)
|> prop_to_struct(:tags, Tag)
|> prop_to_struct(:externalDocs, ExternalDocumentation)
|> add_extensions(map)
end
defp struct_from_map(struct, map) when is_atom(struct) do
struct_from_map(struct.__struct__(), map)
end
defp struct_from_map(%_{} = struct, map) do
keys = struct |> Map.from_struct() |> Map.keys()
Enum.reduce(keys, struct, fn key, struct ->
case map_get(map, key) do
{_, value} -> Map.put(struct, key, value)
_ -> struct
end
end)
end
defp map_get(map, atom_key) when is_atom(atom_key) do
case map do
%{^atom_key => value} ->
{atom_key, value}
_ ->
map_get(map, to_string(atom_key))
end
end
defp map_get(map, string_key) when is_binary(string_key) do
case map do
%{^string_key => value} -> {string_key, value}
_ -> nil
end
end
defp embedded_ref_or_struct(list, mod) when is_list(list) do
list
|> Enum.map(fn
%{"$ref" => _} = v -> struct_from_map(Reference, v)
v -> to_struct(v, mod)
end)
end
defp embedded_ref_or_struct(map, mod) when is_map(map) do
map
|> Map.new(fn
{k, %{"$ref" => _} = v} ->
{k, struct_from_map(Reference, v)}
{k, v} ->
{k, to_struct(v, mod)}
end)
end
defp update_map_if_key_present(map, key, fun) do
case map do
%{^key => value} -> %{map | key => fun.(value)}
%{} -> map
end
end
# In some cases, e.g. Schema type we must convert values that are strings to atoms,
# for example Schema.type should be one of:
#
# :string | :number | :integer | :boolean | :array | :object
#
# This function, ensures that if the map has the key — it'll convert the corresponding value
# to an atom.
defp convert_value_to_atom_if_present(map, key) do
update_fn = fn
nil -> nil
s -> String.to_atom(s)
end
update_map_if_key_present(map, key, update_fn)
end
# In some cases, e.g. Schema type we must convert values that are list of strings to a list atoms,
#
# This function, ensures that if the map has the key — it'll convert the corresponding value
# to a list of atoms.
defp convert_value_to_list_of_atoms_if_present(map, key) do
map_fn = &String.to_atom/1
update_map_if_key_present(map, key, &Enum.map(&1, map_fn))
end
# The Schema.type and Schema.required keys require some special treatment since their
# values should be converted to atoms
defp prepare_schema(map) do
map
|> convert_value_to_atom_if_present("type")
|> convert_value_to_atom_if_present("format")
|> convert_value_to_atom_if_present("x-struct")
|> convert_value_to_list_of_atoms_if_present("required")
end
defp to_struct(nil, _mod), do: nil
defp to_struct(tag, Tag) when is_binary(tag), do: tag
defp to_struct(map, Tag) when is_map(map) do
Tag
|> struct_from_map(map)
|> prop_to_struct(:externalDocs, ExternalDocumentation)
|> add_extensions(map)
end
defp to_struct(list, Tag) when is_list(list) do
list
|> Enum.map(&to_struct(&1, Tag))
end
defp to_struct(map, Components) do
Components
|> struct_from_map(map)
|> prop_to_struct(:schemas, Schemas)
|> prop_to_struct(:responses, Responses)
|> prop_to_struct(:parameters, Parameters)
|> prop_to_struct(:examples, Examples)
|> prop_to_struct(:requestBodies, RequestBodies)
|> prop_to_struct(:headers, Headers)
|> prop_to_struct(:securitySchemes, SecuritySchemes)
|> prop_to_struct(:links, Links)
|> prop_to_struct(:callbacks, Callbacks)
|> add_extensions(map)
end
defp to_struct(map, Link) do
Link
|> struct_from_map(map)
|> prop_to_struct(:server, Server)
|> prop_to_struct(:requestBody, RequestBody)
|> prop_to_struct(:parameters, Parameters)
|> add_extensions(map)
end
defp to_struct(map, Links), do: embedded_ref_or_struct(map, Link)
defp to_struct(map, SecurityScheme) do
SecurityScheme
|> struct_from_map(map)
|> prop_to_struct(:flows, OAuthFlows)
|> add_extensions(map)
end
defp to_struct(map, SecuritySchemes), do: embedded_ref_or_struct(map, SecurityScheme)
defp to_struct(map, OAuthFlow) do
OAuthFlow
|> struct_from_map(map)
|> add_extensions(map)
end
defp to_struct(map, OAuthFlows) do
OAuthFlows
|> struct_from_map(map)
|> prop_to_struct(:implicit, OAuthFlow)
|> prop_to_struct(:password, OAuthFlow)
|> prop_to_struct(:clientCredentials, OAuthFlow)
|> prop_to_struct(:authorizationCode, OAuthFlow)
|> add_extensions(map)
end
defp to_struct(%{"$ref" => _} = map, Schema), do: struct_from_map(Reference, map)
defp to_struct(%{"type" => type} = map, Schema)
when type in ~w(number integer boolean string) do
map
|> prepare_schema()
|> (&struct_from_map(Schema, &1)).()
|> prop_to_struct(:xml, Xml)
|> add_extensions(map)
end
defp to_struct(%{"type" => "array"} = map, Schema) do
map
|> prepare_schema()
|> (&struct_from_map(Schema, &1)).()
|> prop_to_struct(:items, Schema)
|> prop_to_struct(:xml, Xml)
|> add_extensions(map)
end
defp to_struct(%{"anyOf" => _valid_schemas} = map, Schema) do
Schema
|> struct_from_map(prepare_schema(map))
|> prop_to_struct(:anyOf, Schemas)
|> prop_to_struct(:discriminator, Discriminator)
|> add_extensions(map)
end
defp to_struct(%{"oneOf" => _valid_schemas} = map, Schema) do
Schema
|> struct_from_map(prepare_schema(map))
|> prop_to_struct(:oneOf, Schemas)
|> prop_to_struct(:discriminator, Discriminator)
|> add_extensions(map)
end
defp to_struct(%{"allOf" => _valid_schemas} = map, Schema) do
Schema
|> struct_from_map(prepare_schema(map))
|> prop_to_struct(:allOf, Schemas)
|> prop_to_struct(:discriminator, Discriminator)
|> add_extensions(map)
end
defp to_struct(%{"type" => "object"} = map, Schema) do
map
|> Map.update("properties", %{}, fn v ->
v
|> Map.new(fn {k, v} ->
{String.to_atom(k), v}
end)
end)
|> prepare_schema()
|> (&struct_from_map(Schema, &1)).()
|> prop_to_struct(:properties, Schemas)
|> manage_additional_properties()
|> prop_to_struct(:externalDocs, ExternalDocumentation)
|> add_extensions(map)
end
defp to_struct(%{"not" => _valid_schemas} = map, Schema) do
Schema
|> struct_from_map(map)
|> prop_to_struct(:not, Schema)
|> add_extensions(map)
end
defp to_struct(map, Schemas) when is_map(map), do: embedded_ref_or_struct(map, Schema)
defp to_struct(list, Schemas) when is_list(list), do: embedded_ref_or_struct(list, Schema)
defp to_struct(map, Callback) do
map
|> Map.new(fn {k, v} ->
{k, to_struct(v, PathItem)}
end)
end
defp to_struct(map_or_list, Callbacks), do: embedded_ref_or_struct(map_or_list, Callback)
defp to_struct(map, Operation) do
Operation
|> struct_from_map(map)
|> prop_to_struct(:tags, Tag)
|> prop_to_struct(:externalDocs, ExternalDocumentation)
|> prop_to_struct(:responses, Responses)
|> prop_to_struct(:parameters, Parameters)
|> prop_to_struct(:requestBody, RequestBody)
|> prop_to_struct(:callbacks, Callbacks)
|> prop_to_struct(:servers, Server)
|> add_extensions(map)
end
defp to_struct(%{"$ref" => _} = map, RequestBody), do: struct_from_map(Reference, map)
defp to_struct(map, RequestBody) do
RequestBody
|> struct_from_map(map)
|> prop_to_struct(:content, Content)
|> add_extensions(map)
end
defp to_struct(map, RequestBodies), do: embedded_ref_or_struct(map, RequestBody)
defp to_struct(map, Parameter) do
map
|> convert_value_to_atom_if_present("name")
|> convert_value_to_atom_if_present("in")
|> convert_value_to_atom_if_present("style")
|> (&struct_from_map(Parameter, &1)).()
|> prop_to_struct(:examples, Examples)
|> prop_to_struct(:content, Content)
|> prop_to_struct(:schema, Schema)
|> add_extensions(map)
end
defp to_struct(map_or_list, Parameters), do: embedded_ref_or_struct(map_or_list, Parameter)
defp to_struct(map, ServerVariable) do
ServerVariable
|> struct_from_map(map)
|> add_extensions(map)
end
defp to_struct(map, ServerVariables) do
map
|> Map.new(fn {k, v} ->
{k, to_struct(v, ServerVariable)}
end)
end
defp to_struct(map, Server) do
Server
|> struct_from_map(map)
|> prop_to_struct(:variables, ServerVariables)
|> add_extensions(map)
end
defp to_struct(list, Servers) when is_list(list) do
Enum.map(list, &to_struct(&1, Server))
end
defp to_struct(map, Response) do
Response
|> struct_from_map(map)
|> prop_to_struct(:headers, Headers)
|> prop_to_struct(:content, Content)
|> prop_to_struct(:links, Links)
|> add_extensions(map)
end
defp to_struct(map, Responses), do: embedded_ref_or_struct(map, Response)
defp to_struct(map, MediaType) do
MediaType
|> struct_from_map(map)
|> prop_to_struct(:examples, Examples)
|> prop_to_struct(:encoding, Encoding)
|> prop_to_struct(:schema, Schema)
|> add_extensions(map)
end
defp to_struct(map, Content) do
map
|> Map.new(fn {k, v} ->
{k, to_struct(v, MediaType)}
end)
end
defp to_struct(map, Encoding) do
map
|> Map.new(fn {k, v} ->
{k,
Encoding
|> struct_from_map(v)
|> convert_value_to_atom_if_present(:style)
|> prop_to_struct(:headers, Headers)
|> add_extensions(v)}
end)
end
defp to_struct(map, Example), do: Example |> struct_from_map(map) |> add_extensions(map)
defp to_struct(map_or_list, Examples), do: embedded_ref_or_struct(map_or_list, Example)
defp to_struct(map, Header) do
Header
|> struct_from_map(map)
|> prop_to_struct(:schema, Schema)
|> add_extensions(map)
end
defp to_struct(map, Headers), do: embedded_ref_or_struct(map, Header)
defp to_struct(map, PathItem) do
PathItem
|> struct_from_map(map)
|> prop_to_struct(:delete, Operation)
|> prop_to_struct(:get, Operation)
|> prop_to_struct(:head, Operation)
|> prop_to_struct(:options, Operation)
|> prop_to_struct(:patch, Operation)
|> prop_to_struct(:post, Operation)
|> prop_to_struct(:put, Operation)
|> prop_to_struct(:trace, Operation)
|> prop_to_struct(:parameters, Parameters)
|> prop_to_struct(:servers, Servers)
|> add_extensions(map)
end
defp to_struct(map, PathItems) do
map
|> Map.new(fn {k, v} ->
{k, to_struct(v, PathItem)}
end)
end
defp to_struct(map, Info) do
Info
|> struct_from_map(map)
|> prop_to_struct(:contact, Contact)
|> prop_to_struct(:license, License)
|> add_extensions(map)
end
defp to_struct(map, mod)
when mod in [License, Contact, ExternalDocumentation, Discriminator, Xml],
do: mod |> struct_from_map(map) |> add_extensions(map)
defp to_struct(list, mod) when is_list(list) and is_atom(mod),
do: Enum.map(list, &to_struct(&1, mod))
defp to_struct(map, module) when is_map(map) and is_atom(module),
do: struct_from_map(module, map)
defp prop_to_struct(map, key, mod) when is_map(map) and is_atom(key) and is_atom(mod) do
Map.update!(map, key, fn v ->
to_struct(v, mod)
end)
end
# additionalProperties is a reference
defp manage_additional_properties(%_{additionalProperties: %{"$ref" => reference}} = map) do
Map.put(map, :additionalProperties, %Reference{"$ref": reference})
end
# additionalProperties is a boolean without validation
defp manage_additional_properties(%_{additionalProperties: value} = map) when is_boolean(value),
do: map
# additionalProperties with custom one off validation
defp manage_additional_properties(%_{additionalProperties: %{} = props} = map) do
Map.put(map, :additionalProperties, to_struct(props, Schema))
end
defp manage_additional_properties(map), do: map
defp add_extensions(struct, map) do
extensions =
map
|> Enum.filter(fn {key, _val} ->
String.starts_with?(key, "x-") and key not in @open_api_spex_extensions
end)
|> Map.new()
Map.put(struct, :extensions, if(map_size(extensions) == 0, do: nil, else: extensions))
end
end