Skip to content

Commit ea286d3

Browse files
committed
Add conditional normalizes support to Solid::Model
1 parent 7b574a4 commit ea286d3

File tree

5 files changed

+123
-0
lines changed

5 files changed

+123
-0
lines changed

docs/040_ADVANCED_USAGE.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,77 @@ class User::Registration < Solid::Process
9494
end
9595
end
9696
```
97+
98+
## Input Normalization
99+
100+
### Using `before_validation` (all Rails versions)
101+
102+
The example above uses `before_validation` to normalize the email attribute. This approach works on all supported Rails versions, but normalization only runs when `valid?` is called:
103+
104+
```ruby
105+
input do
106+
attribute :email, :string
107+
108+
before_validation do
109+
self.email = email.downcase.strip
110+
end
111+
end
112+
```
113+
114+
### Using `normalizes` (Rails 8.1+)
115+
116+
Rails 8.1 introduced `ActiveModel::Attributes::Normalization`, which provides a declarative way to normalize attributes. When available, `Solid::Model` automatically includes it.
117+
118+
Unlike `before_validation`, `normalizes` applies on attribute assignment, so the value is normalized immediately:
119+
120+
```ruby
121+
input do
122+
attribute :email, :string
123+
124+
normalizes :email, with: -> { _1.strip.downcase }
125+
end
126+
```
127+
128+
#### Normalize multiple attributes with the same rule
129+
130+
```ruby
131+
input do
132+
attribute :email, :string
133+
attribute :username, :string
134+
135+
normalizes :email, :username, with: -> { _1.strip.downcase }
136+
end
137+
```
138+
139+
#### Different normalizations per attribute
140+
141+
```ruby
142+
input do
143+
attribute :email, :string
144+
attribute :phone, :string
145+
146+
normalizes :email, with: -> { _1.strip.downcase }
147+
normalizes :phone, with: -> { _1.delete("^0-9").delete_prefix("1") }
148+
end
149+
```
150+
151+
#### Apply normalization to `nil` values
152+
153+
By default, `normalizes` skips `nil` values. Use `apply_to_nil: true` to change this:
154+
155+
```ruby
156+
input do
157+
attribute :phone, :string
158+
159+
normalizes :phone, with: -> { _1&.delete("^0-9") || "" }, apply_to_nil: true
160+
end
161+
```
162+
163+
#### Normalize a value without instantiation
164+
165+
Use `normalize_value_for` on the input class to normalize a value directly:
166+
167+
```ruby
168+
UserRegistration.input.normalize_value_for(:email, " [email protected]\n")
169+
170+
```

lib/solid/model.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ def inherited(subclass)
2020
include ::ActiveModel.const_defined?(:Access, false) ? ::ActiveModel::Access : ::Solid::Model::Access
2121

2222
include ::ActiveModel::Attributes
23+
24+
if ::ActiveModel::Attributes.const_defined?(:Normalization, false)
25+
include ::ActiveModel::Attributes::Normalization
26+
end
27+
2328
include ::ActiveModel::Dirty
2429
include ::ActiveModel::Validations::Callbacks
2530

lib/solid/value.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def attribute(...)
1717
def validates(...)
1818
super(:value, ...)
1919
end
20+
21+
def normalizes(...)
22+
super(:value, ...)
23+
end
2024
end
2125

2226
def self.included(subclass)

test/solid/model/class_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,30 @@ class ModelWithValidation
7878
assert_includes ancestors, Solid::Model::Access
7979
end
8080

81+
if ActiveModel::Attributes.const_defined?(:Normalization, false)
82+
assert_includes ancestors, ActiveModel::Attributes::Normalization
83+
end
84+
8185
assert_includes ancestors, ActiveModel::Attributes
8286
assert_includes ancestors, ActiveModel::Dirty
8387
assert_includes ancestors, ActiveModel::Validations::Callbacks
8488
end
89+
90+
if ActiveModel::Attributes.const_defined?(:Normalization, false)
91+
class ModelWithNormalizes
92+
include Solid::Model
93+
94+
attribute :uuid, :string
95+
96+
normalizes :uuid, with: -> { _1.strip.downcase }
97+
end
98+
99+
test "model with normalizes" do
100+
uuid = SecureRandom.uuid
101+
102+
model = ModelWithNormalizes.new(uuid: " #{uuid.upcase} ")
103+
104+
assert_equal uuid, model.uuid
105+
end
106+
end
85107
end

test/solid/value/class_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,22 @@ class ValueWithValidation
5858

5959
assert_predicate uuid4, :valid?
6060
end
61+
62+
if ActiveModel::Attributes.const_defined?(:Normalization, false)
63+
class ValueWithNormalizes
64+
include Solid::Value
65+
66+
attribute :string
67+
68+
normalizes with: -> { _1.strip.downcase }
69+
end
70+
71+
test "value with normalizes" do
72+
uuid_str = SecureRandom.uuid
73+
74+
uuid = ValueWithNormalizes.new(" #{uuid_str.upcase} ")
75+
76+
assert_equal uuid_str, uuid.value
77+
end
78+
end
6179
end

0 commit comments

Comments
 (0)