Packages
Complete Stripe SDK for Elixir with parity to the official Ruby SDK. V1+V2 coverage (193 services, 320 resource structs, 525 documented params). Per-event modules, Finch HTTP/2, RustyJSON, automatic retries, OAuth, webhooks, telemetry, per-client config, streaming pagination.
Current section
Files
Jump to
Current section
Files
lib/stripe/generator/resource_generator.ex
defmodule Stripe.Generator.ResourceGenerator do
@moduledoc false
alias Stripe.Generator.DocFormatter
alias Stripe.Generator.Naming
@file_header "# File generated from our OpenAPI spec"
@doc """
Generate resource modules from parsed spec.
Returns `[{file_path, content}]`.
"""
def generate(spec) do
# Map ALL resource schema IDs to modules so typespecs can reference the actual
# struct type. Non-primary schemas (e.g. payment_method_details_crypto) share
# the same {class_name, package} as their primary, so they resolve to the same module.
ref_to_module =
spec.resources
|> Map.new(fn r -> {r.schema_id, Naming.resource_module(r.class_name, r.package)} end)
spec.resources
|> Enum.filter(& &1.is_primary)
|> Enum.map(fn resource ->
generate_resource(resource, ref_to_module)
end)
end
defp generate_resource(resource, ref_to_module) do
module = Naming.resource_module(resource.class_name, resource.package)
path = Naming.module_to_path(module)
props = resource.properties
required = resource.required
expandable_set = MapSet.new(resource.expandable_fields)
struct_fields = Enum.map_join(props, ", ", fn p -> ":#{p.name}" end)
type_fields =
props
|> Enum.map_join(",\n", fn p ->
type_str = type_to_typespec(p.type, p.name, resource, ref_to_module)
type_str =
if MapSet.member?(required, p.name) do
type_str
else
"#{type_str} | nil"
end
" #{p.name}: #{type_str}"
end)
expandable_line =
if resource.expandable_fields != [] do
fields = Enum.map_join(resource.expandable_fields, ", ", &"\"#{&1}\"")
"\n def expandable_fields, do: [#{fields}]\n"
else
""
end
inner_type_blocks = generate_inner_types(resource.inner_types, " ", ref_to_module)
# Build __inner_types__ from BOTH inner types (nested modules) and resource inner refs
inner_entries =
resource.inner_types
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map(fn {name, inner} -> {name, "__MODULE__.#{inner.class_name}"} end)
resource_ref_entries =
(resource[:resource_inner_refs] || %{})
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map(fn {name, meta} ->
mod = Naming.resource_module(meta.class_name, meta.package)
{name, inspect(mod)}
end)
all_inner_entries = Enum.sort_by(inner_entries ++ resource_ref_entries, &elem(&1, 0))
inner_types_fn =
if all_inner_entries != [] do
entries =
all_inner_entries
|> Enum.map_join(",\n", fn {name, mod_str} ->
~s( "#{name}" => #{mod_str})
end)
"""
def __inner_types__ do
%{
#{entries}
}
end
"""
else
""
end
# @moduledoc from schema title + description
moduledoc_content =
DocFormatter.schema_moduledoc(resource.title, resource.description) ||
"#{resource.class_name} resource."
moduledoc = ~s( @moduledoc """\n #{moduledoc_content}\n """)
# @typedoc from property descriptions
enriched_props =
Enum.map(props, fn p ->
Map.put(p, :expandable, MapSet.member?(expandable_set, p.name))
end)
typedoc =
case DocFormatter.build_typedoc_table(enriched_props) do
nil -> ""
table -> "\n @typedoc \"\"\"\n#{table}\n \"\"\""
end
content = """
#{@file_header}
defmodule #{inspect(module)} do
#{moduledoc}
#{typedoc}
@type t :: %__MODULE__{
#{type_fields}
}
defstruct [#{struct_fields}]
@object_name "#{resource.object_name}"
def object_name, do: @object_name
#{expandable_line}#{inner_type_blocks}#{inner_types_fn}end
"""
{path, content}
end
defp generate_inner_types(inner_types, indent, _ref_to_module)
when map_size(inner_types) == 0 do
_ = indent
""
end
defp generate_inner_types(inner_types, indent, ref_to_module) do
inner_types
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map_join("", fn {_name, inner} ->
generate_inner_type_module(inner, indent, ref_to_module)
end)
end
defp generate_inner_type_module(inner, indent, ref_to_module) do
props = Enum.sort_by(inner.properties, & &1.name)
struct_fields = Enum.map_join(props, ", ", fn p -> ":#{p.name}" end)
type_fields =
props
|> Enum.map_join(",\n", fn p ->
type_str = simple_typespec(p.type, ref_to_module)
"#{indent} #{p.name}: #{type_str} | nil"
end)
inner_inner_types = inner[:inner_types] || inner.inner_types
nested_blocks = generate_inner_types(inner_inner_types, indent <> " ", ref_to_module)
inner_types_fn_block =
if is_map(inner_inner_types) and map_size(inner_inner_types) > 0 do
entries =
inner_inner_types
|> Enum.sort_by(fn {name, _} -> name end)
|> Enum.map_join(",\n", fn {name, nested} ->
~s(#{indent} "#{name}" => __MODULE__.#{nested.class_name})
end)
"""
#{indent} def __inner_types__ do
#{indent} %{
#{entries}
#{indent} }
#{indent} end
"""
else
""
end
typedoc =
case DocFormatter.build_typedoc_table(props) do
nil -> ""
table -> "\n#{indent} @typedoc \"\"\"\n#{table}\n#{indent} \"\"\""
end
"""
#{indent}defmodule #{inner.class_name} do
#{indent} @moduledoc "Nested struct within the parent resource."
#{typedoc}
#{indent} @type t :: %__MODULE__{
#{type_fields}
#{indent} }
#{indent} defstruct [#{struct_fields}]
#{nested_blocks}#{inner_types_fn_block}#{indent}end
"""
end
# -- Type specs -------------------------------------------------------------
defp type_to_typespec({:expandable_ref, ref_name}, _field_name, _resource, ref_to_module) do
case Map.get(ref_to_module, ref_name) do
nil -> "map()"
mod -> "String.t() | #{inspect(mod)}.t()"
end
end
defp type_to_typespec({:ref, ref_name}, _field_name, _resource, ref_to_module) do
case Map.get(ref_to_module, ref_name) do
nil -> "map()"
mod -> "#{inspect(mod)}.t()"
end
end
defp type_to_typespec({:list, inner}, field_name, resource, ref_to_module) do
inner_spec = type_to_typespec(inner, field_name, resource, ref_to_module)
"[#{inner_spec}]"
end
defp type_to_typespec({:nullable, inner}, field_name, resource, ref_to_module) do
type_to_typespec(inner, field_name, resource, ref_to_module)
end
defp type_to_typespec({:inner, name}, _field_name, _resource, _ref_to_module),
do: "__MODULE__.#{name}.t()"
defp type_to_typespec({:map, inner}, _field_name, _resource, ref_to_module) do
inner_spec = simple_typespec(inner, ref_to_module)
"%{String.t() => #{inner_spec}}"
end
defp type_to_typespec({:union, types}, _field_name, _resource, ref_to_module) do
types
|> Enum.map(&simple_typespec(&1, ref_to_module))
|> Enum.uniq()
|> Enum.join(" | ")
end
defp type_to_typespec(:string, _, _, _), do: "String.t()"
defp type_to_typespec(:integer, _, _, _), do: "integer()"
defp type_to_typespec(:float, _, _, _), do: "float()"
defp type_to_typespec(:boolean, _, _, _), do: "boolean()"
defp type_to_typespec(:map, _, _, _), do: "map()"
defp type_to_typespec(_, _, _, _), do: "term()"
defp simple_typespec(:string, _), do: "String.t()"
defp simple_typespec(:integer, _), do: "integer()"
defp simple_typespec(:float, _), do: "float()"
defp simple_typespec(:boolean, _), do: "boolean()"
defp simple_typespec(:map, _), do: "map()"
defp simple_typespec({:map, inner}, ref_to_module) do
inner_spec = simple_typespec(inner, ref_to_module)
"%{String.t() => #{inner_spec}}"
end
defp simple_typespec({:union, types}, ref_to_module) do
types
|> Enum.map(&simple_typespec(&1, ref_to_module))
|> Enum.uniq()
|> Enum.join(" | ")
end
defp simple_typespec({:list, inner}, ref_to_module),
do: "[#{simple_typespec(inner, ref_to_module)}]"
defp simple_typespec({:nullable, inner}, ref_to_module),
do: simple_typespec(inner, ref_to_module)
defp simple_typespec({:ref, ref_name}, ref_to_module) do
case Map.get(ref_to_module, ref_name) do
nil -> "map()"
mod -> "#{inspect(mod)}.t()"
end
end
defp simple_typespec({:expandable_ref, ref_name}, ref_to_module) do
case Map.get(ref_to_module, ref_name) do
nil -> "String.t() | map()"
mod -> "String.t() | #{inspect(mod)}.t()"
end
end
defp simple_typespec({:inner, name}, _), do: "__MODULE__.#{name}.t()"
defp simple_typespec(_, _), do: "term()"
end