Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider using the Attributes API instead of before_validation callbacks #43

Open
rmm5t opened this issue Sep 9, 2019 · 9 comments
Open
Labels

Comments

@rmm5t
Copy link
Owner

rmm5t commented Sep 9, 2019

Related to #34.

Investigate and perhaps consider using the new attributes API instead of a before_validation callback.

class Strippable < ActiveRecord::Type::String
  def cast_value(value)
    value.to_s.strip
  end
end

class Something < ActiveRecord::Base
  attribute :title, Strippable
end
@ravicious
Copy link

Sounds like a good idea! I saw the discussion in #34 about using setters, I think whatever you'll end up with will be better than before_validation. The pain with before_validation is that I have to trigger the callbacks before I'm able to use a stripped version of an attribute.

I'm not sure how exactly the attributes API works, but it might give the option to use read_attribute_before_type_cast to access the pristine version of an attribute. Though I'd worry that using the attributes API would prevent me from using another type on a given attribute, but I assume that's why the issue says "investigate". ;)

@Hampei
Copy link

Hampei commented Mar 29, 2021

Just made my own for this, since the current implementation is doing in place string manipulation, which won't work in ruby 3. I only overwrote cast, since I don't want it to run when loading from the database.

On using multiple filters: it's really easy to create a wrapper type, that calls them both. So I would leave that the application.

# frozen_string_literal: true

# Strips whitespace and make nil when string becomes empty.
class StrippedString < ActiveModel::Type::ImmutableString
  # taken from strip_attributes gem
  # Unicode invisible and whitespace characters. The POSIX character class
  # [:space:] corresponds to the Unicode class Z ("separator"). We also
  # include the following characters from Unicode class C ("control"), which
  # are spaces or invisible characters that make no sense at the start or end
  # of a string:
  #   U+180E MONGOLIAN VOWEL SEPARATOR
  #   U+200B ZERO WIDTH SPACE
  #   U+200C ZERO WIDTH NON-JOINER
  #   U+200D ZERO WIDTH JOINER
  #   U+2060 WORD JOINER
  #   U+FEFF ZERO WIDTH NO-BREAK SPACE
  MULTIBYTE_WHITE = "\u180E\u200B\u200C\u200D\u2060\uFEFF"
  MULTIBYTE_SPACE = /[[:space:]#{MULTIBYTE_WHITE}]/.freeze

  def initialize(allow_empty: false)
    super()
    @allow_empty = allow_empty
  end

  def cast(value)
    return unless value
    value = super(value).gsub(/\A#{MULTIBYTE_SPACE}+|#{MULTIBYTE_SPACE}+\z/, "").freeze
    value.blank? && !@allow_empty ? nil : value
  end
end

@Hampei
Copy link

Hampei commented Mar 29, 2021

Note that in your example you need to either use:

class Something < ActiveRecord::Base
  attribute :title, Strippable.new(allow_empty: true)
end

or add this to an initializer

ActiveRecord::Type.register(:stripped_string, StrippedString)

and use it like this:

class Something < ActiveRecord::Base
  attribute :title, :stripped_string, allow_empty: true
end

@rmm5t rmm5t added the ideas label May 13, 2022
@jrochkind
Copy link

Are you still interested in this? I'm thinking about it. i (or someone else) could potentially PR... it's potentially fairly simple code; then again, we could also easily copy/paste fork (thanks for MIT license) your logic into an attribute-based thing in another gem, the code here is pretty simple.

Some features aren't really available anymore with an ActiveModel-attribute-based implementation. Like the if/unless stuff -- inside attribute casting, you don't have access to the containing record to make decisions based on it's state. And you have a bit more cumbersome API to specify, as you need to write an attribute wrapper for each attribute, instead of just easily writing strip_attributes (possibly with only and/or except).

So... it's got plusses and minuses. Maybe it's really a separate gem.

Would you care to update with your current thinking or plans on things?

@justinko
Copy link

justinko commented Jan 5, 2023

Looking forward to this gem using the new normalize Rails method. But not sure if this gem even needs to exist because of it ... will be so easy to write your own wrapper.

@jrochkind
Copy link

@justinko "the new normalize Rails method" -- I haven't heard of that, and am not having luck googling for it. This is something built into rails, not a third-party something? Can you give me a link to something? Thanks!

@justinko
Copy link

justinko commented Jan 6, 2023

Whoops, I got the name wrong!

rails/rails#43945

@petrgazarov
Copy link

One major difference is that:

Normalizations are also applied to matching keyword arguments of finder methods. This allows a record to be created and later queried using an unnormalized value.

I don't think I would always want that. For example, if normalization is converting the attribute from "" -> nil, then going forward, the finder methods would match the record by both "" and nil.

@jrochkind
Copy link

Oh, that is very interesting, thanks @justinko .

It does look like a generalized approach to what is being discussed here.

@petrgazarov , I see what you mean, but any appraoch based on the Rails Attributes API is probably going to work like this. Because Rails will always cast and serialize the value, per the type, when constructing the SQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants