feat: Implement embedded schemas
This commit is contained in:
parent
a6016e31fd
commit
0bd0153204
11 changed files with 192 additions and 7 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -24,3 +24,5 @@ kindling-*.tar
|
|||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
|
||||
/lib/fhir/
|
||||
|
|
19
lib/kindling/config.ex
Normal file
19
lib/kindling/config.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Kindling.Config do
|
||||
@default_embedded_resources [
|
||||
"Coding",
|
||||
"Duration",
|
||||
"Extension",
|
||||
"Identifier",
|
||||
"Meta",
|
||||
"Element",
|
||||
"CodeableConcept",
|
||||
"Narrative",
|
||||
"Period",
|
||||
"Reference",
|
||||
"ResourceList"
|
||||
]
|
||||
|
||||
def embedded_resources() do
|
||||
Application.get_env(:kindling, :embedded_resources, @default_embedded_resources)
|
||||
end
|
||||
end
|
50
lib/kindling/embed_template.eex
Normal file
50
lib/kindling/embed_template.eex
Normal file
|
@ -0,0 +1,50 @@
|
|||
defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@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") %>
|
||||
]
|
||||
|
||||
embedded_schema do
|
||||
<%= if @properties.const != [] do %># Constants<% end %>
|
||||
<%= for {name, df, _} <- @properties.const do %>field :<%= Recase.to_snake(name) %>, :string, virtual: true, default: "<%= df["const"] %>"
|
||||
<% end %>
|
||||
<%= 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 %>
|
||||
<%= if @properties.embed_one != [] do %># Embed One<% end %>
|
||||
<%= for {name, df, _} <- @properties.embed_one do %>embed_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.embed_many != [] do %># Embed Many<% end %>
|
||||
<%= for {name, df, _} <- @properties.embed_many do %>embed_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.has_one != [] do %># Has One<% end %>
|
||||
<%= for {name, df, _} <- @properties.has_one do %>has_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.has_many != [] do %># Has Many<% end %>
|
||||
<%= for {name, df, _} <- @properties.has_many do %>has_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["items"]["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @backlinks != [] do %># belongs_to<% end %>
|
||||
<%= for name <- @backlinks do %>belongs_to :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= class_name(name) %>
|
||||
<% 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
|
|
@ -1,4 +1,5 @@
|
|||
defmodule Kindling.Schema do
|
||||
alias Kindling.Config
|
||||
alias Kindling.Version
|
||||
alias Kindling.Schema.Resource
|
||||
|
||||
|
@ -8,6 +9,33 @@ defmodule Kindling.Schema do
|
|||
filename
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> build_backlinks()
|
||||
end
|
||||
|
||||
def build_backlinks(schema) do
|
||||
new_defs =
|
||||
schema["definitions"]
|
||||
|> Enum.map(fn {name, def} -> {name, Map.put(def, "__backlinks", [])} end)
|
||||
|> Map.new()
|
||||
|
||||
schema =
|
||||
Map.put(schema, "definitions", new_defs)
|
||||
|
||||
defs =
|
||||
schema["definitions"]
|
||||
|> Map.drop(Config.embedded_resources())
|
||||
|> Enum.reduce(schema["definitions"], fn {name, df}, definitions ->
|
||||
refs = Resource.refs(df)
|
||||
|
||||
Enum.reduce(refs, definitions, fn ref, definitions ->
|
||||
prior_backlinks = definitions[ref]["__backlinks"]
|
||||
new_backlinks = [name | prior_backlinks]
|
||||
new_def = Map.put(Map.get(definitions, ref), "__backlinks", new_backlinks)
|
||||
Map.put(definitions, ref, new_def)
|
||||
end)
|
||||
end)
|
||||
|
||||
Map.put(schema, "definitions", defs)
|
||||
end
|
||||
|
||||
def refs_recursive(schema, root_name) do
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule Kindling.Schema.Resource do
|
||||
alias Kindling.Config
|
||||
|
||||
@empty_properties %{array: [], const: [], has_one: [], has_many: [], enum: [], value: []}
|
||||
|
||||
def properties(df) do
|
||||
|
@ -8,6 +10,8 @@ defmodule Kindling.Schema.Resource do
|
|||
def grouped_properties(df) do
|
||||
properties =
|
||||
(df["properties"] || %{})
|
||||
|> Map.delete("id")
|
||||
|> Enum.reject(&is_element/1)
|
||||
|> Enum.map(fn {key, value} -> {key, value, property_type(value)} end)
|
||||
|> Enum.group_by(fn {_key, _value, type} ->
|
||||
type
|
||||
|
@ -42,8 +46,31 @@ defmodule Kindling.Schema.Resource do
|
|||
end
|
||||
|
||||
def property_type(%{"const" => _}), do: :const
|
||||
def property_type(%{"$ref" => _}), do: :has_one
|
||||
def property_type(%{"items" => %{"$ref" => _}}), do: :has_many
|
||||
|
||||
def property_type(%{"$ref" => "#/definitions/" <> name}) do
|
||||
if is_class_name(name) do
|
||||
:has_one
|
||||
else
|
||||
:value
|
||||
end
|
||||
end
|
||||
|
||||
for embed <- Config.embedded_resources() do
|
||||
def property_type(%{"$ref" => "#/definitions/" <> unquote(embed)}), do: :embed_one
|
||||
|
||||
def property_type(%{"items" => %{"$ref" => "#/definitions/" <> unquote(embed)}}) do
|
||||
:embed_many
|
||||
end
|
||||
end
|
||||
|
||||
def property_type(%{"items" => %{"$ref" => "#/definitions/" <> name}}) do
|
||||
if is_class_name(name) do
|
||||
:has_many
|
||||
else
|
||||
:array
|
||||
end
|
||||
end
|
||||
|
||||
def property_type(%{"items" => %{"enum" => _}}), do: :array
|
||||
def property_type(%{"enum" => _}), do: :enum
|
||||
def property_type(_), do: :value
|
||||
|
@ -61,6 +88,15 @@ defmodule Kindling.Schema.Resource do
|
|||
nil
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.filter(&is_class_name/1)
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
def is_class_name(name) do
|
||||
!(name in Config.embedded_resources()) && Regex.match?(~r/^[A-Z]/, name)
|
||||
end
|
||||
|
||||
def is_element({_name, %{"$ref" => "#/definitions/Element"}}), do: true
|
||||
def is_element({name, %{"items" => items}}), do: is_element({name, items})
|
||||
def is_element(_), do: false
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
||||
defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
|
@ -9,6 +9,7 @@ defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
|||
<%= @required_fields |> Enum.map(fn name -> ~s("#{Recase.to_snake(name)}") end) |> Enum.join(",\n") %>
|
||||
]
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "<%= Recase.to_snake(@resource_name) %>" do
|
||||
<%= if @properties.const != [] do %># Constants<% end %>
|
||||
<%= for {name, df, _} <- @properties.const do %>field :<%= Recase.to_snake(name) %>, :string, virtual: true, default: "<%= df["const"] %>"
|
||||
|
@ -21,12 +22,21 @@ defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
|
|||
<%= 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 %>
|
||||
<%= if @properties.embed_one != [] do %># Embed One<% end %>
|
||||
<%= for {name, df, _} <- @properties.embed_one do %>embed_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.embed_many != [] do %># Embed Many<% end %>
|
||||
<%= for {name, df, _} <- @properties.embed_many do %>embed_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.has_one != [] do %># Has One<% end %>
|
||||
<%= for {name, df, _} <- @properties.has_one do %>has_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @properties.has_many != [] do %># Has Many<% end %>
|
||||
<%= for {name, df, _} <- @properties.has_many do %>has_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["items"]["$ref"]) %>
|
||||
<% end %>
|
||||
<%= if @backlinks != [] do %># belongs_to<% end %>
|
||||
<%= for name <- @backlinks do %>belongs_to :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= class_name(name) %>
|
||||
<% end %>
|
||||
end
|
||||
|
||||
def base_changeset(data \\ %__MODULE__{}, attrs) do
|
||||
|
|
|
@ -3,9 +3,11 @@ defmodule Kindling.Templates do
|
|||
|
||||
import Kindling.Templates.Functions
|
||||
|
||||
alias Kindling.Config
|
||||
alias Kindling.Schema.Resource
|
||||
|
||||
EEx.function_from_file(:def, :render, "lib/kindling/template.eex", [:assigns])
|
||||
EEx.function_from_file(:def, :render_embedded, "lib/kindling/embed_template.eex", [:assigns])
|
||||
|
||||
def resource_code(namespace, version, resource_name, resource) do
|
||||
assigns = %{
|
||||
|
@ -14,10 +16,19 @@ defmodule Kindling.Templates do
|
|||
resource_name: resource_name,
|
||||
properties: Resource.grouped_properties(resource),
|
||||
all_fields: Resource.all_fields(resource),
|
||||
required_fields: Resource.required_fields(resource)
|
||||
required_fields: Resource.required_fields(resource),
|
||||
backlinks: resource["__backlinks"]
|
||||
}
|
||||
|
||||
assigns |> render() |> Code.format_string!(file: "#{resource_name}.ex")
|
||||
if resource_name in Config.embedded_resources() do
|
||||
assigns
|
||||
|> render_embedded()
|
||||
|> Code.format_string!(file: "#{resource_name}.ex")
|
||||
else
|
||||
assigns
|
||||
|> render()
|
||||
|> Code.format_string!(file: "#{resource_name}.ex")
|
||||
end
|
||||
end
|
||||
|
||||
def write_code(namespace, version, resource_name, resource) do
|
||||
|
|
|
@ -18,8 +18,32 @@ defmodule Kindling.Templates.Functions do
|
|||
when pattern in @decimal_patterns,
|
||||
do: :decimal
|
||||
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/boolean"}), do: :boolean
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/string"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/integer"}), do: :integer
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/positiveInt"}), do: :integer
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/unsignedInt"}), do: :integer
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/decimal"}), do: :decimal
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/code"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/uri"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/url"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/xhtml"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/markdown"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/base64Binary"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/dateTime"}), do: :utc_datetime_usec
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/instant"}), do: :utc_datetime_usec
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/time"}), do: :time_usec
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/canonical"}), do: :string
|
||||
def fhir_type_to_ecto(%{"$ref" => "#/definitions/id"}), do: :binary_id
|
||||
|
||||
def fhir_type_to_ecto(%{"type" => "array", "items" => %{"enum" => _}}), do: {:array, :string}
|
||||
|
||||
def fhir_type_to_ecto(%{"type" => "array", "items" => items}),
|
||||
do: {:array, fhir_type_to_ecto(items)}
|
||||
|
||||
def ref_to_class_name("#/definitions/" <> name),
|
||||
do: name |> String.replace("_", ".") |> Recase.to_pascal()
|
||||
do: class_name(name)
|
||||
|
||||
def class_name(name),
|
||||
do: name |> String.split("_") |> Enum.map(&Recase.to_pascal/1) |> Enum.join(".")
|
||||
end
|
||||
|
|
|
@ -15,7 +15,9 @@ defmodule Mix.Tasks.Kindling.GenerateSchemas do
|
|||
[namespace, version, root] = args
|
||||
|
||||
schema = Kindling.Schema.schema_object(version)
|
||||
to_generate = Kindling.Schema.refs_recursive(schema, root)
|
||||
|
||||
to_generate =
|
||||
Kindling.Schema.refs_recursive(schema, root)
|
||||
|
||||
Enum.each(
|
||||
to_generate,
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -21,6 +21,7 @@ defmodule Kindling.MixProject do
|
|||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:ecto, "~> 3.11", only: [:dev, :test]},
|
||||
{:jason, "~> 1.4"},
|
||||
{:recase, "~> 0.7.0"},
|
||||
{:req, "~> 0.4.11"}
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -1,5 +1,7 @@
|
|||
%{
|
||||
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
|
||||
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
|
|
Loading…
Reference in a new issue