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
	
	 Robert Prehn
						Robert Prehn