Current section

Files

Jump to
stripity_stripe lib openapi phases compile.ex
Raw

lib/openapi/phases/compile.ex

defmodule Stripe.OpenApi.Phases.Compile do
@moduledoc false
def run(blueprint, _options) do
modules = Enum.map(blueprint.components, fn {_k, component} -> component.module end)
for {_name, component} <- blueprint.components do
funcs_types =
for operation <- component.operations,
operation_definition =
lookup_operation(
{operation["path"], String.to_atom(operation["operation"])},
blueprint.operations
),
operation_definition != nil do
arguments =
operation_definition.path_parameters
|> Enum.map(&String.to_atom(&1.name))
params? =
match?({:object, _, [_ | _]}, operation_definition.query_parameters) ||
match?({:object, _, [_ | _]}, operation_definition.body_parameters)
argument_names =
arguments
|> Enum.map(fn
name ->
Macro.var(name, __MODULE__)
end)
argument_values =
arguments
|> Enum.reject(&(&1 == :params))
|> Enum.map(fn name ->
Macro.var(name, __MODULE__)
end)
argument_specs =
arguments
|> Enum.map(fn
:params ->
quote do
params :: map()
end
name ->
quote do
unquote(Macro.var(name, __MODULE__)) :: binary()
end
end)
function_name = String.to_atom(operation["method_name"])
success_response_spec = return_spec(operation_definition.success_response)
params =
cond do
operation_definition.query_parameters != {:object, [], []} ->
operation_definition.query_parameters
operation_definition.body_parameters != {:object, [], []} ->
operation_definition.body_parameters
true ->
[]
end
{param_specs, object_types} = unnest_object_types(params)
object_types = MapSet.to_list(object_types)
function_code =
if params? do
quote do
@spec unquote(function_name)(
unquote_splicing(argument_specs),
params :: unquote(to_inline_spec(param_specs)),
opts :: Keyword.t()
) ::
{:ok, unquote(success_response_spec)}
| {:error, Stripe.ApiErrors.t()}
| {:error, term()}
def unquote(function_name)(
unquote_splicing(argument_names),
params \\ %{},
opts \\ []
) do
path =
Stripe.OpenApi.Path.replace_path_params(
unquote(operation_definition.path),
unquote(operation_definition.path_parameters),
unquote(argument_names)
)
Stripe.Request.new_request(opts)
|> Stripe.Request.put_endpoint(path)
|> Stripe.Request.put_params(params)
|> Stripe.Request.put_method(unquote(operation_definition.method))
|> Stripe.Request.make_request()
end
end
else
if operation_definition.path == "/v1/files" and operation_definition.method == :post do
quote do
@spec unquote(function_name)(
unquote_splicing(argument_specs),
opts :: Keyword.t()
) ::
{:ok, unquote(success_response_spec)}
| {:error, Stripe.ApiErrors.t()}
| {:error, term()}
def unquote(function_name)(
params \\ %{},
opts \\ []
) do
Stripe.Request.new_request(opts)
|> Stripe.Request.put_endpoint(unquote(operation_definition.path))
|> Stripe.Request.put_params(params)
|> Stripe.Request.put_method(unquote(operation_definition.method))
|> Stripe.Request.make_file_upload_request()
end
end
else
quote do
@spec unquote(function_name)(
unquote_splicing(argument_specs),
opts :: Keyword.t()
) ::
{:ok, unquote(success_response_spec)}
| {:error, Stripe.ApiErrors.t()}
| {:error, term()}
def unquote(function_name)(
unquote_splicing(argument_names),
opts \\ []
) do
path =
Stripe.OpenApi.Path.replace_path_params(
unquote(operation_definition.path),
unquote(operation_definition.path_parameters),
unquote(argument_values)
)
Stripe.Request.new_request(opts)
|> Stripe.Request.put_endpoint(path)
|> Stripe.Request.put_method(unquote(operation_definition.method))
|> Stripe.Request.make_request()
end
end
end
end
deprecated =
if operation_definition.deprecated do
quote do
@deprecated "Stripe has deprecated this operation"
end
else
quote do
end
end
ast =
quote do
unquote(deprecated)
@doc unquote(operation_definition.description)
unquote(function_code)
end
{ast, object_types}
end
{funcs, types} = Enum.unzip(funcs_types)
fields = component.properties |> Map.keys() |> Enum.map(&String.to_atom/1)
# TODO fix uniq
types =
List.flatten(types)
|> Enum.uniq_by(fn {_, meta, _} -> meta[:name] end)
|> Enum.sort()
|> Enum.map(&to_type_spec/1)
specs =
Enum.map(component.properties, fn {key, value} ->
{String.to_atom(key), build_spec(value, modules)}
end)
|> Enum.sort()
typedoc_fields =
component.properties
|> Enum.sort()
|> Enum.map_join("\n", fn {key, value} -> typedoc(key, value) end)
typedoc = """
The `#{component.name}` type.
#{typedoc_fields}
"""
type_doc =
if fields != nil do
quote do
defstruct unquote(fields)
@typedoc unquote(typedoc)
@type t :: %__MODULE__{
unquote_splicing(specs)
}
end
else
quote do
end
end
body =
quote do
use Stripe.Entity
@moduledoc unquote(component.description)
unquote(type_doc)
unquote_splicing(types)
unquote_splicing(funcs)
end
# Module.create(component.module, body, Macro.Env.location(__ENV__))
code =
quote do
defmodule unquote(component.module) do
unquote(body)
end
end
bin =
code
|> Macro.to_string()
|> Code.format_string!()
[_ | names] = Module.split(component.module)
filename = names |> Enum.map_join("__", &Macro.underscore/1)
File.write!("lib/generated/#{filename}.ex", bin)
end
{:ok, blueprint}
end
defp unnest_object_types(params) do
Macro.postwalk(params, MapSet.new(), fn
{:object, meta, children}, acc ->
if meta[:name] == nil || children == [] do
{{:object, meta, children}, acc}
else
{{:ref, [name: meta[:name]], []}, MapSet.put(acc, {:object, meta, children})}
end
other, acc ->
{other, acc}
end)
end
defp to_type_spec({:object, meta, children}) do
specs = Enum.map(children, &to_spec_map/1)
name = type_spec_name(meta[:name])
quote do
@typedoc unquote(meta[:description])
@type unquote(Macro.var(name, __MODULE__)) :: %{
unquote_splicing(specs)
}
end
end
defp to_type_spec({:array, meta, [child]} = _ast) do
name = type_spec_name(meta[:name])
quote do
@typedoc unquote(meta[:description])
@type unquote(Macro.var(name, __MODULE__)) :: unquote(to_type(child))
end
end
defp to_type_spec({_, meta, children}) do
specs = Enum.map(children, &to_spec_map/1)
name = type_spec_name(meta[:name])
quote do
@typedoc unquote(meta[:description])
@type unquote(Macro.var(name, __MODULE__)) :: %{
unquote_splicing(specs)
}
end
end
defp type_spec_name(name) do
if name in [:reference] do
:reference_0
else
name
end
end
defp to_inline_spec({_, _meta, children}) do
specs = Enum.map(children, &to_spec_map/1)
quote do
%{
unquote_splicing(specs)
}
end
end
defp to_spec_map({:array, meta, [_type]} = ast) do
{to_name(meta), to_type(ast)}
end
defp to_spec_map({:any_of, meta, [type | tail]}) do
{to_name(meta),
quote do
unquote(to_type(type)) | unquote(to_type(tail))
end}
end
defp to_spec_map({:ref, meta, _} = ast) do
{to_name(meta), to_type(ast)}
end
defp to_spec_map({_type, meta, _children} = ast) do
{to_name(meta), to_type(ast)}
end
defp to_name(meta) do
if meta[:required] do
meta[:name]
else
quote do
optional(unquote(meta[:name]))
end
end
end
def to_type([type]) do
quote do
unquote(to_type(type))
end
end
def to_type([type | tail]) do
quote do
unquote(to_type(type)) | unquote(to_type(tail))
end
end
def to_type({:ref, meta, _}) do
Macro.var(meta[:name], __MODULE__)
end
def to_type({:array, _meta, [type]}) do
quote do
list(unquote(to_type(type)))
end
end
def to_type({:any_of, _, [type | tail]}) do
quote do
unquote(to_type(type)) | unquote(to_type(tail))
end
end
def to_type({:string, metadata, _}) do
if metadata[:enum] do
to_type(metadata[:enum])
else
to_type(:string)
end
end
def to_type({:object, metadata, _}) do
if metadata[:name] == :metadata do
quote do
%{optional(binary) => binary}
end
else
quote do
map()
end
end
end
def to_type({type, _, _}) do
to_type(type)
end
def to_type(type) when type in [:boolean, :number, :integer, :float] do
quote do
unquote(Macro.var(type, __MODULE__))
end
end
def to_type(:string) do
quote do
binary
end
end
def to_type(type) do
type
end
defp return_spec(%OpenApiGen.Blueprint.Reference{name: name}) do
module = module_from_ref(name)
quote do
unquote(module).t()
end
end
defp return_spec(%OpenApiGen.Blueprint.ListOf{type_of: type}) do
quote do
Stripe.List.t(unquote(return_spec(type)))
end
end
defp return_spec(%OpenApiGen.Blueprint.SearchResult{type_of: type}) do
quote do
Stripe.SearchResult.t(unquote(return_spec(type)))
end
end
defp return_spec(%{any_of: [type]} = _type) do
return_spec(type)
end
defp return_spec(%OpenApiGen.Blueprint.AnyOf{any_of: [any_of | tail]} = type) do
type = Map.put(type, :any_of, tail)
{:|, [], [return_spec(any_of), return_spec(type)]}
end
defp return_spec(_) do
[]
end
defp build_spec(%{"nullable" => true} = type, modules) do
type = Map.delete(type, "nullable")
{:|, [], [build_spec(type, modules), nil]}
end
defp build_spec(%{"anyOf" => [type]} = _type, modules) do
build_spec(type, modules)
end
defp build_spec(%{"anyOf" => [any_of | tail]} = type, modules) do
type = Map.put(type, "anyOf", tail)
{:|, [], [build_spec(any_of, modules), build_spec(type, modules)]}
end
defp build_spec(%{"type" => "string"}, _) do
quote do
binary
end
end
defp build_spec(%{"type" => "boolean"}, _) do
quote do
boolean
end
end
defp build_spec(%{"type" => "integer"}, _) do
quote do
integer
end
end
defp build_spec(%{"$ref" => ref}, modules) do
module = module_from_ref(ref)
if module in modules do
quote do
unquote(module).t()
end
else
quote do
term
end
end
end
defp build_spec(_, _) do
quote do
term
end
end
defp module_from_ref(ref) do
module =
ref |> String.split("/") |> List.last() |> String.split(".") |> Enum.map(&Macro.camelize/1)
Module.concat(["Stripe" | module])
end
defp typedoc(field, props) do
" * `#{field}` #{props["description"]}"
end
defp lookup_operation(path, operations) do
Map.get(operations, path)
end
end