value_objects 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fcf100e50deeb0be95ee828543bebc1271b8ccfe
4
+ data.tar.gz: eb2293813317264b2fcbdfcae89feaa7a8f48298
5
+ SHA512:
6
+ metadata.gz: 650a0c71dcba21c4ea3774471de37a7b3937319c73d80ae411a396e520dca12bcc2fd7c6570f22791bc6e42add3016f10a3c024b1441195712fc0e5fc4654734
7
+ data.tar.gz: ed81e88aa8436eb2d76ba35f87346ff55643573cf27726828e3729f1568f388cf072694db9c1267e16caa6055a473cb52d1b3caf7a5cd30e4b058a94e62a01a5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Matthew Yeow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # ValueObjects
2
+
3
+ Serializable and validatable value objects for ActiveRecord
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'value_objects'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install value_objects
20
+
21
+ ## Usage
22
+
23
+ ### Create the value object class
24
+
25
+ The value object class inherits from `ValueObjects::Base`, and attributes are defined with `attr_accessor`:
26
+
27
+ ```ruby
28
+ class AddressValue < ValueObjects::Base
29
+ attr_accessor :street, :postcode, :city
30
+ end
31
+
32
+ address = AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
33
+ address.street # => "123 Big Street"
34
+ address.street = '321 Main St' # => "321 Main St"
35
+ address.to_hash # => {:street=>"321 Main St", :postcode=>"12345", :city=>"Metropolis"}
36
+ ```
37
+
38
+ ### Add validations
39
+
40
+ Validations can be added using the DSL from `ActiveModel::Validations`:
41
+
42
+ ```ruby
43
+ class AddressValue < ValueObjects::Base
44
+ attr_accessor :street, :postcode, :city
45
+ validates :postcode, presence: true
46
+ end
47
+
48
+ address = AddressValue.new(street: '123 Big Street', city: 'Metropolis')
49
+ address.valid? # => false
50
+ address.errors.to_h # => {:postcode=>"can't be blank"}
51
+ address.postcode = '12345' # => "12345"
52
+ address.valid? # => true
53
+ address.errors.to_h # => {}
54
+ ```
55
+
56
+ ### Serialization with ActiveRecord
57
+
58
+ For columns of `json` type, the value object class can be used as the coder for the `serialize` method:
59
+
60
+ ```ruby
61
+ class Customer < ActiveRecord::Base
62
+ serialize :home_address, AddressValue
63
+ end
64
+
65
+ customer = Customer.new
66
+ customer.home_address = AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
67
+ customer.save
68
+ customer.reload
69
+ customer.home_address # => #<AddressValue:0x00ba9876543210 @street="123 Big Street", @postcode="12345", @city="Metropolis">
70
+ ```
71
+
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
+
80
+ ### Validation with ActiveRecord
81
+
82
+ By default, validating the record does not automatically validate the value object.
83
+ Use the `ValueObjects::ValidValidator` to make this automatic:
84
+
85
+ ```ruby
86
+ class Customer < ActiveRecord::Base
87
+ serialize :home_address, AddressValue
88
+ validates :home_address, 'value_objects/valid': true
89
+ validates :home_address, presence: true # other validations are allowed too
90
+ end
91
+
92
+ customer = Customer.new
93
+ customer.home_address = AddressValue.new(street: '123 Big Street', city: 'Metropolis')
94
+ customer.valid? # => false
95
+ customer.errors.to_h # => {:home_address=>"is invalid"}
96
+ customer.home_address.errors.to_h # => {:postcode=>"can't be blank"}
97
+ customer = Customer.new
98
+ customer.valid? # => false
99
+ customer.errors.to_h # => {:home_address=>"can't be blank"}
100
+ ```
101
+
102
+ ### With `ValueObjects::ActiveRecord`
103
+
104
+ For easy set up of both serialization and validation, `include ValueObjects::ActiveRecord` and invoke `value_object`:
105
+
106
+ ```ruby
107
+ class Customer < ActiveRecord::Base
108
+ include ValueObjects::ActiveRecord
109
+ value_object :home_address, AddressValue
110
+ validates :home_address, presence: true
111
+ end
112
+ ```
113
+
114
+ This basically works the same way but also defines the `<attribute>_attributes=` method which can be used to assign the value object using a hash:
115
+
116
+ ```ruby
117
+ customer.home_address_attributes = { street: '321 Main St', postcode: '54321', city: 'Micropolis' }
118
+ customer.home_address # => #<AddressValue:0x00ba9876503210 @street="321 Main St", @postcode="54321", @city="Micropolis">
119
+ ```
120
+
121
+ This is functionally similar to what `accepts_nested_attributes_for` does for associations.
122
+
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
+
143
+ ### Value object collections
144
+
145
+ Serialization and validation of value object collections are also supported.
146
+
147
+ First, create a nested `Collection` class that inherits from `ValueObjects::Base::Collection`:
148
+
149
+ ```ruby
150
+ class AddressValue < ValueObjects::Base
151
+ attr_accessor :street, :postcode, :city
152
+ validates :postcode, presence: true
153
+
154
+ class Collection < Collection
155
+ end
156
+ end
157
+ ```
158
+
159
+ Then use the nested `Collection` class as the serialization coder:
160
+
161
+ ```ruby
162
+ class Customer < ActiveRecord::Base
163
+ include ValueObjects::ActiveRecord
164
+ value_object :addresses, AddressValue::Collection
165
+ validates :addresses, presence: true
166
+ end
167
+
168
+ customer = Customer.new(addresses: [])
169
+ customer.valid? # => false
170
+ customer.errors.to_h # => {:addresses=>"can't be blank"}
171
+ customer.addresses << AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
172
+ customer.valid? # => true
173
+ customer.addresses << AddressValue.new(street: '321 Main St', city: 'Micropolis')
174
+ customer.valid? # => false
175
+ customer.errors.to_h # => {:addresses=>"is invalid"}
176
+ customer.addresses[1].errors.to_h # => {:postcode=>"can't be blank"}
177
+ ```
178
+
179
+ The `<attribute>_attributes=` method also functions in much the same way:
180
+
181
+ ```ruby
182
+ customer.addresses_attributes = { '0' => { city: 'Micropolis' }, '1' => { city: 'Metropolis' } }
183
+ customer.addresses # => [#<AddressValue:0x00ba9876543210 @city="Micropolis">, #<AddressValue:0x00ba9876503210 @city="Metropolis">]
184
+ ```
185
+
186
+ Except, items with '-1' keys are considered as dummy items and ignored:
187
+
188
+ ```ruby
189
+ customer.addresses_attributes = { '0' => { city: 'Micropolis' }, '-1' => { city: 'Metropolis' } }
190
+ customer.addresses # => [#<AddressValue:0x00ba9876543210 @city="Micropolis">]
191
+ ```
192
+
193
+ This is useful when data is submitted via standard HTML forms encoded with the 'application/x-www-form-urlencoded' media type (which cannot represent empty collections). To work around this, a dummy item can be added to the collection with it's key set to '-1' and it will conveniently be ignored when assigned to the value object collection.
194
+
195
+ ### Integrate with Cocoon
196
+
197
+ Put this into a Rails initializer (e.g. `config/initializers/value_objects.rb`):
198
+
199
+ ```ruby
200
+ ValueObjects::ActionView.integrate_with :cocoon
201
+ ```
202
+
203
+ This will add the `link_to_add_nested_value` & `link_to_remove_nested_value` view helpers.
204
+ Use them in place of Cocoon's `link_to_add_association` & `link_to_remove_association` when working with nested value objects:
205
+
206
+ ```ruby
207
+ # use the attribute name (:addresses) in place of the association name
208
+ # and supply the value object class as the next argument
209
+ link_to_add_nested_value 'Add Address', f, :addresses, AddressValue
210
+
211
+ # the `f` form builder argument is not needed
212
+ link_to_remove_nested_value 'Remove Address'
213
+ ```
214
+
215
+ ## Maintainers
216
+
217
+ * Matthew Yeow (https://github.com/tbsmatt), Tinkerbox Studios (https://www.tinkerbox.com.sg/)
218
+
219
+ ## Contributing
220
+
221
+ * Fork the repository.
222
+ * Make your feature addition or bug fix.
223
+ * Add tests for it. This is important so we don't break it in a future version unintentionally.
224
+ * Commit, but do not mess with rakefile or version. (if you want to have your own version, that is fine but bump version in a commit by itself)
225
+ * Submit a pull request. Bonus points for topic branches.
226
+
227
+ ## License
228
+
229
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
230
+
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+
4
+ module ActionView
5
+
6
+ module Cocoon
7
+
8
+ def link_to_add_nested_value(name, f, attribute, value_class, html_options = {}, &block)
9
+ render_options = html_options.delete(:render_options) || {}
10
+ partial = html_options.delete(:partial)
11
+ wrap_object = html_options.delete(:wrap_object)
12
+ form_name = html_options.delete(:form_name) || 'f'
13
+ count = html_options.delete(:count).to_i
14
+ new_object = value_class.new
15
+ new_object = wrap_object.call(new_object) if wrap_object
16
+
17
+ html_options[:class] = [html_options[:class], 'add_fields'].compact.join(' ')
18
+ html_options[:'data-association'] = attribute
19
+ html_options[:'data-association-insertion-template'] = CGI.escapeHTML(render_association(attribute, f, new_object, form_name, render_options, partial)).html_safe
20
+ html_options[:'data-count'] = count if count > 0
21
+
22
+ name = capture(&block) if block_given?
23
+ dummy_input = tag(:input, type: 'hidden', name: "#{f.object_name}[#{attribute}_attributes][-1][_]")
24
+ content_tag(:a, name, html_options) + dummy_input
25
+ end
26
+
27
+ def link_to_remove_nested_value(name, html_options = {}, &block)
28
+ wrapper_class = html_options.delete(:wrapper_class)
29
+
30
+ html_options[:class] = [html_options[:class], 'remove_fields dynamic'].compact.join(' ')
31
+ html_options[:'data-wrapper-class'] = wrapper_class if wrapper_class
32
+
33
+ name = capture(&block) if block_given?
34
+ content_tag(:a, name, html_options)
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ ActionView::Base.send(:include, ValueObjects::ActionView::Cocoon)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+
4
+ module ActionView
5
+
6
+ def self.integrate_with(library)
7
+ require "value_objects/action_view/#{library}"
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+
4
+ module ActiveRecord
5
+
6
+ def self.included(base)
7
+ base.extend self
8
+ end
9
+
10
+ def value_object(attribute, value_class, options = {})
11
+ type =
12
+ begin
13
+ column_for_attribute(attribute)&.type
14
+ rescue ::ActiveRecord::StatementInvalid
15
+ # This can happen if `column_for_attribute` is called but the database table does not exist
16
+ # as will be the case when migrations are run and the model class is loaded by initializers
17
+ # before the table is created.
18
+ # This is a workaround to prevent such migrations from failing.
19
+ nil
20
+ end
21
+ coder =
22
+ case type
23
+ when :string, :text
24
+ JsonCoder.new(value_class)
25
+ else
26
+ value_class
27
+ end
28
+ serialize(attribute, coder)
29
+ validates_with(::ValueObjects::ValidValidator, options.merge(attributes: [attribute])) unless options[:no_validation]
30
+ setter = :"#{attribute}="
31
+ define_method("#{attribute}_attributes=") do |attributes|
32
+ send(setter, value_class.new(attributes))
33
+ end
34
+ end
35
+
36
+ class JsonCoder
37
+
38
+ EMPTY_ARRAY = [].freeze
39
+
40
+ def initialize(value_class)
41
+ @value_class = value_class
42
+ end
43
+
44
+ def load(value)
45
+ @value_class.load(JSON.load(value) || EMPTY_ARRAY) if value
46
+ end
47
+
48
+ def dump(value)
49
+ value.to_json if value
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+
4
+ class Base
5
+
6
+ include ::ActiveModel::Model
7
+
8
+ def ==(other)
9
+ self.class == other.class && self.class.attrs.all? { |key| public_send(key) == other.public_send(key) }
10
+ end
11
+
12
+ def to_hash
13
+ self.class.attrs.each_with_object({}) { |key, hash| hash[key] = public_send(key) }
14
+ end
15
+
16
+ class << self
17
+
18
+ def load(value)
19
+ new(value) if value
20
+ end
21
+
22
+ def dump(value)
23
+ value.to_hash if value
24
+ end
25
+
26
+ def i18n_scope
27
+ :value_objects
28
+ end
29
+
30
+ attr_reader :attrs
31
+
32
+ private
33
+
34
+ def attr_accessor(*args)
35
+ (@attrs ||= []).concat(args)
36
+ super(*args)
37
+ end
38
+
39
+ end
40
+
41
+ class Collection
42
+
43
+ class << self
44
+
45
+ def inherited(subclass)
46
+ subclass.instance_variable_set(:@value_class, subclass.parent)
47
+ end
48
+
49
+ def new(attributes)
50
+ # Data encoded with the 'application/x-www-form-urlencoded' media type cannot represent empty collections.
51
+ # As a workaround, a dummy item can be added to the collection with it's key set to '-1'.
52
+ # This dummy item will be ignored when initializing the value collection.
53
+ attributes.map { |k, v| @value_class.new(v) if k != '-1' }.compact
54
+ end
55
+
56
+ def load(values)
57
+ (values.blank? ? [] : values.map { |value| @value_class.new(value) }) if values
58
+ end
59
+
60
+ def dump(values)
61
+ values.map(&:to_hash) if values
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+
4
+ class ValidValidator < ActiveModel::EachValidator
5
+
6
+ def validate_each(record, attribute, value)
7
+ record.errors.add(attribute, :invalid) unless value && Array(value).count(&:invalid?) == 0
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module ValueObjects
3
+ VERSION = '0.1.0'
4
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record'
3
+ require 'value_objects/version'
4
+ require 'value_objects/base'
5
+ require 'value_objects/valid_validator'
6
+ require 'value_objects/active_record'
7
+ require 'value_objects/action_view'
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: value_objects
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Yeow
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: actionview
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ description:
98
+ email:
99
+ - matthew.yeow@tinkerbox.com.sg
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - LICENSE.txt
105
+ - README.md
106
+ - lib/value_objects.rb
107
+ - lib/value_objects/action_view.rb
108
+ - lib/value_objects/action_view/cocoon.rb
109
+ - lib/value_objects/active_record.rb
110
+ - lib/value_objects/base.rb
111
+ - lib/value_objects/valid_validator.rb
112
+ - lib/value_objects/version.rb
113
+ homepage: https://github.com/tinkerbox/value_objects
114
+ licenses:
115
+ - MIT
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '2.2'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 2.5.1
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Serializable and validatable value objects for ActiveRecord
137
+ test_files: []