chore: Add documentation

This commit is contained in:
Robert Prehn 2024-04-12 10:16:58 -05:00
parent c7c6568e37
commit 9e9c45aee1
No known key found for this signature in database
19 changed files with 563 additions and 62 deletions

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Robert Prehn, Mythic Insight Cooperative Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

209
README.md
View file

@ -1,11 +1,10 @@
# Kindling Kindling is a library for working with [HL7 FHIR](https://hl7.org/fhir/) APIs. It can generate
each FHIR resource as an `Ecto.Schema`. It also contains a client for working with the FHIR REST
**TODO: Add description** API.
## Installation ## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed Kindling can be installed by adding `kindling` to your list of dependencies in `mix.exs`:
by adding `kindling` to your list of dependencies in `mix.exs`:
```elixir ```elixir
def deps do def deps do
@ -15,7 +14,201 @@ def deps do
end end
``` ```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) You should then configure your root resources, which are the FHIR resources that your application
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can uses:
be found at <https://hexdocs.pm/kindling>.
```elixir
config :kindling, root_resources: ["Bundle", "Patient", "Encounter"]
```
When you generate resource schemas, Kindling will generate these schemas, plus any that they reference (recursively).
## Generating Resource Schemas
[mix kindling.generate_schemas](`Mix.Tasks.Kindling.GenerateSchemas`) will generate Elixir source files for the resource schemas under a namespace module under the `lib/` directory. It takes two arguments: the name of the namespace module, and a FHIR version.
Example:
```sh
mix kindling.generate_schemas FHIR R4
```
## Example Schema Module
The generated schema modules are normal `Ecto.Schema`s. Here's an example of a Patient resource schema:
```elixir
defmodule FHIR.R4.Patient do
use Ecto.Schema
import Ecto.Changeset
@fields [
:active,
:multiple_birth_boolean,
:language,
:implicit_rules,
:birth_date,
:multiple_birth_integer,
:id,
:deceased_boolean,
:gender,
:deceased_date_time
]
@required_fields []
@primary_key {:id, :binary_id, autogenerate: true}
schema "patient" do
# Constants
field(:resource_type, :string, virtual: true, default: "Patient")
# Fields
field(:active, :boolean)
field(:multiple_birth_boolean, :boolean)
field(:language, :string)
field(:implicit_rules, :string)
field(:birth_date, :date)
field(:multiple_birth_integer, :integer)
field(:deceased_boolean, :boolean)
field(:deceased_date_time, :string)
# Enum
field(:gender, Ecto.Enum, values: [:male, :female, :other, :unknown])
# Embed One
embeds_one(:marital_status, FHIR.R4.CodeableConcept)
embeds_one(:managing_organization, FHIR.R4.Reference)
embeds_one(:text, FHIR.R4.Narrative)
embeds_one(:meta, FHIR.R4.Meta)
# Embed Many
embeds_many(:photo, FHIR.R4.Attachment)
embeds_many(:communication, FHIR.R4.Patient.Communication)
embeds_many(:name, FHIR.R4.HumanName)
embeds_many(:extension, FHIR.R4.Extension)
embeds_many(:telecom, FHIR.R4.ContactPoint)
embeds_many(:contained, FHIR.R4.ResourceList)
embeds_many(:link, FHIR.R4.Patient.Link)
embeds_many(:contact, FHIR.R4.Patient.Contact)
embeds_many(:modifier_extension, FHIR.R4.Extension)
embeds_many(:identifier, FHIR.R4.Identifier)
embeds_many(:general_practitioner, FHIR.R4.Reference)
embeds_many(:address, FHIR.R4.Address)
end
def version, do: FHIR.R4
def path, do: "/Patient"
def base_changeset(data \\ %__MODULE__{}, attrs) do
data
|> cast(attrs, @fields)
|> validate_required(@required_fields)
end
end
```
## API Client
Kindling also includes a [FHIR REST API client](`Kindling.Client`) that can be used to request resources
from a FHIR server. The client will automatically convert the results to resource schema structs:
```elixir
# Use the public FHIR test server
client = %{
base_url: "http://hapi.fhir.org/baseR4",
auth_mode: :open
}
patient_id = "593166"
Kindling.Client.read(client, FHIR.R4.Patient, patient_id)
```
Results in a struct something like this:
```elixir
%FHIR.R4.Patient{
__meta__: #Ecto.Schema.Metadata<:built, "patient">,
id: "593166",
resource_type: "Patient",
active: nil,
multiple_birth_boolean: nil,
language: nil,
implicit_rules: nil,
birth_date: ~D[2000-10-31],
multiple_birth_integer: nil,
deceased_boolean: nil,
deceased_date_time: nil,
gender: :female,
marital_status: nil,
managing_organization: nil,
text: %FHIR.R4.Narrative{
id: nil,
div: "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Sabrina <b>SPELLMAN </b></div><table class=\"hapiPropertyTable\"><tbody/></table></div>",
status: :generated,
extension: []
},
meta: %FHIR.R4.Meta{
id: nil,
last_updated: {:error, :invalid_format},
source: "#NPQrzINFNuDwuDgM",
version_id: "1",
profile: nil,
extension: [],
security: [],
tag: []
},
photo: [],
communication: [],
name: [
%FHIR.R4.HumanName{
id: nil,
family: "Spellman",
text: nil,
given: ["Sabrina"],
prefix: nil,
suffix: nil,
use: nil,
period: nil,
extension: []
}
],
extension: [],
telecom: [
%FHIR.R4.ContactPoint{
id: nil,
rank: nil,
value: "1(845)443-7666",
system: :phone,
use: :home,
period: nil,
extension: []
}
],
contained: [],
link: [],
contact: [],
modifier_extension: [],
identifier: [],
general_practitioner: [],
address: [
%FHIR.R4.Address{
id: nil,
city: "Greendale",
country: "United States",
district: nil,
postal_code: "11199",
state: "New York",
text: nil,
line: ["1138 Decario Lane"],
type: nil,
use: nil,
period: nil,
extension: []
}
]
}
```
## Additional Resources
The docs can be found at <https://hexdocs.pm/kindling>.

