Confused about `one_of` vs `attribute_equals`

lpmay
2023-07-18

lpmay:

Still messing around and encountered a confusing behavior with validations. I have a :verify action on a resource, which I only want to run on resources which have :role set to :unverified The below works as expected (produces valid/invalid changesets as expected):

    update :verify do #custom action to verify an unverified user
      validate attribute_equals(:role, :unverified) #only verify unverified users
      change set_attribute(:role, :verified)
      accept []
    end

What confuses me is that the below version using one_of instead of attribute_equals seems to me like it should do the same thing, but it never seems to produce an invalid changeset:

    update :verify do #custom action to verify an unverified user
      validate one_of(:role, [:unverified]) #only verify unverified users
      change set_attribute(:role, :verified)
      accept []
    end

Can someone point out what I am misunderstanding?

zachdaniel:

They happen in order šŸ˜„

zachdaniel:

    update :verify do #custom action to verify an unverified user
      change set_attribute(:role, :verified)
      validate one_of(:role, [:unverified]) #only verify unverified users
      accept []
    end

zachdaniel:

that should produce an error

lpmay:

Hmm Ok the in order thing is good to know - but I think I’ve tried it with the order the same but the behavior of attribute_equals and one_of with a list of one is not the same

lpmay:

Am I maybe completely misusing this feature? Are validations just to make sure the attributes are consistent after the action, but I’m trying to use them to gate which resources the action can run on in the first place?

lpmay:

yea ok I just did it again - exact same order, but if the validate clause uses attribute_equals I can generate invalid changesets, but not with one_of

lpmay:

hmm so attribute_in works as I expect one_of to work, so clearly there is some distinction I don’t quite get yet

lpmay:

The distinction here is still confusing: attribute_in ā€œValidates that an attribute is being changed to one of a set of specific values, or is in the the given list if it is not being changed. ā€œ which seems ill-defined for my case where I am changing the attribute, but not to one of the specified values one_of ā€œ Validates that an attribute’s value is in a given listā€ - seems more like what I’m going for but obviously doesn’t work how I expect

zachdaniel:

Hmm….yeah something seems up there

zachdaniel:

Does attribute_in not seem like what you want?

zachdaniel:

Or do you want the opposite of that

lpmay:

Yea attribute_in and attribute_equals both seem to work exactly as I expect, but one_of with a list of one element works differently

lpmay:

I’m just getting started with Ash so my question is mostly around how to understand the difference, there’s either something wrong with one_of or more likely my mental model for how I should use these validations is wrong

lpmay:

I realize what I said is kind of confusing - one_of just seems to work differently in general, I’ve tried with different sized lists too

lpmay:

Sorry for the walls of text, but I also realize I could’ve been clearer about what I’m doing in the first place: I have a User resource with a :role attribute constrained to be one_of: [:admin, :verified, :unverified] . I want my verify action to move an unverified user to verified but be invalid for any other type of user

lpmay:

just as an exercise to kick the tires with

dblack1:

You can run the validation in a before_action hook, something like validate one_of(:role, [:unverified]), before_action?: true should do it

dblack1:

Bit more info about the validate options are here: https://ash-hq.org/docs/dsl/ash-resource#validations-validate

dblack1:

sorry, re-read your original question. I’m not sure why one_of would act differently to attribute_equals

dblack1:

seems like they both use different functions to get the value from the changeset

dblack1:

Not sure if that’s expected or a bug?

dblack1:

So yeah the difference between attribute_equals and one_of is subtle… one_of seems to check for changes to an attribute or argument already passed to the changeset, and passes if that attribute hasn’t yet changed. In your example above :role hasn’t yet changed so it always passes

lpmay:

Thanks for the clarification, I think I get it. Is it correct to think of it as one_of is validating the changeset while attribute_equals is validating against the resource ? My verify action is adding the change to the role attribute, so the changeset one_of validates against does not have a role field to fail on?

lpmay:

Thanks for pointing out the before_action? option. Probably not a cut and dry answer, but is using before_action generally the more idiomatic approach, or is it better to rely on the order inside the action?

lpmay:

Ok - I’m even more confused now.

validate attribute_equals(:role, :unverified), before_action?: false creates failed changests for user records with :role != :unverified as expected.

validate attribute_equals(:role, :unverified), before_action?: true does not create a failed changset for user records with :role != :unverified .

lpmay:

I’ll have to spend some more time with the docs tomorrow and see if I can start to wrap my head around this a little better

dblack1:

Yeah I believe that’s the case for one_of … The doco indicates attribute_equals validates the changeset then the resource if that field isn’t getting changed in the changeset

dblack1:

I think I might have sent you on a bum steer with before_action, that’s probably not useful for this example

dblack1:

Not 100% sure but maybe before_action operates only on the changeset? You’d use it to modify the changeset before running the action

zachdaniel:

The before_action? will only fail when you attempt to submit the action (not when you validate the changeset)

zachdaniel:

It definitely seems like the behavior of those builtin validations can be confusing, especially when comparing the two

zachdaniel:

what I’d say is that a lot of those are just recomendations, and by design you can write your own validations if there is ever confusion/you aren’t getting behavior that you want. Of couse we should fix any issues with the builtin ones though šŸ™‚

zachdaniel:

But, for example:

validate fn changeset, _ -> 
  ...
end

# or
validate MyValidation

defmodule Myvalidation do
  use Ash.Resource.Validation
end