feat: Recursively generate resource code

This commit is contained in:
Robert Prehn 2024-02-23 12:19:58 -06:00
parent 73dd9cc152
commit a6016e31fd
No known key found for this signature in database
6 changed files with 115 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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