View file

@ -1,18 +1,4 @@
defmodule Kindling do defmodule Kindling do
@moduledoc """ @external_resource "README.md"
Documentation for `Kindling`. @moduledoc File.read!("README.md")
"""
@doc """
Hello world.
## Examples
iex> Kindling.hello()
:world
"""
def hello do
:world
end
end end

View file

@ -1,9 +1,44 @@
defmodule Kindling.Client do defmodule Kindling.Client do
@moduledoc """
FHIR API client.
"""
alias Kindling.Converter alias Kindling.Converter
defstruct [:base_url, :access_token, auth_mode: :bearer] defstruct [:base_url, :access_token, auth_mode: :bearer]
def read(client, resource_module, id, opts \\ []) do @typedoc """
Configuration of a FHIR client.
"""
@type t :: %{
base_url: fhir_server_base_url(),
access_token: access_token(),
auth_mode: :bearer | :basic | :open
}
@typedoc """
The base URL of the FHIR server. Often (but not always), includes the FHIR version in the URL path.
"""
@type fhir_server_base_url :: String.t()
@typedoc """
The FHIR access token. Used when :auth_mode is :bearer or :basic. Ignored when :auth_mode is :open.
"""
@type access_token :: String.t()
@typedoc """
A string that is the ID of a FHIR resource.
"""
@type resource_id :: String.t()
@doc """
Make a read REST request against a FHIR API server, for a resource of type `resource_module`
and the id `id`. Returns a schema struct of the results, or an error.
`opts`:
- `headers`: additional HTTP request headers to send with the request, as a list of {key, value}
pairs.
"""
@spec read(t(), Kindling.Schema.Resource.t(), resource_id()) ::
{:ok, Kindling.Schema.Resource.schema(), Keyword.t()} | term()
def read(client, resource_module, id, opts \\ [], req_fn \\ &Kindling.Client.Req.get/2) do
base_uri = URI.parse(client.base_url) base_uri = URI.parse(client.base_url)
uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_path("/#{id}") uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_path("/#{id}")
more_headers = Keyword.get(opts, :headers, []) more_headers = Keyword.get(opts, :headers, [])
@ -11,17 +46,35 @@ defmodule Kindling.Client do
headers = headers(client, more_headers) headers = headers(client, more_headers)
uri uri
|> Req.get(headers: headers) |> req_fn.(headers: headers)
|> case do |> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 -> {:ok, %{status: status} = response} when status >= 200 and status < 300 ->
Converter.convert(resource_module.version(), response.body) Converter.convert(resource_module.version_namespace(), response.body)
other -> other ->
other other
end end
end end
def search(client, resource_module, params \\ [], opts \\ []) do @doc """
Make a search REST request against a FHIR API server, for a resource of type `resource_module`
using the search params `params`.
Returns a schema struct of the results (usually a FHIR bundle), or an error.
`opts`:
- `headers`: additional HTTP request headers to send with the request, as a list of {key, value}
pairs.
"""
@spec search(t(), Kindling.Schema.Resource.t(), Keyword.t(), Keyword.t()) ::
{:ok, Kindling.Schema.Resource.schema()} | term()
def search(
client,
resource_module,
params \\ [],
opts \\ [],
req_fn \\ &Kindling.Client.Req.get/2
) do
base_uri = URI.parse(client.base_url) base_uri = URI.parse(client.base_url)
query = URI.encode_query(params) query = URI.encode_query(params)
uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_query(query) uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_query(query)
@ -30,16 +83,17 @@ defmodule Kindling.Client do
headers = headers(client, more_headers) headers = headers(client, more_headers)
uri uri
|> Req.get(headers: headers) |> req_fn.(headers: headers)
|> case do |> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 -> {:ok, %{status: status} = response} when status >= 200 and status < 300 ->
Converter.convert(resource_module.version(), response.body) Converter.convert(resource_module.version_namespace(), response.body)
other -> other ->
other other
end end
end end
@doc false
def headers(client, more_headers) do def headers(client, more_headers) do
case client.auth_mode do case client.auth_mode do
:open -> :open ->
@ -50,12 +104,14 @@ defmodule Kindling.Client do
end end
end end
@doc false
def auth_header(%{auth_mode: :bearer, access_token: token}), def auth_header(%{auth_mode: :bearer, access_token: token}),
do: {"Authorization", "Bearer #{token}"} do: {"Authorization", "Bearer #{token}"}
def auth_header(%{auth_mode: :basic, access_token: token}), def auth_header(%{auth_mode: :basic, access_token: token}),
do: {"Authorization", "Basic #{token}"} do: {"Authorization", "Basic #{token}"}
@doc false
def format_header do def format_header do
{"Accept", "application/json"} {"Accept", "application/json"}
end end

View file

@ -0,0 +1,5 @@
defmodule Kindling.Client.Req do
def get(request, opts \\ []) do
Req.get(request, opts)
end
end

View file

@ -1,4 +1,12 @@
defmodule Kindling.Config do defmodule Kindling.Config do
@moduledoc """
Configuration for Kindling.
"""
@doc """
Returns the list of configured root resources.
"""
@spec root_resources() :: [String.t()]
def root_resources, def root_resources,
do: Application.get_env(:kindling, :root_resources, ["Bundle", "Patient", "Encounter"]) do: Application.get_env(:kindling, :root_resources, ["Bundle", "Patient", "Encounter"])
end end

View file

@ -1,6 +1,16 @@
defmodule Kindling.Converter do defmodule Kindling.Converter do
@moduledoc """
Convert between JSON-style maps and Elixir structs, using the generated resource Ecto schemas as
a guide.
"""
alias Kindling.Schema.Resource alias Kindling.Schema.Resource
@doc """
Convert a JSON-style map with string keys to a FHIR resource schema. `version_namespace` is
the module namespace where your resource schema module have been generated (e.g. `FHIR.R4`)
and `resource_json` is the map of data.
"""
@spec convert(atom() | binary(), map()) :: Kindling.Schema.Resource.schema()
def convert(version_namespace, %{"resourceType" => resource_type} = resource_json) do def convert(version_namespace, %{"resourceType" => resource_type} = resource_json) do
resource_module = Module.concat(version_namespace, Resource.class_name(resource_type)) resource_module = Module.concat(version_namespace, Resource.class_name(resource_type))
resource_list_module = Module.concat(version_namespace, "ResourceList") resource_list_module = Module.concat(version_namespace, "ResourceList")
@ -8,6 +18,7 @@ defmodule Kindling.Converter do
structify(resource_module, resource_list_module, resource_json) structify(resource_module, resource_list_module, resource_json)
end end
@doc false
def structify( def structify(
resource_module, resource_module,
resource_list_module, resource_list_module,
@ -40,6 +51,7 @@ defmodule Kindling.Converter do
struct!(resource_module, atom_map) struct!(resource_module, atom_map)
end end
@doc false
def convert_field(resource_module, resource_list_module, field, value) do def convert_field(resource_module, resource_list_module, field, value) do
cond do cond do
field in resource_module.__schema__(:associations) -> field in resource_module.__schema__(:associations) ->
@ -56,6 +68,7 @@ defmodule Kindling.Converter do
end end
end end
@doc false
def cast_field(resource_module, field, value) do def cast_field(resource_module, field, value) do
type = resource_module.__schema__(:type, field) type = resource_module.__schema__(:type, field)
@ -82,6 +95,7 @@ defmodule Kindling.Converter do
end end
end end
@doc false
def convert_association(resource_module, resource_list_module, field, value) do def convert_association(resource_module, resource_list_module, field, value) do
%{cardinality: cardinality, related: type} = resource_module.__schema__(:association, field) %{cardinality: cardinality, related: type} = resource_module.__schema__(:association, field)
@ -94,6 +108,7 @@ defmodule Kindling.Converter do
end end
end end
@doc false
def convert_embed(resource_module, resource_list_module, field, value) do def convert_embed(resource_module, resource_list_module, field, value) do
%{cardinality: cardinality, related: type} = resource_module.__schema__(:embed, field) %{cardinality: cardinality, related: type} = resource_module.__schema__(:embed, field)

View file

@ -1,4 +1,10 @@
defmodule Kindling.Converter.DateTime do defmodule Kindling.Converter.DateTime do
@moduledoc """
Handles dates and datetimes in FHIR's format. Dates may be in ISO8601 format, or may be in a truncated
form that indicates only certain parts of a date (e.g. year, or year+month, etc). DateTimes work
similarly.
"""
defstruct year: "", defstruct year: "",
month: "", month: "",
day: "", day: "",
@ -7,6 +13,18 @@ defmodule Kindling.Converter.DateTime do
second: "", second: "",
zone: "" zone: ""
@doc """
Parse a string containing a FHIR-style datetime. Return `{:ok, DateTime.t(), integer()}` or a parse
error.
"""
@spec parse(binary()) ::
{:error,
:incompatible_calendars
| :invalid_date
| :invalid_format
| :invalid_time
| :missing_offset}
| {:ok, DateTime.t(), integer()}
def parse(value) do def parse(value) do
value value
|> String.codepoints() |> String.codepoints()
@ -16,6 +34,7 @@ defmodule Kindling.Converter.DateTime do
|> DateTime.from_iso8601() |> DateTime.from_iso8601()
end end
@doc false
def to_iso_string(%{ def to_iso_string(%{
year: year, year: year,
month: month, month: month,
@ -28,6 +47,7 @@ defmodule Kindling.Converter.DateTime do
"#{year}-#{month}-#{day}T#{hour}:#{minute}:#{second}#{zone}" "#{year}-#{month}-#{day}T#{hour}:#{minute}:#{second}#{zone}"
end end
@doc false
def set_defaults(%{ def set_defaults(%{
year: year, year: year,
month: month, month: month,
@ -51,36 +71,43 @@ defmodule Kindling.Converter.DateTime do
defp format("", default), do: default defp format("", default), do: default
defp format(string, default), do: String.pad_leading(string, String.length(default), "0") defp format(string, default), do: String.pad_leading(string, String.length(default), "0")
@doc false
def do_year([], data), do: data def do_year([], data), do: data
def do_year(["-" | tail], data), do: do_month(tail, data) def do_year(["-" | tail], data), do: do_month(tail, data)
def do_year([hd | tail], %{year: year} = data), do: do_year(tail, %{data | year: year <> hd}) def do_year([hd | tail], %{year: year} = data), do: do_year(tail, %{data | year: year <> hd})
@doc false
def do_month([], data), do: data def do_month([], data), do: data
def do_month(["-" | tail], data), do: do_day(tail, data) def do_month(["-" | tail], data), do: do_day(tail, data)
def do_month([hd | tail], %{month: month} = data), def do_month([hd | tail], %{month: month} = data),
do: do_month(tail, %{data | month: month <> hd}) do: do_month(tail, %{data | month: month <> hd})
@doc false
def do_day([], data), do: data def do_day([], data), do: data
def do_day(["T" | tail], data), do: do_hour(tail, data) def do_day(["T" | tail], data), do: do_hour(tail, data)
def do_day([hd | tail], %{day: day} = data), do: do_day(tail, %{data | day: day <> hd}) def do_day([hd | tail], %{day: day} = data), do: do_day(tail, %{data | day: day <> hd})
@doc false
def do_hour([], data), do: data def do_hour([], data), do: data
def do_hour([":" | tail], data), do: do_minute(tail, data) def do_hour([":" | tail], data), do: do_minute(tail, data)
def do_hour([hd | tail], %{hour: hour} = data), do: do_hour(tail, %{data | hour: hour <> hd}) def do_hour([hd | tail], %{hour: hour} = data), do: do_hour(tail, %{data | hour: hour <> hd})
@doc false
def do_minute([], data), do: data def do_minute([], data), do: data
def do_minute([":" | tail], data), do: do_second(tail, data) def do_minute([":" | tail], data), do: do_second(tail, data)
def do_minute([hd | tail], %{minute: minute} = data), def do_minute([hd | tail], %{minute: minute} = data),
do: do_minute(tail, %{data | minute: minute <> hd}) do: do_minute(tail, %{data | minute: minute <> hd})
@doc false
def do_second([], data), do: data def do_second([], data), do: data
def do_second([c | tail], data) when c in ["+", "-", "Z"], do: do_zone(tail, data) def do_second([c | tail], data) when c in ["+", "-", "Z"], do: do_zone(tail, data)
def do_second([hd | tail], %{second: second} = data), def do_second([hd | tail], %{second: second} = data),
do: do_second(tail, %{data | second: second <> hd}) do: do_second(tail, %{data | second: second <> hd})
@doc false
def do_zone([], data), do: data def do_zone([], data), do: data
def do_zone([hd | tail], %{zone: zone} = data), do: do_zone(tail, %{data | zone: zone <> hd}) def do_zone([hd | tail], %{zone: zone} = data), do: do_zone(tail, %{data | zone: zone <> hd})
end end

View file

@ -19,7 +19,7 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
<%= for {name, df, _} <- @properties.array do %>field :<%= Recase.to_snake(name) %>, <%= inspect(fhir_type_to_ecto(df)) %> <%= for {name, df, _} <- @properties.array do %>field :<%= Recase.to_snake(name) %>, <%= inspect(fhir_type_to_ecto(df)) %>
<% end %> <% 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: <%= enum_value_string(df) %>
<% end %> <% end %>
<%= if @properties.embeds_one != [] do %># Embed One<% end %> <%= if @properties.embeds_one != [] do %># Embed One<% end %>
<%= for {name, df, _} <- @properties.embeds_one do %>embeds_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %> <%= for {name, df, _} <- @properties.embeds_one do %>embeds_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
@ -35,6 +35,9 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
<% end %> <% end %>
end end
def version_namespace, do: <%= @namespace %>.<%= @version %>
def version, do: "<%= @version %>"
def base_changeset(data \\ %__MODULE__{}, attrs) do def base_changeset(data \\ %__MODULE__{}, attrs) do
data data
|> cast(attrs, @fields) |> cast(attrs, @fields)

View file

@ -1,8 +1,22 @@
defmodule Kindling.Schema do defmodule Kindling.Schema do
alias Kindling.Version @moduledoc """
alias Kindling.Schema.Resource Tools for working with the JSON schema for FHIR.
"""
def schema_object(version) do alias Kindling.Schema.Resource
alias Kindling.Version
@typedoc """
A string representing a particular FHIR version ("R5" | "R4B" | "R4" | "R3" ).
"""
@type version_string :: String.t()
@doc """
Load and process a JSON schema file for the given `version`, return the map defining the spec
for that version.
"""
@spec schema_map(version_string()) :: map()
def schema_map(version) do
filename = Path.join(Version.version_dir(version), "fhir.schema.json") filename = Path.join(Version.version_dir(version), "fhir.schema.json")
filename filename
@ -11,6 +25,13 @@ defmodule Kindling.Schema do
|> build_backlinks() |> build_backlinks()
end end
def all_resources(schema) do
schema
|> get_in(["discriminator", "mapping"])
|> Map.keys()
end
@doc false
def build_backlinks(schema) do def build_backlinks(schema) do
new_defs = new_defs =
schema["definitions"] schema["definitions"]
@ -36,6 +57,13 @@ defmodule Kindling.Schema do
Map.put(schema, "definitions", defs) Map.put(schema, "definitions", defs)
end end
@doc """
Given a schema map in the style returned by `&schema_map/1`, and a root resource name (e.g.
`"Encounter"`), return a `MapSet` of resources that are referenced (recursively) from the
resource. This can be used to determine the set of resource schemas that need to be generated
to full support that resource as a fully-defined set or schemas.
"""
@spec refs_recursive(map(), String.t()) :: MapSet.t()
def refs_recursive(schema, root_name) when is_binary(root_name) do def refs_recursive(schema, root_name) when is_binary(root_name) do
do_recurse(schema, MapSet.new([root_name]), [root_name]) do_recurse(schema, MapSet.new([root_name]), [root_name])
end end

View file

@ -1,32 +1,68 @@
defmodule Kindling.Schema.Resource do defmodule Kindling.Schema.Resource do
@empty_properties %{ @moduledoc """
array: [], Functions for working on entries of the `definitions` section of the FHIR schema, which define
const: [], the Resources available in FHIR.
embeds_one: [], """
embeds_many: [],
has_one: [],
has_many: [],
enum: [],
value: []
}
@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 def properties(df) do
df["properties"] df["properties"]
end 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 def grouped_properties(df, roots) do
properties = properties =
(df["properties"] || %{}) (df["properties"] || %{})
|> Map.delete("id") |> Map.delete("id")
|> Enum.reject(&is_element/1) |> Enum.reject(&element?/1)
|> Enum.map(fn {key, value} -> {key, value, property_type(value, roots)} end) |> Enum.map(fn {key, value} -> {key, value, property_type(value, roots)} end)
|> Enum.group_by(fn {_key, _value, type} -> |> Enum.group_by(fn {_key, _value, type} ->
type type
end) end)
Map.merge(@empty_properties, properties) Map.merge(%__MODULE__{}, properties)
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.
"""
@spec all_fields(definition()) :: [String.t()]
def all_fields(df) do def all_fields(df) do
(df["properties"] || %{}) (df["properties"] || %{})
|> Enum.filter(fn {_name, definition} -> |> Enum.filter(fn {_name, definition} ->
@ -35,6 +71,12 @@ defmodule Kindling.Schema.Resource do
|> Enum.map(fn {name, _} -> name end) |> Enum.map(fn {name, _} -> name end)
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 def required_fields(df) do
df df
|> required() |> required()
@ -44,18 +86,27 @@ defmodule Kindling.Schema.Resource do
|> Enum.map(fn {name, _} -> name end) |> Enum.map(fn {name, _} -> name end)
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 def required(df) do
Map.take(df["properties"] || %{}, df["required"] || []) Map.take(df["properties"] || %{}, df["required"] || [])
end 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 def required?(df, key) do
key in df["required"] key in df["required"]
end end
@doc false
def property_type(%{"const" => _}, _roots), do: :const def property_type(%{"const" => _}, _roots), do: :const
def property_type(%{"$ref" => "#/definitions/" <> name}, roots) do def property_type(%{"$ref" => "#/definitions/" <> name}, roots) do
if is_class_name(name) do if class_name?(name) do
if name in roots do if name in roots do
:has_one :has_one
else else
@ -67,7 +118,7 @@ defmodule Kindling.Schema.Resource do
end end
def property_type(%{"items" => %{"$ref" => "#/definitions/" <> name}}, roots) do def property_type(%{"items" => %{"$ref" => "#/definitions/" <> name}}, roots) do
if is_class_name(name) do if class_name?(name) do
if name in roots do if name in roots do
:has_many :has_many
else else
@ -82,6 +133,11 @@ defmodule Kindling.Schema.Resource do
def property_type(%{"enum" => _}, _roots), do: :enum def property_type(%{"enum" => _}, _roots), do: :enum
def property_type(_, _roots), do: :value 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 def refs(df) do
(df["properties"] || %{}) (df["properties"] || %{})
|> Enum.map(fn |> Enum.map(fn
@ -95,21 +151,38 @@ defmodule Kindling.Schema.Resource do
nil nil
end) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
|> Enum.filter(&is_class_name/1) |> Enum.filter(&class_name?/1)
|> MapSet.new() |> MapSet.new()
end end
def is_class_name(name) do @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) Regex.match?(~r/^[A-Z]/, name)
end end
def is_element({_name, %{"$ref" => "#/definitions/Element"}}), do: true @doc false
def is_element({name, %{"items" => items}}), do: is_element({name, items}) def element?({_name, %{"$ref" => "#/definitions/Element"}}), do: true
def is_element(_), do: false 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), def ref_to_class_name("#/definitions/" <> name),
do: class_name(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), def class_name(name),
do: name |> String.split("_") |> Enum.map(&Recase.to_pascal/1) |> Enum.join(".") do: name |> String.split("_") |> Enum.map_join(".", &Recase.to_pascal/1)
end end

View file

@ -1,10 +1,26 @@
defmodule Kindling.SchemaDownloader do defmodule Kindling.SchemaDownloader do
@moduledoc """
Handles downloading and unzipping FHIR JSON schemas from the hl7 server.
"""
@version_aliases %{
"R3" => "STU3"
}
@doc """
Download and unzip the JSON schema file for the given `version` into the _build directory for
the library to reference.
"""
@spec download_version(Kindling.Schema.version_string()) :: :ok | {:error, any()}
def download_version(version) do def download_version(version) do
{:ok, _} = Application.ensure_all_started(:req)
version_alias = Map.get(@version_aliases, version, version)
version_dir = "#{Mix.Project.build_path(Mix.Project.config())}/fhir/#{version}" version_dir = "#{Mix.Project.build_path(Mix.Project.config())}/fhir/#{version}"
filename = "#{version_dir}/fhir.schema.json.zip" filename = "#{version_dir}/fhir.schema.json.zip"
File.mkdir_p!(version_dir) File.mkdir_p!(version_dir)
"http://hl7.org/fhir/#{version}/fhir.schema.json.zip" "http://hl7.org/fhir/#{version_alias}/fhir.schema.json.zip"
|> Req.get!() |> Req.get!()
|> then(&File.write!(filename, &1.body)) |> then(&File.write!(filename, &1.body))
@ -13,7 +29,28 @@ defmodule Kindling.SchemaDownloader do
:ok :ok
other -> other ->
dbg(other)
other other
end end
end end
@doc """
Check if the `version` JSON schema is downloaded and unzipped for use. If not, download and unzip
it. If that download & unzip fails, raise an error.
"""
def ensure_version!(version) do
version_dir = "#{Mix.Project.build_path(Mix.Project.config())}/fhir/#{version}"
filename = "#{version_dir}/fhir.schema.json"
cond do
File.exists?(filename) ->
:ok
download_version(version) == :ok ->
:ok
true ->
raise "Could not download and unzip the FHIR schema for version #{version}."
end
end
end end

View file

@ -20,7 +20,7 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
<%= for {name, df, _} <- @properties.array do %>field :<%= Recase.to_snake(name) %>, <%= inspect(fhir_type_to_ecto(df)) %> <%= for {name, df, _} <- @properties.array do %>field :<%= Recase.to_snake(name) %>, <%= inspect(fhir_type_to_ecto(df)) %>
<% end %> <% 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: <%= enum_value_string(df) %>
<% end %> <% end %>
<%= if @properties.embeds_one != [] do %># Embed One<% end %> <%= if @properties.embeds_one != [] do %># Embed One<% end %>
<%= for {name, df, _} <- @properties.embeds_one do %>embeds_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %> <%= for {name, df, _} <- @properties.embeds_one do %>embeds_one :<%= Recase.to_snake(name) %>, <%= @namespace %>.<%= @version %>.<%= ref_to_class_name(df["$ref"]) %>
@ -39,7 +39,8 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
<% end %> <% end %>
end end
def version, do: <%= @namespace %>.<%= @version %> def version_namespace, do: <%= @namespace %>.<%= @version %>
def version, do: "<%= @version %>"
def path, do: "/<%= @resource_name %>" def path, do: "/<%= @resource_name %>"
def base_changeset(data \\ %__MODULE__{}, attrs) do def base_changeset(data \\ %__MODULE__{}, attrs) do

View file

@ -1,4 +1,8 @@
defmodule Kindling.Templates do defmodule Kindling.Templates do
@moduledoc """
Template functions for generating FHIR Resource modules.
"""
require EEx require EEx
import Kindling.Templates.Functions import Kindling.Templates.Functions

View file

@ -1,11 +1,28 @@
defmodule Kindling.Templates.Functions do defmodule Kindling.Templates.Functions do
@moduledoc """
Helper functions used the in generator templates.
"""
@int_patterns [ @int_patterns [
"^[1-9][0-9]*$", "^[1-9][0-9]*$",
"^[0]|([1-9][0-9]*)$", "^[0]|([1-9][0-9]*)$",
"^-?([0]|([1-9][0-9]*))$" "^-?([0]|([1-9][0-9]*))$",
"^[0]|[-+]?[1-9][0-9]*$"
] ]
@decimal_patterns ["^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$"] @decimal_patterns [
"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$",
"^-?(0|[1-9][0-9]{0,17})(\\.[0-9]{1,17})?([eE][+-]?[0-9]{1,9}})?$"
]
def enum_value_string(df) do
df["enum"]
|> Enum.map(&Recase.to_snake(&1))
|> Enum.map(&String.to_atom/1)
|> Enum.uniq()
|> Enum.map_join(",\n", &inspect/1)
|> then(&"[\n#{&1}\n]")
end
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" => "boolean"}), do: :boolean
@ -30,6 +47,7 @@ defmodule Kindling.Templates.Functions do
def fhir_type_to_ecto(%{"$ref" => "#/definitions/xhtml"}), 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/markdown"}), do: :string
def fhir_type_to_ecto(%{"$ref" => "#/definitions/base64Binary"}), do: :string def fhir_type_to_ecto(%{"$ref" => "#/definitions/base64Binary"}), do: :string
def fhir_type_to_ecto(%{"$ref" => "#/definitions/integer64"}), do: :string
def fhir_type_to_ecto(%{"$ref" => "#/definitions/date"}), do: :date def fhir_type_to_ecto(%{"$ref" => "#/definitions/date"}), do: :date
def fhir_type_to_ecto(%{"$ref" => "#/definitions/dateTime"}), do: :utc_datetime_usec 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/instant"}), do: :utc_datetime_usec

View file

@ -1,4 +1,8 @@
defmodule Kindling.Version do defmodule Kindling.Version do
@moduledoc """
Functions for working with FHIR versions e.g. R3, R4, R5, STU3, etc.
"""
def version_dir(version), def version_dir(version),
do: Path.join([Mix.Project.build_path(Mix.Project.config()), "fhir", version]) do: Path.join([Mix.Project.build_path(Mix.Project.config()), "fhir", version])
end end

View file

@ -14,7 +14,9 @@ defmodule Mix.Tasks.Kindling.GenerateSchemas do
[namespace, version] = args [namespace, version] = args
schema = Kindling.Schema.schema_object(version) Kindling.SchemaDownloader.ensure_version!(version)
schema = Kindling.Schema.schema_map(version)
roots = Kindling.Config.root_resources() roots = Kindling.Config.root_resources()
to_generate = to_generate =

27
mix.exs
View file

@ -4,13 +4,34 @@ defmodule Kindling.MixProject do
def project do def project do
[ [
app: :kindling, app: :kindling,
version: "0.1.0", version: version(),
elixir: "~> 1.15", elixir: "~> 1.15",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps() deps: deps(),
# Docs
source_url: "https://gitlab.com/mythic-insight/kindling",
docs: [
main: "Kindling"
],
# Hex
licenses: ["MIT"],
links: %{
"source" => "https://gitlab.com/mythic-insight/kindling"
}
] ]
end end
def version do
[_, version] =
"README.md"
|> File.read!()
|> then(&Regex.run(~r/{:kindling, "~> ([^"]+)"}/, &1))
version
end
# Run "mix help compile.app" to learn about applications. # Run "mix help compile.app" to learn about applications.
def application do def application do
[ [
@ -21,7 +42,9 @@ defmodule Kindling.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:credo, "~> 1.7", only: [:dev]},
{:ecto, "~> 3.11", only: [:dev, :test]}, {:ecto, "~> 3.11", only: [:dev, :test]},
{:ex_doc, "~> 0.31.2", only: [:dev]},
{:jason, "~> 1.4"}, {:jason, "~> 1.4"},
{:recase, "~> 0.7.0"}, {:recase, "~> 0.7.0"},
{:req, "~> 0.4.11"} {:req, "~> 0.4.11"}

View file

@ -1,14 +1,23 @@
%{ %{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"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"}, "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"},
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"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"}, "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"}, "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"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_ownership": {:hex, :nimble_ownership, "0.2.1", "3e44c72ebe8dd213db4e13aff4090aaa331d158e72ce1891d02e0ffb05a1eb2d", [:mix], [], "hexpm", "bf38d2ef4fb990521a4ecf112843063c1f58a5c602484af4c7977324042badee"}, "nimble_ownership": {:hex, :nimble_ownership, "0.2.1", "3e44c72ebe8dd213db4e13aff4090aaa331d158e72ce1891d02e0ffb05a1eb2d", [:mix], [], "hexpm", "bf38d2ef4fb990521a4ecf112843063c1f58a5c602484af4c7977324042badee"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, "recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"req": {:hex, :req, "0.4.11", "cb19f87d5251e7de30cfc67d1899696b290711092207c6b2e8fc2294f237fcdc", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbf4f2393c649fa4146a3b8470e2a7e8c9b23e4100a16c75f5e7d1d3d33144f3"}, "req": {:hex, :req, "0.4.11", "cb19f87d5251e7de30cfc67d1899696b290711092207c6b2e8fc2294f237fcdc", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbf4f2393c649fa4146a3b8470e2a7e8c9b23e4100a16c75f5e7d1d3d33144f3"},