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
 | 
					defmodule Kindling.Schema do
 | 
				
			||||||
  alias Kindling.Version
 | 
					  alias Kindling.Version
 | 
				
			||||||
 | 
					  alias Kindling.Schema.Resource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def schema_object(version) do
 | 
					  def schema_object(version) do
 | 
				
			||||||
    filename = Path.join(Version.version_dir(version), "fhir.schema.json")
 | 
					    filename = Path.join(Version.version_dir(version), "fhir.schema.json")
 | 
				
			||||||
| 
						 | 
					@ -8,4 +9,17 @@ defmodule Kindling.Schema do
 | 
				
			||||||
    |> File.read!()
 | 
					    |> File.read!()
 | 
				
			||||||
    |> Jason.decode!()
 | 
					    |> Jason.decode!()
 | 
				
			||||||
  end
 | 
					  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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
defmodule Kindling.Schema.Resource do
 | 
					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
 | 
					  def properties(df) do
 | 
				
			||||||
    df["properties"]
 | 
					    df["properties"]
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ defmodule Kindling.Schema.Resource do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def grouped_properties(df) do
 | 
					  def grouped_properties(df) do
 | 
				
			||||||
    properties =
 | 
					    properties =
 | 
				
			||||||
      (df["properties"] || [])
 | 
					      (df["properties"] || %{})
 | 
				
			||||||
      |> Enum.map(fn {key, value} -> {key, value, property_type(value)} end)
 | 
					      |> Enum.map(fn {key, value} -> {key, value, property_type(value)} end)
 | 
				
			||||||
      |> Enum.group_by(fn {_key, _value, type} ->
 | 
					      |> Enum.group_by(fn {_key, _value, type} ->
 | 
				
			||||||
        type
 | 
					        type
 | 
				
			||||||
| 
						 | 
					@ -16,8 +16,25 @@ defmodule Kindling.Schema.Resource do
 | 
				
			||||||
    Map.merge(@empty_properties, properties)
 | 
					    Map.merge(@empty_properties, properties)
 | 
				
			||||||
  end
 | 
					  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
 | 
					  def required(df) do
 | 
				
			||||||
    Map.take(df["properties"], df["required"])
 | 
					    Map.take(df["properties"] || %{}, df["required"] || [])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def required?(df, key) do
 | 
					  def required?(df, key) do
 | 
				
			||||||
| 
						 | 
					@ -26,7 +43,24 @@ defmodule Kindling.Schema.Resource do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def property_type(%{"const" => _}), do: :const
 | 
					  def property_type(%{"const" => _}), do: :const
 | 
				
			||||||
  def property_type(%{"$ref" => _}), do: :has_one
 | 
					  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(%{"enum" => _}), do: :enum
 | 
				
			||||||
  def property_type(_), do: :value
 | 
					  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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,13 @@
 | 
				
			||||||
defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
 | 
					defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
 | 
				
			||||||
  use Ecto.Schema
 | 
					  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
 | 
					  schema "<%= Recase.to_snake(@resource_name) %>" do
 | 
				
			||||||
    <%= if @properties.const != [] do %># Constants<% end %>
 | 
					    <%= if @properties.const != [] do %># Constants<% end %>
 | 
				
			||||||
| 
						 | 
					@ -8,6 +16,8 @@ defmodule <%= @namespace %>.<%= @version %>.<%= @resource_name %> do
 | 
				
			||||||
    <%= if @properties.value != [] do %># Fields<% end %>
 | 
					    <%= if @properties.value != [] do %># Fields<% end %>
 | 
				
			||||||
    <%= for {name, df, _} <- @properties.value do %>field :<%= Recase.to_snake(name) %>, :<%= fhir_type_to_ecto(df) %>
 | 
					    <%= for {name, df, _} <- @properties.value do %>field :<%= Recase.to_snake(name) %>, :<%= fhir_type_to_ecto(df) %>
 | 
				
			||||||
    <% end %>
 | 
					    <% 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 %>
 | 
					    <%= 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)) %>
 | 
					    <%= 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 %>
 | 
					    <% 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"]) %>
 | 
					    <%= for {name, df, _} <- @properties.has_many do %>has_many :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["items"]["$ref"]) %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
  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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,17 +12,28 @@ defmodule Kindling.Templates do
 | 
				
			||||||
      namespace: namespace,
 | 
					      namespace: namespace,
 | 
				
			||||||
      version: version,
 | 
					      version: version,
 | 
				
			||||||
      resource_name: resource_name,
 | 
					      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")
 | 
					    assigns |> render() |> Code.format_string!(file: "#{resource_name}.ex")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def write_code(namespace, version, resource_name, resource) do
 | 
					  def write_code(namespace, version, resource_name, resource) do
 | 
				
			||||||
    dir = Path.join(String.downcase(namespace), String.downcase(version))
 | 
					    dir = Path.join(["lib", String.downcase(namespace), String.downcase(version)])
 | 
				
			||||||
    file = Path.join(dir, "#{Recase.to_snake(resource_name)}.ex")
 | 
					    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))
 | 
					    File.write!(file, resource_code(namespace, version, resource_name, resource))
 | 
				
			||||||
  end
 | 
					  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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ defmodule Kindling.Templates.Functions do
 | 
				
			||||||
  @decimal_patterns ["^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$"]
 | 
					  @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" => "string"}), do: :string
 | 
				
			||||||
 | 
					  def fhir_type_to_ecto(%{"type" => "boolean"}), do: :boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def fhir_type_to_ecto(%{"type" => "number", "pattern" => pattern})
 | 
					  def fhir_type_to_ecto(%{"type" => "number", "pattern" => pattern})
 | 
				
			||||||
      when pattern in @int_patterns,
 | 
					      when pattern in @int_patterns,
 | 
				
			||||||
| 
						 | 
					@ -17,6 +18,8 @@ defmodule Kindling.Templates.Functions do
 | 
				
			||||||
      when pattern in @decimal_patterns,
 | 
					      when pattern in @decimal_patterns,
 | 
				
			||||||
      do: :decimal
 | 
					      do: :decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def fhir_type_to_ecto(%{"type" => "array", "items" => %{"enum" => _}}), do: {:array, :string}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def ref_to_class_name("#/definitions/" <> name),
 | 
					  def ref_to_class_name("#/definitions/" <> name),
 | 
				
			||||||
    do: name |> String.replace("_", ".") |> Recase.to_pascal()
 | 
					    do: name |> String.replace("_", ".") |> Recase.to_pascal()
 | 
				
			||||||
end
 | 
					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