Current section
Files
Jump to
Current section
Files
lib/kindling/schema/resource.ex
defmodule Kindling.Schema.Resource do
@moduledoc """
Functions for working on entries of the `definitions` section of the FHIR schema, which define
the Resources available in FHIR.
"""
@typedoc """
A module representing a given FHIR resource, generated by
[mix kindling.generate_schemas](`Mix.Tasks.Kindling.GenerateSchemas`).
"""
@type t() :: atom()
@typedoc """
An `Ecto.Schema` struct representing a FHIR Resource.
"""
@type schema() :: Ecto.Schema.t()
@typedoc """
A string-keyed map that represents one resource in the `definitions` section of the FHIR schema.
"""
@type definition() :: map()
defstruct array: [],
const: [],
embeds_one: [],
embeds_many: [],
has_one: [],
has_many: [],
enum: [],
value: []
@type grouped_properties_struct :: %__MODULE__{}
@doc """
Return the property fields of a given definition map.
"""
@spec properties(definition()) :: map()
def properties(df) do
df["properties"]
end
@doc """
Given the definition `df` and a list of root resource types, return a struct which contains
all the properties of `df`, grouped by the type of field that should be used to represent them (
:array, :const, :embeds_one, :embeds_many, :has_one, :has_many, :enum, :value).
"""
@spec grouped_properties(definition(), [String.t()]) :: grouped_properties_struct()
def grouped_properties(df, roots) do
properties =
(df["properties"] || %{})
|> Map.delete("id")
|> Enum.reject(&element?/1)
|> Enum.map(fn {key, value} -> {key, value, property_type(value, roots)} end)
|> Enum.group_by(fn {_key, _value, type} ->
type
end)
Map.merge(%__MODULE__{}, properties)
end
@doc """
Given the definition `df` return a list of all the properties which are "simple values", i.e.
arrays, enum values, or base value types like integer or string.
"""
@spec all_fields(definition()) :: [String.t()]
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
@doc """
Given the definition `df` return a list of all the properties which are "simple values", i.e.
arrays, enum values, or base value types like integer or string AND are required by the FHIR
spec.
"""
@spec required_fields(definition()) :: [String.t()]
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
@doc """
Return a map of all properties (simple or reference) that are required under the FHIR spec.
"""
@spec required(definition()) :: map()
def required(df) do
Map.take(df["properties"] || %{}, df["required"] || [])
end
@doc """
Return true if `key` is required in the definition `df`, else return false.
"""
@spec required?(definition(), any()) :: boolean()
def required?(df, key) do
key in df["required"]
end
@doc false
def property_type(%{"const" => _}, _roots), do: :const
def property_type(%{"$ref" => "#/definitions/" <> name}, roots) do
if class_name?(name) do
if name in roots do
:has_one
else
:embeds_one
end
else
:value
end
end
def property_type(%{"items" => %{"$ref" => "#/definitions/" <> name}}, roots) do
if class_name?(name) do
if name in roots do
:has_many
else
:embeds_many
end
else
:array
end
end
def property_type(%{"items" => %{"enum" => _}}, _roots), do: :array
def property_type(%{"enum" => _}, _roots), do: :enum
def property_type(_, _roots), do: :value
@doc """
Return all properties of `df` that are references to other resource (i.e. are not simple value
types).
"""
@spec refs(definition()) :: MapSet.t()
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)
|> Enum.filter(&class_name?/1)
|> MapSet.new()
end
@doc """
Return true if the resource type name matches the convention for resource type names (i.e. they
start with a capital letter).
"""
@spec class_name?(binary()) :: boolean()
def class_name?(name) do
Regex.match?(~r/^[A-Z]/, name)
end
@doc false
def element?({_name, %{"$ref" => "#/definitions/Element"}}), do: true
def element?({name, %{"items" => items}}), do: element?({name, items})
def element?(_), do: false
@doc """
Convert from a definition reference in the FHIR spec (i.e. a string like
`"#/definitions/ResourceName"`) to the corresponding Elixir-style module name (i.e.
`ResourceName`) as a string.
"""
@spec ref_to_class_name(String.t()) :: String.t()
def ref_to_class_name("#/definitions/" <> name),
do: class_name(name)
@doc """
Convert from a FHIR spec resource type name ("Encounter_patient") to the corresponding
Elixir-style module name as a string ("Encounter.Patient").
"""
@spec class_name(String.t()) :: String.t()
def class_name(name),
do: name |> String.split("_") |> Enum.map_join(".", &Recase.to_pascal/1)
end