crecto icon indicating copy to clipboard operation
crecto copied to clipboard

Changeset-level validations (and maybe a changeset dsl?)

Open faultyserver opened this issue 7 years ago • 3 comments

Kind of expanding from #208 / #210, I'd like to be able to use validations at the changeset level rather than at the model level. For example:

class Accounts < Crecto::Model
  schema "accounts" do
    field :username, String
    field :password, String, virtual: true
    field :password_confirm, String, virtual: true
  end

  # When creating a User, validate that the username and password are both
  # given, and that the password confirmation matches.
  def create_changeset(user : self, attrs)
    changeset = user.cast(attrs, [:username, :password, :password_confirm])
    changeset.validate_required(:username, :password, :password_confirm)
    changeset.validate_matches(:password, :password_confirm)
    changeset
  end

  # When updating a User, they are not allowed to change their username, so
  # only cast and validate the password changes.
  def update_changeset(user : self, attrs)
    changeset = user.cast(attrs, [:password, :password_confirm])
    changeset.validate_matches(:password, :password_confirm)
    changeset
  end
end

The inspiration for this comes from reading this article on Ecto's Changesets, seeing this presentation at ElixirConf about breaking down User monoliths, and applying the multi-changeset pattern in real world applications. A large benefit of this style is being able to create different changesets for different purposes and really lock down what valid operations are for a given model.

 

It's a little verbose to keep writing changeset... for each validation line. There's the easy change of renaming the variable something like c, but another options would be adding another changeset method to the Model that accepts a block and acts a little like tap, returning the changeset at the end. Something like this:

def create_changeset(user : self, attrs)
  changeset(user) do |c|
    c.cast(attrs, [:username, :password, :password_confirm])
    c.validate_required(:username, :password, :password_confirm)
    c.validate_matches(:password, :password_confirm)
  end
end

faultyserver avatar Sep 15 '18 17:09 faultyserver

Looking through this more, I think this also has some implications about how changesets in Crecto work.

Right now, as I understand it, Crecto's changesets work in an after-the-fact matter, where changes are made to a model directly, and then the changeset figures out what has been changed and works from there.

case and these changeset-level validations would realistically have an inverse model, where the changeset is responsible for keeping track of changes to apply to the model from the start, and the original instance doesn't change until the changeset gets applied to the instance. (Ecto implements this as apply_changes/1).

Personally, I think I would like to see that be the standard for working with changesets in Crecto for a few reasons:

  • it better matches Ecto's original pattern.
  • it reduces how invasive Crecto has to be on the model classes (can probably get rid of initial_values on Crecto::Model, all the validate_* methods can be taken out of the model's namespace, etc.)
  • it segregates making changes to a model and saving those changes. For example, since the instance itself isn't changed until the changeset is applied, it's easy to abort the changes if they are invalid.
  • it would allow changesets to easily be used on arbitrary objects. Right now (I think) changeset depends on initial_values as defined in Crecto::Model. Since that wouldn't exist anymore, Changeset would become an independent module usable anywhere.

Anyway, those are just some thoughts I've had while messing around with an implementation of this changeset-level validation stuff.

faultyserver avatar Sep 16 '18 02:09 faultyserver

You are correct, currently changesets are more of an after-the-fact thing and not as integrated as Ectos. I am totally on board with everything you've described.

Unfortunately I have been super busy the past 6+ months and havent had any time to contribute much to any OSS.

fridgerator avatar Sep 16 '18 04:09 fridgerator

I've been in the same boat for quite a while as well and have just started getting back into it. I'm in a bit of a time crunch until the 30th, but I'm hoping after that I'll have some time to really flesh this out.

.....or maybe sooner if it becomes something really helpful for me with this project.

faultyserver avatar Sep 17 '18 13:09 faultyserver