feat: Start XML support

This commit is contained in:
Robert Prehn 2024-04-25 10:55:26 -05:00
parent 9e9c45aee1
commit 4fc2b745e4
No known key found for this signature in database
10 changed files with 195 additions and 15 deletions

View file

@ -49,7 +49,10 @@ defmodule Kindling.Client do
|> req_fn.(headers: headers)
|> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 ->
Converter.convert(resource_module.version_namespace(), response.body)
{:ok, Converter.convert(resource_module.version_namespace(), response.body)}
{:ok, %{body: %{"resourceType" => _}} = response} ->
{:error, Converter.convert(resource_module.version_namespace(), response.body)}
other ->
other
@ -86,13 +89,86 @@ defmodule Kindling.Client do
|> req_fn.(headers: headers)
|> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 ->
Converter.convert(resource_module.version_namespace(), response.body)
{:ok, Converter.convert(resource_module.version_namespace(), response.body)}
{:ok, %{body: %{"resourceType" => _}} = response} ->
{:error, Converter.convert(resource_module.version_namespace(), response.body)}
other ->
other
end
end
def create(client, resource_module, attrs, opts \\ [], req_fn \\ &Kindling.Client.Req.post/2) do
base_uri = URI.parse(client.base_url)
uri = base_uri |> URI.append_path(resource_module.path())
more_headers = Keyword.get(opts, :headers, [])
headers = headers(client, more_headers)
uri
|> req_fn.(headers: headers, json: attrs)
|> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 ->
{:ok, Converter.convert(resource_module.version_namespace(), response.body)}
{:ok, %{body: %{"resourceType" => _}} = response} ->
{:error, Converter.convert(resource_module.version_namespace(), response.body)}
other ->
other
end
end
def update(client, resource_module, id, attrs, opts \\ [], req_fn \\ &Kindling.Client.Req.put/2) do
base_uri = URI.parse(client.base_url)
uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_path("/#{id}")
more_headers = Keyword.get(opts, :headers, [])
headers = headers(client, more_headers)
uri
|> req_fn.(headers: headers, json: attrs)
|> case do
{:ok, %{status: status} = response} when status >= 200 and status < 300 ->
{:ok, Converter.convert(resource_module.version_namespace(), response.body)}
{:ok, %{body: %{"resourceType" => _}} = response} ->
{:error, Converter.convert(resource_module.version_namespace(), response.body)}
other ->
other
end
end
def delete(client, resource_module, id, opts \\ [], req_fn \\ &Kindling.Client.Req.delete/2) do
base_uri = URI.parse(client.base_url)
uri = base_uri |> URI.append_path(resource_module.path()) |> URI.append_path("/#{id}")
more_headers = Keyword.get(opts, :headers, [])
headers = headers(client, more_headers)
uri
|> req_fn.(headers: headers)
|> convert_response(resource_module)
end
def convert_response({:ok, %{status: status}}, _resource_module)
when status == 204 do
:ok
end
def convert_response({:ok, %{status: status} = response}, resource_module)
when status >= 200 and status < 300 do
{:ok, Converter.convert(resource_module.version_namespace(), response.body)}
end
def convert_response({:ok, %{body: %{"resourceType" => _}} = response}, resource_module) do
{:error, Converter.convert(resource_module.version_namespace(), response.body)}
end
def convert_response(other, _), do: other
@doc false
def headers(client, more_headers) do
case client.auth_mode do

View file

@ -2,4 +2,16 @@ defmodule Kindling.Client.Req do
def get(request, opts \\ []) do
Req.get(request, opts)
end
def post(request, opts \\ []) do
Req.post(request, opts)
end
def put(request, opts \\ []) do
Req.put(request, opts)
end
def delete(request, opts \\ []) do
Req.delete(request, opts)
end
end

View file

