diff --git a/CHANGELOG.md b/CHANGELOG.md index b51265f..3697ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## v0.2.4 (2025-04-15) + + + + +### Improvements: + +* encode record or records returned from generic actions + ## v0.2.3 (2025-03-06) diff --git a/lib/ash_ops/task/action.ex b/lib/ash_ops/task/action.ex index bfa252d..35974f3 100644 --- a/lib/ash_ops/task/action.ex +++ b/lib/ash_ops/task/action.ex @@ -4,7 +4,7 @@ defmodule AshOps.Task.Action do This should only ever be called from the mix task itself. """ - alias Ash.ActionInput + alias Ash.{ActionInput, Resource.Info} alias AshOps.Task.ArgSchema import AshOps.Task.Common @@ -14,6 +14,7 @@ defmodule AshOps.Task.Action do {:ok, actor} <- load_actor(cfg[:actor], cfg[:tenant]), cfg <- Map.put(cfg, :actor, actor), {:ok, result} <- run_action(task, cfg), + {:ok, result} <- maybe_load(result, task, cfg), {:ok, output} <- serialise_result(result, cfg) do Mix.shell().info(output) @@ -23,6 +24,35 @@ defmodule AshOps.Task.Action do end end + defp maybe_load(result, task, cfg) do + if record_or_records?(result) do + {load, opts} = + cfg + |> Map.take([:load, :actor, :tenant]) + |> Map.put(:domain, task.domain) + |> Keyword.new() + |> Keyword.pop(:load) + + if load == [] do + {:ok, result} + else + Ash.load(result, load, opts) + end + else + {:ok, result} + end + end + + defp record_or_records?([%struct{} | _]) do + Info.resource?(struct) + end + + defp record_or_records?(%struct{}) do + Info.resource?(struct) + end + + defp record_or_records?(_), do: false + defp run_action(task, cfg) do args = cfg @@ -44,7 +74,19 @@ defmodule AshOps.Task.Action do end end - defp serialise_result(result, cfg) when cfg.format == :yaml do + defp serialise_result(result, cfg) do + if record_or_records?(result) do + if is_list(result) do + serialise_records(result, hd(result).__struct__, cfg) + else + serialise_record(result, result.__struct__, cfg) + end + else + serialise_generic_result(result, cfg) + end + end + + defp serialise_generic_result(result, cfg) when cfg.format == :yaml do result |> Ymlr.document() |> case do @@ -53,7 +95,7 @@ defmodule AshOps.Task.Action do end end - defp serialise_result(result, cfg) when cfg.format == :json do + defp serialise_generic_result(result, cfg) when cfg.format == :json do result |> Jason.encode(pretty: true) end diff --git a/lib/ash_ops/task/common.ex b/lib/ash_ops/task/common.ex index 17f291c..58d636f 100644 --- a/lib/ash_ops/task/common.ex +++ b/lib/ash_ops/task/common.ex @@ -47,18 +47,46 @@ defmodule AshOps.Task.Common do stop() end + @doc "Return the filter field for the configured identity, or the primary key" + def identity_or_pk_field(resource, cfg) + when is_atom(cfg.identity) and not is_nil(cfg.identity) do + case Info.identity(resource, cfg.identity) do + %{keys: [field]} -> {:ok, field} + _ -> {:error, "Composite identity error"} + end + end + + def identity_or_pk_field(resource, _cfg) do + case Info.primary_key(resource) do + [pk] -> {:ok, pk} + _ -> {:error, "Primary key error"} + end + end + @doc "Serialise the record for display" - def serialise_record(record, task, cfg) do - record - |> filter_record(task, cfg) - |> format_record(cfg[:format] || :yaml) + def serialise_record(record, resource, cfg) do + data = prepare_record(record, resource, cfg) + + case cfg.format do + :yaml -> + data + |> Ymlr.document() + |> case do + {:ok, yaml} -> {:ok, String.replace_leading(yaml, "---\n", "")} + {:error, reason} -> {:error, reason} + end + + :json -> + data + |> Jason.encode(pretty: true) + end end @doc "Serialise a list of records for display" - def serialise_records(records, task, cfg) when cfg.format == :yaml do + def serialise_records(records, resource, cfg) when cfg.format == :yaml do with {:ok, outputs} <- Enum.reduce_while(records, {:ok, []}, fn record, {:ok, outputs} -> - case serialise_record(record, task, cfg) do + case serialise_record(record, resource, cfg) do {:ok, output} -> {:cont, {:ok, [output | outputs]}} {:error, reason} -> {:halt, {:error, reason}} end @@ -72,56 +100,109 @@ defmodule AshOps.Task.Common do end end - def serialise_records(records, task, cfg) when cfg.format == :json do + def serialise_records(records, resource, cfg) when cfg.format == :json do records - |> Enum.map(&filter_record(&1, task, cfg)) + |> Enum.map(&prepare_record(&1, resource, cfg)) |> Jason.encode(pretty: true) end - def serialise_records(records, task, cfg), - do: serialise_records(records, task, Map.put(cfg, :format, :yaml)) + def serialise_records(records, resource, cfg), + do: serialise_records(records, resource, Map.put(cfg, :format, :yaml)) - @doc "Return the filter field for the configured identity, or the primary key" - def identity_or_pk_field(task, cfg) when is_atom(cfg.identity) and not is_nil(cfg.identity) do - case Info.identity(task.resource, cfg.identity) do - %{keys: [field]} -> {:ok, field} - _ -> {:error, "Composite identity error"} - end + # Filter and format record fields, but do not encode + defp prepare_record(record, resource, cfg) do + record + |> filter_record(resource, cfg) + |> format_record(resource, cfg) end - def identity_or_pk_field(task, _cfg) do - case Info.primary_key(task.resource) do - [pk] -> {:ok, pk} - _ -> {:error, "Primary key error"} + # Apply formatting to each field of a filtered record + defp format_record(record, resource, cfg) do + Map.new(record, fn + {key, value} -> + field_info = resource |> Info.field(key) + {key, format_value(value, field_info, cfg)} + end) + end + + @doc """ + Format a value given the field type info and formatting configuration options + """ + def format_value(value, field_info, cfg) + + # NOTE: In future, dispatch on the type, not the value to support new types + def format_value(%Ash.CiString{} = value, field_info, cfg) do + format_value(to_string(value), field_info, cfg) + end + + def format_value(nil, _field, %{format: :yaml}) do + "nil" + end + + def format_value(value, attribute = %{type: {:array, type}}, cfg) when is_list(value) do + inner_type = type + inner_constraints = attribute.constraints[:items] || [] + inner_attribute = %{attribute | type: inner_type, constraints: inner_constraints} + Enum.map(value, &format_value(&1, inner_attribute, cfg)) + end + + # HasMany or ManyToMany relationships + def format_value(value, attribute = %{cardinality: :many}, cfg) when is_list(value) do + Enum.map(value, &format_value(&1, attribute, cfg)) + end + + def format_value(%struct{} = value, field_info, cfg) do + if Info.resource?(struct) do + load = cfg[:load][field_info.name] || [] + cfg = Map.put(cfg, :load, load) + prepare_record(value, struct, cfg) + else + format_fallback_value(value, cfg) end end - defp format_record(record, :yaml) do - record - |> Map.new(fn - {key, nil} -> {key, "nil"} - {key, value} -> {key, value} - end) - |> Ymlr.document() - |> case do - {:ok, yaml} -> {:ok, String.replace_leading(yaml, "---\n", "")} - {:error, reason} -> {:error, reason} + def format_value(value, _field_info, cfg) do + format_fallback_value(value, cfg) + end + + defp format_fallback_value(value, %{format: :json}) do + if Jason.Encoder.impl_for(value) do + value + else + "" end end - defp format_record(record, :json) do - record - |> Jason.encode(pretty: true) + defp format_fallback_value(value, %{format: :yaml}) do + if Ymlr.Encoder.impl_for(value) do + value + else + "" + end end - defp filter_record(record, task, cfg) do - task.resource + # Convert a record to a plain map, excluding private fields + defp filter_record(record, _resource, cfg) do + record |> Info.public_fields() |> Enum.map(& &1.name) |> Enum.concat(cfg[:load] || []) + |> Enum.concat(include_metadata?(record, cfg)) |> do_filter_record(record) end + defp include_metadata?(record, cfg) when cfg.metadata == true do + dbg(cfg) + + [:__metadata__] + end + + defp include_metadata?(record, cfg) + when cfg.metadata == true and map_size(record.__metadata__) > 0, + do: [:__metadata__] + + defp include_metadata?(_record, _cfg), do: [] + defp do_filter_record(fields, record, result \\ %{}) defp do_filter_record([], _record, result), do: result diff --git a/lib/ash_ops/task/create.ex b/lib/ash_ops/task/create.ex index f060a8c..8b4c88a 100644 --- a/lib/ash_ops/task/create.ex +++ b/lib/ash_ops/task/create.ex @@ -14,7 +14,7 @@ defmodule AshOps.Task.Create do with {:ok, cfg} <- ArgSchema.parse(arg_schema, argv), {:ok, changeset} <- read_input(task, cfg), {:ok, record} <- create_record(changeset, task, cfg), - {:ok, output} <- serialise_record(record, task, cfg) do + {:ok, output} <- serialise_record(record, task.resource, cfg) do Mix.shell().info(output) :ok else @@ -233,6 +233,14 @@ defmodule AshOps.Task.Create do ], [:i] ) + |> ArgSchema.add_switch( + :metadata, + :boolean, + type: :boolean, + required: false, + default: false, + doc: "Whether or not to include any record metadata in the result" + ) @shortdoc "Create a `#{inspect(@task.resource)}` record using the `#{@task.action.name}` action" diff --git a/lib/ash_ops/task/destroy.ex b/lib/ash_ops/task/destroy.ex index 667360f..6a4a476 100644 --- a/lib/ash_ops/task/destroy.ex +++ b/lib/ash_ops/task/destroy.ex @@ -17,7 +17,7 @@ defmodule AshOps.Task.Destroy do :ok <- destroy_record(task, cfg) do :ok else - {:error, reason} -> handle_error(reason) + {:error, reason} -> handle_error({:error, reason}) end end @@ -28,7 +28,7 @@ defmodule AshOps.Task.Destroy do |> Map.put(:domain, task.domain) |> Enum.to_list() - with {:ok, field} <- identity_or_pk_field(task, cfg) do + with {:ok, field} <- identity_or_pk_field(task.resource, cfg) do task.resource |> Query.new() |> Query.filter_input(%{field => %{"eq" => cfg.positional_arguments.id}}) diff --git a/lib/ash_ops/task/get.ex b/lib/ash_ops/task/get.ex index 1f65c16..5cef0b3 100644 --- a/lib/ash_ops/task/get.ex +++ b/lib/ash_ops/task/get.ex @@ -16,7 +16,7 @@ defmodule AshOps.Task.Get do {:ok, actor} <- load_actor(cfg[:actor], cfg[:tenant]), cfg <- Map.put(cfg, :actor, actor), {:ok, record} <- load_record(task, cfg), - {:ok, output} <- serialise_record(record, task, cfg) do + {:ok, output} <- serialise_record(record, task.resource, cfg) do Mix.shell().info(output) :ok @@ -34,7 +34,7 @@ defmodule AshOps.Task.Get do |> Map.put(:authorize_with, :error) |> Enum.to_list() - with {:ok, field} <- identity_or_pk_field(task, cfg) do + with {:ok, field} <- identity_or_pk_field(task.resource, cfg) do task.resource |> Query.new() |> Query.for_read(task.action.name) @@ -60,6 +60,14 @@ defmodule AshOps.Task.Get do ], [:i] ) + |> ArgSchema.add_switch( + :metadata, + :boolean, + type: :boolean, + required: false, + default: false, + doc: "Whether or not to include any record metadata in the result" + ) @shortdoc "Get a single `#{inspect(@task.resource)}` record using the `#{@task.action.name}` action" diff --git a/lib/ash_ops/task/list.ex b/lib/ash_ops/task/list.ex index f2c65c8..7b73d1a 100644 --- a/lib/ash_ops/task/list.ex +++ b/lib/ash_ops/task/list.ex @@ -16,7 +16,7 @@ defmodule AshOps.Task.List do {:ok, query} <- QueryLang.parse(task, query), {:ok, actor} <- load_actor(cfg[:actor], cfg[:tenant]), {:ok, records} <- load_records(query, task, Map.put(cfg, :actor, actor)), - {:ok, output} <- serialise_records(records, task, cfg) do + {:ok, output} <- serialise_records(records, task.resource, cfg) do Mix.shell().info(output) :ok @@ -109,6 +109,14 @@ defmodule AshOps.Task.List do required: false, doc: "An optional sort to apply to the query" ) + |> ArgSchema.add_switch( + :metadata, + :boolean, + type: :boolean, + required: false, + default: false, + doc: "Whether or not to include any record metadata in the result" + ) @shortdoc "Query for `#{inspect(@task.resource)}` records using the `#{@task.action.name}` action" diff --git a/lib/ash_ops/task/types.ex b/lib/ash_ops/task/types.ex index f26e340..584a32d 100644 --- a/lib/ash_ops/task/types.ex +++ b/lib/ash_ops/task/types.ex @@ -76,7 +76,7 @@ defmodule AshOps.Task.Types do {:ok, args} else {:error, - "Expected #{input_length} positional arguments, but received #{expected_arg_count}"} + "Expected #{expected_arg_count} positional arguments, but received #{input_length}"} end end diff --git a/lib/ash_ops/task/update.ex b/lib/ash_ops/task/update.ex index 025bbf6..470d940 100644 --- a/lib/ash_ops/task/update.ex +++ b/lib/ash_ops/task/update.ex @@ -17,7 +17,7 @@ defmodule AshOps.Task.Update do {:ok, record} <- load_record(task, cfg), {:ok, changeset} <- read_input(record, task, cfg), {:ok, record} <- update_record(changeset, task, cfg), - {:ok, output} <- serialise_record(record, task, cfg) do + {:ok, output} <- serialise_record(record, task.resource, cfg) do Mix.shell().info(output) :ok else @@ -44,7 +44,7 @@ defmodule AshOps.Task.Update do |> Map.put(:authorize_with, :error) |> Enum.to_list() - with {:ok, field} <- identity_or_pk_field(task, cfg) do + with {:ok, field} <- identity_or_pk_field(task.resource, cfg) do task.resource |> Query.new() |> Query.for_read(task.read_action.name) @@ -262,6 +262,14 @@ defmodule AshOps.Task.Update do doc: "Read action input from STDIN in this format. Valid options are `json`, `yaml` and `interactive`. Defaults to `interactive`." ) + |> ArgSchema.add_switch( + :metadata, + :boolean, + type: :boolean, + required: false, + default: false, + doc: "Whether or not to include any record metadata in the result" + ) @shortdoc "Update a single `#{inspect(@task.resource)}` record using the `#{@task.action.name}` action" diff --git a/mix.exs b/mix.exs index b5ef1db..61eb27f 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule AshOps.MixProject do use Mix.Project @moduledoc "An Ash extension which generates mix tasks for Ash actions" - @version "0.2.3" + @version "0.2.4" def project do [ aliases: aliases(), @@ -79,7 +79,8 @@ defmodule AshOps.MixProject do [ "spark.formatter": "spark.formatter --extensions AshOps", "spark.cheat_sheets": "spark.cheat_sheets --extensions AshOps", - docs: ["spark.cheat_sheets", "docs"] + docs: ["spark.cheat_sheets", "docs"], + credo: "credo --strict" ] end diff --git a/mix.lock b/mix.lock index d597613..98fe5fa 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ - "ash": {:hex, :ash, "3.4.67", "b2e3e65ddff550998a5c8dd1fc7f8ef4d7ebfe75a474c1e620e9b51b3ff5f70d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.24 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "770a637208ef0be5b9778687efb55be7b4f75d0202f80ddc4b1b918a8282ba28"}, + "ash": {:hex, :ash, "3.5.6", "2f187150110b4c280c8551ad411f56d95862fcb37c067a0b8b94eb682bcc43e8", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.24 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d9aeb5aacfdc12253fae1e7e4720991868c5f69632c2766afb03b2b1830f55"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [: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", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [: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", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "doctor": {:hex, :doctor, "0.22.0", "223e1cace1f16a38eda4113a5c435fa9b10d804aa72d3d9f9a71c471cc958fe7", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "96e22cf8c0df2e9777dc55ebaa5798329b9028889c4023fed3305688d902cd5b"}, @@ -10,15 +10,15 @@ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, - "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, + "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.7.2", "2d3c164a8bcaf13f129ab339e8e9f0a99c80ffa8f85dd0b344d7515275236dbc", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "1dcd68b3f5bcd0999d69274cd21e74e652a90452e683b54d490fa5b26152945f"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, - "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, - "igniter": {:hex, :igniter, "0.5.31", "efee5efb94bcfce37d317ab54c54b979d8864bf9b76aafded82f7bb52a306076", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "4f0c9456135444286ae89b22db10f8f1ad18932ccd4935bc14a5ad41704e5a1f"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.5.47", "7a1041d5e38303e526fa6b6de37c9e78013f5cb573833ed51183d18e3a152f10", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "53a900909e20f217a25d15a34fef629c562b4822c1fb39cfa5d6999bc72992ed"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -35,16 +35,16 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"}, - "reactor": {:hex, :reactor, "0.14.0", "8dc5d4946391010bf9fa7b58dd1e75d3c1cf97315e5489b7797cf64b82ae27a4", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9cf5068e4042791c150f0dfbc00f4f435433eb948036b44b95b940e457b35a6a"}, - "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{: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, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, + "reactor": {:hex, :reactor, "0.15.2", "8c1b3fe0527b7a92b0b22c3f33f2e66858dd069bf1dd51d1031f63cd8cbd1fd5", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "091435a1fa0cab9bc2ed3934b203a0fd190f62e8b6aca63741f9242b8c7631ac"}, + "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{: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, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, "simple_sat": {:hex, :simple_sat, "0.1.3", "f650fc3c184a5fe741868b5ac56dc77fdbb428468f6dbf1978e14d0334497578", [:mix], [], "hexpm", "a54305066a356b7194dc81db2a89232bacdc0b3edaef68ed9aba28dcbc34887b"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, - "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, - "spark": {:hex, :spark, "2.2.45", "19e3a879e80d02853ded85ed7b4c0a84a5d2e395f9d0c884e1a13afbe026929d", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "70b272d0ee16e3c10a4f8cf0ef6152840828152e68f2f8e3046e89567f2b49ad"}, - "spitfire": {:hex, :spitfire, "0.1.5", "10b041e781bff9544d2fdf00893e1a325758408c5366a9bfa4333072568659b1", [:mix], [], "hexpm", "866a55d21fe827934ff38200111335c9dd311df13cbf2580ed71d84b0a783150"}, + "sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"}, + "spark": {:hex, :spark, "2.2.54", "0dff77bae9267fc5d0aea440001caa9dc277ad183fe0ecee60d2ea1f7027c1f7", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "251df484af26d3af79054aab3f6d98a28633b8a38b5d90acf05e53e7c67ef485"}, + "spitfire": {:hex, :spitfire, "0.2.0", "0de1f519a23f65bde40d316adad53c07a9563f25cc68915d639d8a509a0aad8a", [:mix], [], "hexpm", "743daaee2d81a0d8095431729f478ce49b47ea8943c7d770de86704975cb7775"}, "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, - "stream_data": {:hex, :stream_data, "1.1.3", "15fdb14c64e84437901258bb56fc7d80aaf6ceaf85b9324f359e219241353bfb", [:mix], [], "hexpm", "859eb2be72d74be26c1c4f272905667672a52e44f743839c57c7ee73a1a66420"}, + "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, diff --git a/test/ash_ops/task/action_test.exs b/test/ash_ops/task/action_test.exs new file mode 100644 index 0000000..01ce50a --- /dev/null +++ b/test/ash_ops/task/action_test.exs @@ -0,0 +1,25 @@ +defmodule AshOps.Task.ActionTest do + @moduledoc false + use ExUnit.Case, async: true + import ExUnit.CaptureIO + + setup do + ansi_enabled? = Application.get_env(:elixir, :ansi_enabled) + Application.put_env(:elixir, :ansi_enabled, false) + + on_exit(fn -> + Application.put_env(:elixir, :ansi_enabled, ansi_enabled?) + end) + end + + test "a resource is encoded" do + id = Ash.UUID.generate() + + output = + capture_io(fn -> + Mix.Task.rerun("ash_ops.example.publish_post", [id, "platform"]) + end) + + assert output =~ ~r/id: #{id}/m + end +end diff --git a/test/ash_ops/task/get_test.exs b/test/ash_ops/task/get_test.exs index 86ae99f..08f435a 100644 --- a/test/ash_ops/task/get_test.exs +++ b/test/ash_ops/task/get_test.exs @@ -73,7 +73,7 @@ defmodule AshOps.Task.GetTest do ]) end) - assert output =~ ~r/not found/m + assert output =~ ~r/not found/im end test "it can format the output as JSON", %{post: post} do @@ -125,7 +125,7 @@ defmodule AshOps.Task.GetTest do ]) end) - assert output =~ ~r/forbidden/m + assert output =~ ~r/forbidden/im end test "when the post doesn't exist, it fails" do @@ -134,7 +134,7 @@ defmodule AshOps.Task.GetTest do Mix.Task.rerun("ash_ops.example.get_post", [to_string(Ash.UUID.generate())]) end) - assert output =~ ~r/not found/m + assert output =~ ~r/not found/im end test "calculations can be loaded and returned", %{post: post} do @@ -167,4 +167,13 @@ defmodule AshOps.Task.GetTest do assert output =~ ~r/id: #{post.id}/m assert output =~ ~r/author:\n id: #{author.id}/m end + + test "metadata can be included in the output", %{post: post} do + # output = + # capture_io(fn -> + Mix.Task.rerun("ash_ops.example.get_post", ["--metadata", post.id]) + # end) + # + # refute output =~ ~r/__metadata__:/m + end end diff --git a/test/support/example/post.ex b/test/support/example/post.ex index c254655..584bb46 100644 --- a/test/support/example/post.ex +++ b/test/support/example/post.ex @@ -8,10 +8,30 @@ defmodule Example.Post do actions do defaults [:read, :destroy, create: :*, update: :*] - action :publish, :boolean do + read :read_with_etag do + metadata :etag, :string, allow_nil?: false + + prepare after_action(fn _query, records, _context -> + records = + records + |> Enum.map(fn record -> + etag = + record + |> Map.take([:id, :title, :body, :slug, :tenant, :updated_at]) + |> :erlang.phash2() + + Ash.Resource.put_metadata(record, :etag, etag) + end) + + {:ok, records} + end) + end + + action :publish, :struct do + constraints instance_of: __MODULE__ argument :id, :uuid, public?: true, allow_nil?: false argument :platform, :string, public?: true, allow_nil?: false - run fn _, _ -> {:ok, true} end + run fn input, _ -> {:ok, %__MODULE__{id: input.arguments.id}} end end end