diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index cc6adca2..b7f03afd 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -351,30 +351,6 @@ defmodule Ash.Changeset do end @for_create_opts [ - relationships: [ - type: :any, - doc: """ - customize relationship behavior. - - By default, any relationships are ignored. There are three ways to change relationships with this function: - - ### Action Arguments (preferred) - - Create an argument on the action and add a `Ash.Resource.Change.Builtins.manage_relationship/3` change to the action. - - ### Overrides - - You can pass the `relationships` option to specify the behavior. It is a keyword list of relationship and either - * one of the preset manage types: #{inspect(@manage_types)} - * explicit options, in the form of `{:manage, [...opts]}` - - ```elixir - Ash.Changeset.for_create(MyResource, :create, params, relationships: [relationship: :append, other_relationship: {:manage, [...opts]}]) - ``` - - You can also use explicit calls to `manage_relationship/4`. - """ - ], require?: [ type: :boolean, default: false, @@ -532,7 +508,7 @@ defmodule Ash.Changeset do |> set_tenant(opts[:tenant] || changeset.tenant) |> Map.put(:__validated_for_action__, action.name) |> Map.put(:action, action) - |> cast_params(action, params, opts) + |> cast_params(action, params) |> set_argument_defaults(action) |> run_action_changes(action, opts[:actor]) |> add_validations() @@ -560,7 +536,7 @@ defmodule Ash.Changeset do |> set_tenant(opts[:tenant] || changeset.tenant || changeset.data.__metadata__[:tenant]) |> Map.put(:action, action) |> Map.put(:__validated_for_action__, action.name) - |> cast_params(action, params || %{}, opts) + |> cast_params(action, params || %{}) |> set_argument_defaults(action) |> validate_attributes_accepted(action) |> require_values(action.type, false, action.require_attributes) @@ -767,7 +743,7 @@ defmodule Ash.Changeset do end end - defp cast_params(changeset, action, params, opts) do + defp cast_params(changeset, action, params) do changeset = %{ changeset | params: Map.merge(changeset.params || %{}, Enum.into(params || %{}, %{})) @@ -785,24 +761,6 @@ defmodule Ash.Changeset do changeset end - rel = Ash.Resource.Info.public_relationship(changeset.resource, name) -> - if rel.writable? do - behaviour = opts[:relationships][rel.name] - - case behaviour do - nil -> - changeset - - type when is_atom(type) -> - manage_relationship(changeset, rel.name, value, type: type) - - {:manage, manage_opts} -> - manage_relationship(changeset, rel.name, value, manage_opts) - end - else - changeset - end - true -> changeset end @@ -1568,11 +1526,6 @@ defmodule Ash.Changeset do * belongs_to - an update action on the source resource """ ], - relationships: [ - type: :any, - default: [], - doc: "A keyword list of instructions for nested relationships." - ], error_path: [ type: :any, doc: """ @@ -1778,15 +1731,6 @@ defmodule Ash.Changeset do add_error(changeset, error) - %{writable?: false} = relationship -> - error = - InvalidRelationship.exception( - relationship: relationship.name, - message: "Relationship is not editable" - ) - - add_error(changeset, error) - relationship -> if relationship.cardinality == :many && is_map(input) && !is_struct(input) do case map_input_to_list(input) do diff --git a/lib/ash/dsl/dsl.ex b/lib/ash/dsl/dsl.ex index e2503dfc..b12f4283 100644 --- a/lib/ash/dsl/dsl.ex +++ b/lib/ash/dsl/dsl.ex @@ -159,7 +159,18 @@ defmodule Ash.Dsl do defmacro __after_compile__(_, _) do quote do - Ash.Dsl.Extension.run_after_compile() + transformers_to_run = + @extensions + |> Enum.flat_map(& &1.transformers()) + |> Ash.Dsl.Transformer.sort() + |> Enum.filter(& &1.after_compile?()) + + __MODULE__ + |> Ash.Dsl.Extension.run_transformers( + transformers_to_run, + Module.get_attribute(__MODULE__, :ash_dsl_config), + false + ) end end diff --git a/lib/ash/dsl/extension.ex b/lib/ash/dsl/extension.ex index 5cbe3c51..85c0bed9 100644 --- a/lib/ash/dsl/extension.ex +++ b/lib/ash/dsl/extension.ex @@ -484,23 +484,6 @@ defmodule Ash.Dsl.Extension do end end - defmacro run_after_compile do - quote do - transformers_to_run = - @extensions - |> Enum.flat_map(& &1.transformers()) - |> Ash.Dsl.Transformer.sort() - |> Enum.filter(& &1.after_compile?()) - - __MODULE__ - |> Ash.Dsl.Extension.run_transformers( - transformers_to_run, - Module.get_attribute(__MODULE__, :ash_dsl_config), - false - ) - end - end - def run_transformers(mod, transformers, ash_dsl_config, store?) do Enum.reduce_while(transformers, ash_dsl_config, fn transformer, dsl -> result = diff --git a/lib/ash/elixir_sense/plugin.ex b/lib/ash/elixir_sense/plugin.ex index 738352af..d43e72ae 100644 --- a/lib/ash/elixir_sense/plugin.ex +++ b/lib/ash/elixir_sense/plugin.ex @@ -10,6 +10,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do alias ElixirSense.Providers.Suggestion.Matcher def suggestions(hint, {_, function_call, arg_index, info}, _chain, opts) do + opts = add_module_store(opts) option = info.option || get_option(opts.cursor_context.text_before) if option do @@ -20,6 +21,7 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do end def suggestions(hint, opts) do + opts = add_module_store(opts) option = get_section_option(opts.cursor_context.text_before) if option do @@ -29,6 +31,22 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do end end + # For some reason, the module store does not change when modules are defined + # so we are building our own fresh copy here. This is definitely a performance + # hit + defp add_module_store(opts) do + Map.put(opts, :module_store, ElixirSense.Core.ModuleStore.build(all_loaded())) + end + + defp all_loaded do + :code.all_loaded() + |> Enum.filter(fn + {mod, _} when is_atom(mod) -> true + _ -> false + end) + |> Enum.map(&elem(&1, 0)) + end + def get_suggestions(hint, opts, opt_path \\ [], type \\ nil) do with true <- Enum.any?(opts.env.attributes, &(&1.name == :ash_is)), dsl_mod when not is_nil(dsl_mod) <- @@ -587,7 +605,14 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do end) |> Enum.uniq() |> Enum.map(&Module.concat([&1])) - |> Enum.filter(&Code.ensure_loaded?/1) + |> Enum.filter(fn module -> + try do + Code.ensure_loaded?(module) + rescue + _ -> + false + end + end) end end end diff --git a/lib/ash/elixir_sense/resource.ex b/lib/ash/elixir_sense/resource.ex index f9587705..01fab267 100644 --- a/lib/ash/elixir_sense/resource.ex +++ b/lib/ash/elixir_sense/resource.ex @@ -30,6 +30,41 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do else [] end + |> Enum.reject(fn + %{name: name} when name in ["__info__", "module_info", "module_info"] -> + true + + _ -> + false + end) + |> Enum.map(fn + %{type: :function, origin: origin, name: name, arity: arity} = completion -> + try do + {:docs_v1, _, _, _, _, _, functions} = Code.fetch_docs(Module.concat([origin])) + + new_summary = + Enum.find_value(functions, fn + {{:function, func_name, func_arity}, _, _, + %{ + "en" => docs + }, _} -> + if to_string(func_name) == name && func_arity == arity do + docs + end + + _other -> + false + end) + + %{completion | summary: new_summary || completion.summary} + rescue + _e -> + completion + end + + other -> + other + end) custom = for module <- module_store.by_behaviour[behaviour] || [], @@ -51,6 +86,8 @@ if Code.ensure_loaded?(ElixirSense.Plugin) do builtins ++ custom end + defp lowercase_string?(""), do: true + defp lowercase_string?(string) do first = String.first(string) String.downcase(first) == first diff --git a/lib/ash/resource/change/builtins.ex b/lib/ash/resource/change/builtins.ex index 2203a643..6314795e 100644 --- a/lib/ash/resource/change/builtins.ex +++ b/lib/ash/resource/change/builtins.ex @@ -24,8 +24,8 @@ defmodule Ash.Resource.Change.Builtins do If a zero argument function is provided, it is called to determine the value. - If a tuple of `{:arg, :argument_name}` is provided, the value will be read from the argument if supplied. - If the argument is not supplied then nothing happens. + If a `arg(:arg_name)` is provided, the value will be read from the argument if supplied. + If the argument specified is not given to the action, then nothing happens. """ def set_attribute(attribute, value) do {Ash.Resource.Change.SetAttribute, attribute: attribute, value: value} diff --git a/lib/ash/resource/relationships/belongs_to.ex b/lib/ash/resource/relationships/belongs_to.ex index f5420a58..a3819690 100644 --- a/lib/ash/resource/relationships/belongs_to.ex +++ b/lib/ash/resource/relationships/belongs_to.ex @@ -56,6 +56,12 @@ defmodule Ash.Resource.Relationships.BelongsTo do @opt_schema Ash.OptionsHelpers.merge_schemas( [ + writable?: [ + type: :boolean, + doc: + "Whether or not the attribute created by this relationship will be marked with `writable?: true`.", + default: false + ], primary_key?: [ type: :boolean, default: false, diff --git a/lib/ash/resource/relationships/has_many.ex b/lib/ash/resource/relationships/has_many.ex index 5de45f8f..483bea17 100644 --- a/lib/ash/resource/relationships/has_many.ex +++ b/lib/ash/resource/relationships/has_many.ex @@ -8,7 +8,6 @@ defmodule Ash.Resource.Relationships.HasMany do :source_field, :source, :context, - :writable?, :description, :filter, :sort, @@ -28,7 +27,6 @@ defmodule Ash.Resource.Relationships.HasMany do type: :has_many, cardinality: :many, source: Ash.Resource.t(), - writable?: boolean, read_action: atom, filter: Ash.Filter.t() | nil, no_fields?: boolean, diff --git a/lib/ash/resource/relationships/has_one.ex b/lib/ash/resource/relationships/has_one.ex index cbd0d8b3..ec2b08e3 100644 --- a/lib/ash/resource/relationships/has_one.ex +++ b/lib/ash/resource/relationships/has_one.ex @@ -9,7 +9,6 @@ defmodule Ash.Resource.Relationships.HasOne do :private?, :source_field, :allow_orphans?, - :writable?, :context, :description, :filter, @@ -31,7 +30,6 @@ defmodule Ash.Resource.Relationships.HasOne do type: :has_one, cardinality: :one, source: Ash.Resource.t(), - writable?: boolean, name: atom, read_action: atom, no_fields?: boolean, diff --git a/lib/ash/resource/relationships/many_to_many.ex b/lib/ash/resource/relationships/many_to_many.ex index a336e54d..80dd707a 100644 --- a/lib/ash/resource/relationships/many_to_many.ex +++ b/lib/ash/resource/relationships/many_to_many.ex @@ -13,7 +13,6 @@ defmodule Ash.Resource.Relationships.ManyToMany do :not_found_message, :violation_message, :api, - :writable?, :private?, :sort, :read_action, @@ -30,7 +29,6 @@ defmodule Ash.Resource.Relationships.ManyToMany do type: :many_to_many, cardinality: :many, source: Ash.Resource.t(), - writable?: boolean, private?: boolean, filter: Ash.Filter.t() | nil, read_action: atom, diff --git a/lib/ash/resource/relationships/shared_options.ex b/lib/ash/resource/relationships/shared_options.ex index 98eb42a1..bb1e29c7 100644 --- a/lib/ash/resource/relationships/shared_options.ex +++ b/lib/ash/resource/relationships/shared_options.ex @@ -26,11 +26,6 @@ defmodule Ash.Resource.Relationships.SharedOptions do doc: "The field on this resource that should match the `destination_field` on the related resource." ], - writable?: [ - type: :boolean, - doc: "Whether or not the relationship may be edited.", - default: true - ], description: [ type: :string, doc: "An optional description for the relationship" diff --git a/lib/ash/resource/transformers/belongs_to_attribute.ex b/lib/ash/resource/transformers/belongs_to_attribute.ex index d4252786..9cd1afbc 100644 --- a/lib/ash/resource/transformers/belongs_to_attribute.ex +++ b/lib/ash/resource/transformers/belongs_to_attribute.ex @@ -29,7 +29,7 @@ defmodule Ash.Resource.Transformers.BelongsToAttribute do else not relationship.required? end, - writable?: false, + writable?: relationship.writable?, private?: true, primary_key?: relationship.primary_key? ) diff --git a/test/actions/create_test.exs b/test/actions/create_test.exs index 77b2a481..7913e5ad 100644 --- a/test/actions/create_test.exs +++ b/test/actions/create_test.exs @@ -692,18 +692,6 @@ defmodule Ash.Test.Actions.CreateTest do end describe "creating with required belongs_to relationships" do - test "allows creating with belongs_to relationship" do - author = - Author - |> new() - |> change_attribute(:bio, "best dude") - |> Api.create!() - - ProfileWithBelongsTo - |> Ash.Changeset.for_create(:create, [author: author], relationships: [author: :replace]) - |> Api.create!() - end - test "does not allow creating without the required belongs_to relationship" do assert_raise Ash.Error.Invalid, ~r/relationship author is required/, fn -> ProfileWithBelongsTo diff --git a/test/elixir_sense/plugin_test.exs b/test/elixir_sense/plugin_test.exs index 2fb2a308..4498ca8c 100644 --- a/test/elixir_sense/plugin_test.exs +++ b/test/elixir_sense/plugin_test.exs @@ -543,7 +543,8 @@ defmodule Ash.ElixirSense.PluginTest do snippet: nil, spec: "", summary: - "Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument", + "Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument" <> + _, type: :function, visibility: :public }, @@ -558,7 +559,8 @@ defmodule Ash.ElixirSense.PluginTest do snippet: nil, spec: "", summary: - "Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument", + "Calls `Ash.Changeset.manage_relationship/4` with the changeset and relationship provided, using the value provided for the named argument" <> + _, type: :function, visibility: :public }