feat: Recursively generate resource code
This commit is contained in:
parent
73dd9cc152
commit
a6016e31fd
6 changed files with 115 additions and 8 deletions
|
@ -1,5 +1,6 @@
|
|||
defmodule Kindling.Schema do
|
||||
alias Kindling.Version
|
||||
alias Kindling.Schema.Resource
|
||||
|
||||
def schema_object(version) do
|
||||
filename = Path.join(Version.version_dir(version), "fhir.schema.json")
|
||||
|
@ -8,4 +9,17 @@ defmodule Kindling.Schema do
|
|||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
end
|
||||
|
||||
def refs_recursive(schema, root_name) do
|
||||
do_recurse(schema, MapSet.new([root_name]), [root_name])
|
||||
end
|
||||
|
||||
defp do_recurse(_schema, visited, []), do: visited
|
||||
|
||||
defp do_recurse(schema, visited, [hd | tail]) do
|
||||
refs = Resource.refs(schema["definitions"][hd])
|
||||
new_refs = MapSet.difference(refs, visited)
|
||||
new_visited = MapSet.union(visited, refs)
|
||||
do_recurse(schema, new_visited, MapSet.to_list(new_refs) ++ tail)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Kindling.Schema.Resource do
|
||||
@empty_properties %{const: [], has_one: [], has_many: [], enum: [], value: []}
|
||||
@empty_properties %{array: [], const: [], has_one: [], has_many: [], enum: [], value: []}
|
||||
|
||||
def properties(df) do
|
||||
df["properties"]
|
||||
|
@ -7,7 +7,7 @@ defmodule Kindling.Schema.Resource do
|
|||
|
||||
def grouped_properties(df) do
|
||||
properties =
|
||||
(df["properties"] || [])
|
||||
(df["properties"] || %{})
|
||||
|> Enum.map(fn {key, value} -> {key, value, property_type(value)} end)
|
||||
|> Enum.group_by(fn {_key, _value, type} ->
|
||||
type
|
||||
|
@ -16,8 +16,25 @@ defmodule Kindling.Schema.Resource do
|
|||
Map.merge(@empty_properties, properties)
|
||||
end
|
||||
|
||||
def all_fields(df) do
|
||||
(df["properties"] || %{})
|
||||
|> Enum.filter(fn {_name, definition} ->
|
||||
property_type(definition) in [:array, :enum, :value]
|
||||
end)
|
||||
|> Enum.map(fn {name, _} -> name end)
|
||||
end
|
||||
|
||||
def required_fields(df) do
|
||||
df
|
||||
|> required()
|
||||
|> Enum.filter(fn {_name, definition} ->
|
||||
property_type(definition) in [:array, :enum, :value]
|
||||
end)
|
||||
|> Enum.map(fn {name, _} -> name end)
|
||||
end
|
||||
|
||||
def required(df) do
|
||||
Map.take(df["properties"], df["required"])
|
||||
Map.take(df["properties"] || %{}, df["required"] || [])
|
||||
end
|
||||
|
||||
def required?(df, key) do
|
||||
|
@ -26,7 +43,24 @@ defmodule Kindling.Schema.Resource do
|
|||
|
||||
def property_type(%{"const" => _}), do: :const
|
||||
def property_type(%{"$ref" => _}), do: :has_one
|
||||
def property_type(%{"items" => _}), do: :has_many
|
||||
def property_type(%{"items" => %{"$ref" => _}}), do: :has_many
|
||||
def property_type(%{"items" => %{"enum" => _}}), do: :array
|
||||
def property_type(%{"enum" => _}), do: :enum
|
||||
def property_type(_), do: :value
|
||||
|
||||
def refs(df) do
|
||||
(df["properties"] || %{})
|
||||
|> Enum.map(fn
|
||||
{_, %{"$ref" => "#/definitions/" <> name}} ->
|
||||
name
|
||||
|
||||
{_, %{"items" => %{"$ref" => "#/definitions/" <> name}}} ->
|
||||
name
|
||||
|
||||
_other ->
|
||||
nil
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> MapSet.new()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@fields [
|
||||
<%= @all_fields |> Enum.map(fn name -> ~s("#{Recase.to_snake(name)}") end) |> Enum.join(",\n") %>
|
||||
]
|
||||
@required_fields [
|
||||
<%= @required_fields |> Enum.map(fn name -> ~s("#{Recase.to_snake(name)}") end) |> Enum.join(",\n") %>
|
||||
]
|
||||
|
||||
schema "<%= Recase.to_snake(@resource_name) %>" do
|
||||
<%= if @properties.const != [] do %># Constants<% end %>
|
||||
|
@ -8,6 +16,8 @@ defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
|||
<%= if @properties.value != [] do %># Fields<% end %>
|
||||
<%= for {name, df, _} <- @properties.value do %>field :<%= Recase.to_snake(name) %>, :<%= fhir_type_to_ecto(df) %>
|
||||
<% end %>
|
||||
<%= for {name, df, _} <- @properties.array do %>field :<%= Recase.to_snake(name) %>, <%= inspect(fhir_type_to_ecto(df)) %>
|
||||
<% end %>
|
||||
<%= if @properties.enum != [] do %># Enum<% end %>
|
||||
<%= for {name, df, _} <- @properties.enum do %>field :<%= Recase.to_snake(name) %>, Ecto.Enum, values: <%= inspect(df["enum"] |> Enum.map(&Recase.to_snake(&1)) |> Enum.map(&String.to_atom/1)) %>
|
||||
<% end %>
|
||||
|
@ -18,4 +28,14 @@ defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
|||
<%= for {name, df, _} <- @properties.has_many do %>has_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["items"]["$ref"]) %>
|
||||
<% end %>
|
||||
end
|
||||
|
||||
def base_changeset(data \\ %__MODULE__{}, attrs) do
|
||||
data
|
||||
|> cast(attrs, @fields)
|
||||
<%= for {name, df, _} <- @properties.has_one do %>|> cast_assoc(:<%= Recase.to_snake(name) %>, with: &<%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>.base_changeset/1)
|
||||
<% end %>
|
||||
<%= for {name, df, _} <- @properties.has_many do %>|> cast_assoc(:<%= Recase.to_snake(name) %>, with: &<%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["items"]["$ref"]) %>.base_changeset/1)
|
||||
<% end %>
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,17 +12,28 @@ defmodule Kindling.Templates do
|
|||
namespace: namespace,
|
||||
version: version,
|
||||
resource_name: resource_name,
|
||||
properties: Resource.grouped_properties(resource)
|
||||
properties: Resource.grouped_properties(resource),
|
||||
all_fields: Resource.all_fields(resource),
|
||||
required_fields: Resource.required_fields(resource)
|
||||
}
|
||||
|
||||
assigns |> render() |> Code.format_string!(file: "#{resource_name}.ex")
|
||||
end
|
||||
|
||||
def write_code(namespace, version, resource_name, resource) do
|
||||
dir = Path.join(String.downcase(namespace), String.downcase(version))
|
||||
file = Path.join(dir, "#{Recase.to_snake(resource_name)}.ex")
|
||||
dir = Path.join(["lib", String.downcase(namespace), String.downcase(version)])
|
||||
file = Path.join([dir | to_source_file_name(resource_name)])
|
||||
|
||||
File.mkdir_p!(dir)
|
||||
File.mkdir_p!(Path.dirname(file))
|
||||
File.write!(file, resource_code(namespace, version, resource_name, resource))
|
||||
end
|
||||
|
||||
def to_source_file_name(resource_name) do
|
||||
resource_name
|
||||
|> String.split("_")
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&Recase.to_snake/1)
|
||||
|> then(fn [hd | tail] -> ["#{hd}.ex" | tail] end)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Kindling.Templates.Functions do
|
|||
@decimal_patterns ["^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$"]
|
||||
|
||||
def fhir_type_to_ecto(%{"type" => "string"}), do: :string
|
||||
def fhir_type_to_ecto(%{"type" => "boolean"}), do: :boolean
|
||||
|
||||
def fhir_type_to_ecto(%{"type" => "number", "pattern" => pattern})
|
||||
when pattern in @int_patterns,
|
||||
|
@ -17,6 +18,8 @@ defmodule Kindling.Templates.Functions do
|
|||
when pattern in @decimal_patterns,
|
||||
do: :decimal
|
||||
|
||||
def fhir_type_to_ecto(%{"type" => "array", "items" => %{"enum" => _}}), do: {:array, :string}
|
||||
|
||||
def ref_to_class_name("#/definitions/" <> name),
|
||||
do: name |> String.replace("_", ".") |> Recase.to_pascal()
|
||||
end
|
||||
|
|
25
lib/mix/tasks/kindling/generate_schemas.ex
Normal file
25
lib/mix/tasks/kindling/generate_schemas.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Mix.Tasks.Kindling.GenerateSchemas do
|
||||
@moduledoc "Generates FHIR schemas"
|
||||
@shortdoc "Generates FHIR schemas"
|
||||
|
||||
use Mix.Task
|
||||
|
||||
@impl Mix.Task
|
||||
def run(args) do
|
||||
if Enum.count(args) != 3 do
|
||||
Mix.shell().error(
|
||||
"usage: mix mix kindling.generate_schemas <namespace> <R5|R4|R3> <root resource name>"
|
||||
)
|
||||
end
|
||||
|
||||
[namespace, version, root] = args
|
||||
|
||||
schema = Kindling.Schema.schema_object(version)
|
||||
to_generate = Kindling.Schema.refs_recursive(schema, root)
|
||||
|
||||
Enum.each(
|
||||
to_generate,
|
||||
&Kindling.Templates.write_code(namespace, version, &1, schema["definitions"][&1])
|
||||
)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue