diff --git a/lib/kindling/client.ex b/lib/kindling/client.ex index d590bad..661a198 100644 --- a/lib/kindling/client.ex +++ b/lib/kindling/client.ex @@ -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 diff --git a/lib/kindling/client/req.ex b/lib/kindling/client/req.ex index b8f3a26..c88142d 100644 --- a/lib/kindling/client/req.ex +++ b/lib/kindling/client/req.ex @@ -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 diff --git a/lib/kindling/converter.ex b/lib/kindling/converter.ex index c60fcc8..2e20235 100644 --- a/lib/kindling/converter.ex +++ b/lib/kindling/converter.ex @@ -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 diff --git a/lib/kindling/converter/date_time.ex b/lib/kindling/converter/date_time.ex index b63be3a..5f3e9ae 100644 --- a/lib/kindling/converter/date_time.ex +++ b/lib/kindling/converter/date_time.ex @@ -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. diff --git a/lib/kindling/embed_template.eex b/lib/kindling/embed_template.eex index fec282c..0c65d20 100644 --- a/lib/kindling/embed_template.eex +++ b/lib/kindling/embed_template.eex @@ -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 diff --git a/lib/kindling/template.eex b/lib/kindling/template.eex index 6255953..d713f6f 100644 --- a/lib/kindling/template.eex +++ b/lib/kindling/template.eex @@ -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 diff --git a/lib/kindling/xml.ex b/lib/kindling/xml.ex new file mode 100644 index 0000000..1c3ddca --- /dev/null +++ b/lib/kindling/xml.ex @@ -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 diff --git a/mix.exs b/mix.exs index 2ff3f69..34970a0 100644 --- a/mix.exs +++ b/mix.exs @@ -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"} ] diff --git a/mix.lock b/mix.lock index 399365f..2d5d8bd 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, } diff --git a/test/kindling_test.exs b/test/kindling_test.exs index 3d92d45..5ac2310 100644 --- a/test/kindling_test.exs +++ b/test/kindling_test.exs @@ -1,8 +1,4 @@ defmodule KindlingTest do use ExUnit.Case doctest Kindling - - test "greets the world" do - assert Kindling.hello() == :world - end end