strip_attributes icon indicating copy to clipboard operation
strip_attributes copied to clipboard

Consider using the Attributes API instead of before_validation callbacks

Open rmm5t opened this issue 6 years ago • 9 comments

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

rmm5t avatar Sep 09 '19 22:09 rmm5t

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". ;)

ravicious avatar Nov 26 '19 09:11 ravicious

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 avatar Mar 29 '21 07:03 Hampei

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

Hampei avatar Mar 29 '21 07:03 Hampei

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?

jrochkind avatar Jan 05 '23 20:01 jrochkind

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.

justinko avatar Jan 05 '23 21:01 justinko

@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!

jrochkind avatar Jan 05 '23 22:01 jrochkind

Whoops, I got the name wrong!

https://github.com/rails/rails/pull/43945

justinko avatar Jan 06 '23 02:01 justinko

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.

petrgazarov avatar Jan 07 '23 21:01 petrgazarov

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.

jrochkind avatar Jan 07 '23 23:01 jrochkind