@ -42,11 +42,24 @@ defmodule Kindling.Converter do
atom_map =
resource_json
|> Enum.map(fn {key_string, value} ->
|> map_or_nil(fn {key_string, value} ->
key_atom = key_string |> Recase.to_snake() |> String.to_existing_atom()
{key_atom, convert_field(resource_module, resource_list_module, key_atom, value)}
case value do
nil ->
{key_atom, nil}
value ->
{key_atom, convert_field(resource_module, resource_list_module, key_atom, value)}
end
end)
|> Map.new()
|> case do
nil ->
nil
enum ->
Map.new(enum)
end
struct!(resource_module, atom_map)
end
@ -85,6 +98,16 @@ defmodule Kindling.Converter do
end
end
defp do_cast_field({:array, _} = type, value) do
case Ecto.Type.cast(type, wrap(value)) do
{:ok, v} ->
v
other ->
other
end
end
defp do_cast_field(type, value) do
case Ecto.Type.cast(type, value) do
{:ok, v} ->
@ -101,7 +124,7 @@ defmodule Kindling.Converter do
case cardinality do
:many ->
Enum.map(value, &structify(type, resource_list_module, &1))
map_or_nil(wrap(value), &structify(type, resource_list_module, &1))
:one ->
structify(type, resource_list_module, value)
@ -114,10 +137,20 @@ defmodule Kindling.Converter do
case cardinality do
:many ->
Enum.map(value, &structify(type, resource_list_module, &1))
map_or_nil(wrap(value), &structify(type, resource_list_module, &1))
:one ->
structify(type, resource_list_module, value)
end
end
defp map_or_nil(nil, _), do: nil
defp map_or_nil(enum, mapper) do
Enum.map(enum, mapper)
end
defp wrap(binary) when is_binary(binary), do: [binary]
defp wrap(list) when is_list(list), do: list
defp wrap(other), do: [other]
end

View file

@ -13,6 +13,12 @@ defmodule Kindling.Converter.DateTime do
second: "",
zone: ""
def parse!(value) do
{:ok, v, 0} = parse(value)
v
end
@doc """
Parse a string containing a FHIR-style datetime. Return `{:ok, DateTime.t(), integer()}` or a parse
error.

View file

@ -38,13 +38,17 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
def version_namespace, do: <%= @namespace %>.<%= @version %>
def version, do: "<%= @version %>"
def base_changeset(data \\ %__MODULE__{}, attrs) do
def 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 %>
<%= for {name, _df, _} <- @properties.embeds_one do %>|> cast_embed(:<%= Recase.to_snake(name) %>)
<% end %>
<%= for {name, _df, _} <- @properties.embeds_many do %>|> cast_embed(:<%= Recase.to_snake(name) %>)
<% end %>
|> validate_required(@required_fields)
end
end

View file

@ -43,13 +43,17 @@ defmodule <%= @namespace %>.<%= @version %>.<%= class_name(@resource_name) %> do
def version, do: "<%= @version %>"
def path, do: "/<%= @resource_name %>"
def base_changeset(data \\ %__MODULE__{}, attrs) do
def 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 %>
<%= for {name, _df, _} <- @properties.embeds_one do %>|> cast_embed(:<%= Recase.to_snake(name) %>)
<% end %>
<%= for {name, _df, _} <- @properties.embeds_many do %>|> cast_embed(:<%= Recase.to_snake(name) %>)
<% end %>
|> validate_required(@required_fields)
end
end

47
lib/kindling/xml.ex Normal file
View file

@ -0,0 +1,47 @@
defmodule Kindling.Xml do
def parse(doc) do
case Saxy.SimpleForm.parse_string(doc) do
{:ok, tree} ->
process(tree)
other ->
other
end
end
def process({resource_type, _, children}) do
base = %{
"resourceType" => resource_type
}
Enum.reduce(children, base, &reduce_child/2)
end
def reduce_child(binary, acc) when is_binary(binary), do: acc
def reduce_child({"div", _subattrs, _children}, acc), do: acc
def reduce_child({attribute_name, subattrs, children}, acc) do
value =
case subattrs do
[{"value", value}] ->
value
list when is_list(list) ->
attrs = Enum.into(list, %{})
Enum.reduce(children, attrs, &reduce_child/2)
end
case Map.get(acc, attribute_name) do
nil ->
Map.put(acc, attribute_name, value)
list when is_list(list) ->
Map.put(acc, attribute_name, list ++ [value])
other ->
Map.put(acc, attribute_name, [other, value])
end
end
end

View file

@ -43,11 +43,12 @@ defmodule Kindling.MixProject do
defp deps do
[
{:credo, "~> 1.7", only: [:dev]},
{:ecto, "~> 3.11", only: [:dev, :test]},
{:ecto, "~> 3.11"},
{:ex_doc, "~> 0.31.2", only: [:dev]},
{:jason, "~> 1.4"},
{:recase, "~> 0.7.0"},
{:req, "~> 0.4.11"}
{:req, "~> 0.4.11"},
{:saxy, "~> 1.5.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]

View file

@ -21,5 +21,6 @@
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"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"},
"saxy": {:hex, :saxy, "1.5.0", "0141127f2d042856f135fb2d94e0beecda7a2306f47546dbc6411fc5b07e28bf", [:mix], [], "hexpm", "ea7bb6328fbd1f2aceffa3ec6090bfb18c85aadf0f8e5030905e84235861cf89"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}

View file

@ -1,8 +1,4 @@
defmodule KindlingTest do
use ExUnit.Case
doctest Kindling
test "greets the world" do
assert Kindling.hello() == :world
end
end