Skip to content

Commit de4fd94

Browse files
committed
Merge pull request #3 from tinkerbox/feature/optional_validation_and_serialization
Optional Validation and Serialization
2 parents 67384f7 + 12f1874 commit de4fd94

File tree

5 files changed

+132
-32
lines changed

5 files changed

+132
-32
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ address.errors.to_h # => {}
5555

5656
### Serialization with ActiveRecord
5757

58-
The value object class can be used as the coder for the `serialize` method:
58+
For columns of `json` type, the value object class can be used as the coder for the `serialize` method:
5959

6060
```ruby
6161
class Customer < ActiveRecord::Base
@@ -69,6 +69,14 @@ customer.reload
6969
customer.home_address # => #<AddressValue:0x00ba9876543210 @street="123 Big Street", @postcode="12345", @city="Metropolis">
7070
```
7171

72+
For columns of `string` or `text` type, wrap the value object class in a `JsonCoder`:
73+
74+
```ruby
75+
class Customer < ActiveRecord::Base
76+
serialize :home_address, ValueObjects::ActiveRecord::JsonCoder.new(AddressValue)
77+
end
78+
```
79+
7280
### Validation with ActiveRecord
7381

7482
By default, validating the record does not automatically validate the value object.
@@ -112,6 +120,26 @@ customer.home_address # => #<AddressValue:0x00ba9876503210 @street="321 Main St"
112120

113121
This is functionally similar to what `accepts_nested_attributes_for` does for associations.
114122

123+
Also, `value_object` will use the `JsonCoder` automatically if it detects that the column type is `string` or `text`.
124+
125+
Additional options may be passed in to customize validation:
126+
127+
```ruby
128+
class Customer < ActiveRecord::Base
129+
include ValueObjects::ActiveRecord
130+
value_object :home_address, AddressValue, allow_nil: true
131+
end
132+
```
133+
134+
Or, to skip validation entirely:
135+
136+
```ruby
137+
class Customer < ActiveRecord::Base
138+
include ValueObjects::ActiveRecord
139+
value_object :home_address, AddressValue, no_validation: true
140+
end
141+
```
142+
115143
### Value object collections
116144

117145
Serialization and validation of value object collections are also supported.

lib/value_objects/active_record.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,40 @@ def self.included(base)
77
base.extend self
88
end
99

10-
def value_object(attribute, value_class)
11-
serialize(attribute, value_class)
12-
validates_with(::ValueObjects::ValidValidator, attributes: [attribute])
10+
def value_object(attribute, value_class, options = {})
11+
coder =
12+
case column_for_attribute(attribute).type
13+
when :string, :text
14+
JsonCoder.new(value_class)
15+
else
16+
value_class
17+
end
18+
serialize(attribute, coder)
19+
validates_with(::ValueObjects::ValidValidator, options.merge(attributes: [attribute])) unless options[:no_validation]
1320
setter = :"#{attribute}="
1421
define_method("#{attribute}_attributes=") do |attributes|
1522
send(setter, value_class.new(attributes))
1623
end
1724
end
1825

26+
class JsonCoder
27+
28+
EMPTY_ARRAY = [].freeze
29+
30+
def initialize(value_class)
31+
@value_class = value_class
32+
end
33+
34+
def load(value)
35+
@value_class.load(JSON.load(value) || EMPTY_ARRAY) if value
36+
end
37+
38+
def dump(value)
39+
value.to_json if value
40+
end
41+
42+
end
43+
1944
end
2045

2146
end

lib/value_objects/value.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ def to_hash
1717
class << self
1818

1919
def load(value)
20-
new(JSON.load(value)) if value
20+
new(value) if value
2121
end
2222

2323
def dump(value)
24-
value.to_json if value
24+
value.to_hash if value
2525
end
2626

2727
def i18n_scope
@@ -55,11 +55,11 @@ def new(attributes)
5555
end
5656

5757
def load(values)
58-
values.blank? ? [] : JSON.load(values).map { |value| @value_class.new(value) } if values
58+
(values.blank? ? [] : values.map { |value| @value_class.new(value) }) if values
5959
end
6060

6161
def dump(values)
62-
values.to_json if values
62+
values.map(&:to_hash) if values
6363
end
6464

6565
end

spec/value_objects/active_record_spec.rb

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
self.verbose = false
55

66
create_table :test_records do |t|
7-
t.string :value
8-
t.string :values
7+
t.string :value, default: ''
8+
t.string :value1
9+
t.string :values, default: ''
10+
t.string :values1
911
end
1012

1113
end
@@ -24,36 +26,81 @@ class TestRecord < ActiveRecord::Base
2426

2527
include ValueObjects::ActiveRecord
2628

27-
value_object :value, FooBarValue
28-
value_object :values, FooBarValue::Collection
29+
value_object :value, FooBarValue, allow_nil: true
30+
value_object :value1, FooBarValue, no_validation: true
31+
value_object :values, FooBarValue::Collection, allow_nil: true
32+
value_object :values1, FooBarValue::Collection, no_validation: true
2933

3034
end
3135

3236
describe 'serialization' do
3337

34-
let(:value) { FooBarValue.new(foo: '123', bar: 'abc') }
35-
let(:values) { [FooBarValue.new(foo: 'abc', bar: '123'), FooBarValue.new(foo: 'cba', bar: 321)] }
36-
let(:record) { TestRecord.create!(value: value, values: values).reload }
38+
context 'with non-nil values' do
3739

38-
it 'serializes the value object' do
39-
aggregate_failures do
40-
expect(record.read_attribute_before_type_cast(:value)).to eq('{"foo":"123","bar":"abc"}')
41-
expect(record.read_attribute_before_type_cast(:values)).to eq('[{"foo":"abc","bar":"123"},{"foo":"cba","bar":321}]')
40+
let(:value) { FooBarValue.new(foo: '123', bar: 'abc') }
41+
let(:values) { [FooBarValue.new(foo: 'abc', bar: '123'), FooBarValue.new(foo: 'cba', bar: 321)] }
42+
let(:record) { TestRecord.create!(value: value, values: values).reload }
43+
44+
it 'serializes the value object' do
45+
aggregate_failures do
46+
expect(record.read_attribute_before_type_cast(:value)).to eq('{"foo":"123","bar":"abc"}')
47+
expect(record.read_attribute_before_type_cast(:values)).to eq('[{"foo":"abc","bar":"123"},{"foo":"cba","bar":321}]')
48+
end
49+
end
50+
51+
it 'deserializes the value object' do
52+
aggregate_failures do
53+
expect(record.read_attribute(:value)).to eq(value)
54+
expect(record.read_attribute(:values)).to eq(values)
55+
end
4256
end
57+
4358
end
4459

45-
it 'deserializes the value object' do
46-
aggregate_failures do
47-
expect(record.read_attribute(:value)).to eq(value)
48-
expect(record.read_attribute(:values)).to eq(values)
60+
context 'with nil values' do
61+
62+
let(:value) { nil }
63+
let(:values) { nil }
64+
let(:record) { TestRecord.create!(value: value, values: values).reload }
65+
66+
it 'serializes the value object' do
67+
aggregate_failures do
68+
expect(record.read_attribute_before_type_cast(:value)).to eq(nil)
69+
expect(record.read_attribute_before_type_cast(:values)).to eq(nil)
70+
end
4971
end
72+
73+
it 'deserializes the value object' do
74+
aggregate_failures do
75+
expect(record.read_attribute(:value)).to eq(value)
76+
expect(record.read_attribute(:values)).to eq(values)
77+
end
78+
end
79+
80+
end
81+
82+
context 'with blank values' do
83+
84+
let(:value) { FooBarValue.new }
85+
let(:values) { [] }
86+
let(:record) { TestRecord.new.tap { |r| r.save!(validate: false) }.reload }
87+
88+
it 'deserializes the value object' do
89+
aggregate_failures do
90+
expect(record.read_attribute_before_type_cast(:value)).to eq('')
91+
expect(record.read_attribute_before_type_cast(:values)).to eq('')
92+
expect(record.read_attribute(:value)).to eq(value)
93+
expect(record.read_attribute(:values)).to eq(values)
94+
end
95+
end
96+
5097
end
5198

5299
end
53100

54101
describe 'validation' do
55102

56-
let(:record) { TestRecord.new(value: value, values: values).tap(&:valid?) }
103+
let(:record) { TestRecord.new(value: value, value1: value, values: values, values1: values).tap(&:valid?) }
57104

58105
context 'with valid values' do
59106

spec/value_objects/value_spec.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ class Collection < Collection
5656

5757
context 'with empty value' do
5858

59-
let(:value) { TestValue.load('') }
59+
let(:value) { TestValue.load({}) }
6060

6161
it { expect(value).to eq(TestValue.new) }
6262

6363
end
6464

6565
context 'with non-empty value' do
6666

67-
let(:value) { TestValue.load('{"foo":321,"bar":"cba"}') }
67+
let(:value) { TestValue.load('foo' => 321, 'bar' => 'cba') }
6868

6969
it { expect(value).to eq(TestValue.new(foo: 321, bar: 'cba')) }
7070

@@ -86,15 +86,15 @@ class Collection < Collection
8686

8787
let(:value) { TestValue.dump(TestValue.new) }
8888

89-
it { expect(value).to eq('{"foo":null,"bar":null}') }
89+
it { expect(value).to eq(foo: nil, bar: nil) }
9090

9191
end
9292

9393
context 'with non-empty value' do
9494

9595
let(:value) { TestValue.dump(TestValue.new(foo: 321, bar: 'cba')) }
9696

97-
it { expect(value).to eq('{"foo":321,"bar":"cba"}') }
97+
it { expect(value).to eq(foo: 321, bar: 'cba') }
9898

9999
end
100100

@@ -146,15 +146,15 @@ class Collection < Collection
146146

147147
context 'with empty value' do
148148

149-
let(:value) { TestValue::Collection.load('') }
149+
let(:value) { TestValue::Collection.load([]) }
150150

151151
it { expect(value).to eq([]) }
152152

153153
end
154154

155155
context 'with non-empty value' do
156156

157-
let(:value) { TestValue::Collection.load('[{"foo":321,"bar":"cba"},{"foo":"abc","bar":123}]') }
157+
let(:value) { TestValue::Collection.load([{ 'foo' => 321, 'bar' => 'cba' }, { 'foo' => 'abc', 'bar' => 123 }]) }
158158

159159
it { expect(value).to eq([TestValue.new(foo: 321, bar: 'cba'), TestValue.new(foo: 'abc', bar: 123)]) }
160160

@@ -176,15 +176,15 @@ class Collection < Collection
176176

177177
let(:values) { TestValue::Collection.dump([]) }
178178

179-
it { expect(values).to eq('[]') }
179+
it { expect(values).to eq([]) }
180180

181181
end
182182

183183
context 'with non-empty array' do
184184

185185
let(:values) { TestValue::Collection.dump([TestValue.new(foo: 123, bar: 'abc'), TestValue.new(foo: 'cba', bar: 321)]) }
186186

187-
it { expect(values).to eq('[{"foo":123,"bar":"abc"},{"foo":"cba","bar":321}]') }
187+
it { expect(values).to eq([{ foo: 123, bar: 'abc' }, { foo: 'cba', bar: 321 }]) }
188188

189189
end
190190

0 commit comments

Comments
 (0)