deco_lite 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/README.md +56 -59
- data/lib/deco_lite/hash_loadable.rb +2 -4
- data/lib/deco_lite/model.rb +2 -2
- data/lib/deco_lite/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5e504d4b1c9442022ff1d9dc1727f1116b3be5bef76cce7c3f0b4373e1d5531
|
4
|
+
data.tar.gz: c92ec939a0264e410d01d847521ecbd6432fd67e949858c8cc5183354e4aabd2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2e87a67fce2ce3177231eb78ba340bef28b415a67263098ec7c9ee1a99e8b9e670bcbf04f9393c9db98e4f3186c5e34eb90e9db5140ecb88df5de9088304d3f
|
7
|
+
data.tar.gz: cc3c507551e330c3b6fe052384106a0deed623a3d842ade9f0d119ee5f0886def3cce4de96737dbeec246c816941e5d62c19f7f62c6260900403d2fac9a15d11
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
### 1.2.1
|
2
|
+
* Bugs
|
3
|
+
* Fix bug that did not recognize loaded field values if `attr_accessors` were previously created for the loaded fields due to model validations being present. If `ActiveModel` validators are present on the model, DecoLite will automatically create `attr_accessors` for the fields associated with the validations. The presence of these fields being automatically loaded prohibited any fields subsequently loaded having the same field names to not be recognized as having been loaded, causing validation to validate incorrectly (the model did not recognize the fields as having been loaded).
|
4
|
+
* Fix bug that wiped out field values previously loaded if `ActiveModel` validations were present for fields having the same name. This is because DecoLite will automatically create `attr_accessors` for the fields associated with the validations, initializing these automatically created field vars to be initialized to nil. The auto-creation of these `attr_accessors` was taking place in #initialize AFTER the initial load of any Hash passed to #initialize in the :hash param, so the values loaded were getting nilled out.
|
5
|
+
* Changes
|
6
|
+
* Add specs to test the above scenarios.
|
7
|
+
|
8
|
+
### 1.2.0
|
9
|
+
* Changes
|
10
|
+
* Update the README.md file with better explainations and examples.
|
11
|
+
|
1
12
|
### 1.1.0
|
2
13
|
* Changes
|
3
14
|
* Update mad_flatter gem to v2.0.0.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# DecoLite
|
2
2
|
|
3
|
-
[![GitHub version](http://badge.fury.io/gh/gangelo%
|
3
|
+
[![GitHub version](http://badge.fury.io/gh/gangelo%2Fdeco_lite.svg)](https://badge.fury.io/gh/gangelo%2Fdeco_lite)
|
4
4
|
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/deco_lite.svg)](https://badge.fury.io/rb/deco_lite)
|
6
6
|
|
@@ -13,93 +13,77 @@
|
|
13
13
|
|
14
14
|
## Introduction
|
15
15
|
|
16
|
-
|
16
|
+
*DecoLite* is a little gem that allows you to use the provided `DecoLite::Model` class to dynamically create Decorator class objects. Use the `DecoLite::Model` class directly, or inherit from the `DecoLite::Model` class to create your own unique subclasses with custom functionality. `DecoLite::Model` includes `ActiveModel::Model`, so validation can be applied using [ActiveModel validation helpers](https://api.rubyonrails.org/v6.1.3/classes/ActiveModel/Validations/HelperMethods.html) you're familiar with; or, you can roll your own - just like any other `ActiveModel`.
|
17
17
|
|
18
|
-
|
18
|
+
`DecoLite::Model` allows you to consume a Ruby `Hash` that you supply via the initializer (`DecoLite::Model#new`) or via the `DecoLite::Model#load!` method. Any number of Ruby `Hashes` can be consumed. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (or *"fields"*) on the model. Each attribute created is then assigned the value from the Hash that was loaded. Again, any number of hashes can be consumed using the `DecoLite::Model#load!` method.
|
19
19
|
|
20
|
-
`
|
20
|
+
`attr_accessors` created during initialization, or by calling `DecoLite::Model#load!`, are *mangled* to include namespacing. This allows `DecoLite` to create *unique* attribute names for nested Hashes that may have non-unique key names. For example:
|
21
21
|
|
22
22
|
```ruby
|
23
23
|
# NOTE: keys :name and :age are not unique across this Hash.
|
24
24
|
family = {
|
25
|
+
# :name and :age are not unique
|
25
26
|
name: 'John Doe',
|
26
27
|
age: 35,
|
27
28
|
wife: {
|
29
|
+
# :name and :age are not unique
|
28
30
|
name: 'Mary Doe',
|
29
31
|
age: 30,
|
30
32
|
}
|
31
33
|
}
|
32
34
|
```
|
33
|
-
Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object and assign the values:
|
35
|
+
Given the above example, `DecoLite` will produce the following *unique* `attr_accessors` on the `DecoLite::Model` object, and assign the values:
|
34
36
|
|
35
37
|
```ruby
|
36
|
-
#
|
38
|
+
# Instead of the below, you can also use DecoLite::Model.new.load!(hash: family)
|
37
39
|
model = DecoLite::Model.new(hash: family)
|
38
40
|
|
39
41
|
model.name #=> 'John Doe'
|
40
|
-
model.respond_to? :name= #=> true
|
41
|
-
|
42
42
|
model.age #=> 35
|
43
|
-
model.respond_to? :age= #=> true
|
44
43
|
|
45
44
|
model.wife_name #=> 'Mary Doe'
|
46
|
-
model.respond_to? :wife_name= #=> true
|
47
|
-
|
48
45
|
model.wife_age #=> 30
|
49
|
-
model.respond_to? :wife_age= #=> true
|
50
46
|
```
|
51
47
|
|
52
|
-
|
48
|
+
In the above example, notice how `DecoLite` *mangles* attributes `:wife_name` and `:wife_age` using the `:wife` `Hash` key name to make them unique.
|
53
49
|
|
54
|
-
|
50
|
+
`DecoLite::Model#load!` can be called *multiple times*, on the same model using different `Hashes`. This could potentially cause `attr_accessor` name clashes. In order to ensure unique `attr_accessor` names, a *"namespace"* may be *explicitly* provided to ensure attribute name uniqueness.
|
51
|
+
|
52
|
+
For example, **continuing from the previous example,** if we were to call `DecoLite::Model#load!` a *second time* with the following `Hash`, this would produce `attr_accessor` name clashes which would raise errors, because `:name` and `:age` attributes already exist on the `DecoLite::Model` in question:
|
55
53
|
|
56
54
|
```ruby
|
57
55
|
grandpa = {
|
58
56
|
name: 'Henry Doe',
|
59
57
|
age: 85,
|
60
58
|
}
|
61
|
-
# The :name and :age Hash keys above will produce :name/:name= and :age/:age= attr_accessors
|
62
|
-
# and clash because these were already added to the model when "John Doe" was loaded with
|
63
|
-
# the first call to DecoLite::Model.new(hash: family).
|
64
59
|
```
|
65
60
|
|
66
|
-
|
61
|
+
To handle the above scenario, `DecoLite::Model` allows you to pass a `:namespace` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method; this would produce the following `attr_accessors`, ensuring their uniqueness:
|
67
62
|
|
68
63
|
```ruby
|
69
64
|
model.load!(hash: grandpa, options: { namespace: :grandpa })
|
70
65
|
|
71
|
-
# Unique now that the namespace: :grandpa has been applied:
|
72
66
|
model.grandpa_name #=> 'Henry Doe'
|
73
|
-
model.respond_to? :grandpa_name= #=> true
|
74
|
-
|
75
67
|
model.grandpa_age #=> 85
|
76
|
-
model.respond_to? :grandpa_age= #=> true
|
77
68
|
|
78
|
-
# All the other attributes on the model remain the same, and unique:
|
79
69
|
model.name #=> 'John Doe'
|
80
|
-
model.respond_to? :name= #=> true
|
81
|
-
|
82
70
|
model.age #=> 35
|
83
|
-
model.respond_to? :age= #=> true
|
84
71
|
|
85
72
|
model.wife_name #=> 'Mary Doe'
|
86
|
-
model.respond_to? :wife_name= #=> true
|
87
|
-
|
88
73
|
model.wife_age #=> 30
|
89
|
-
model.respond_to? :wife_age= #=> true
|
90
74
|
```
|
91
75
|
|
92
|
-
###
|
76
|
+
### More examples and usage
|
93
77
|
|
94
|
-
For more examples and usage, see the [Examples and usage](#examples-and-usage) and [
|
78
|
+
For more examples and usage, see the [Examples and usage](#examples-and-usage) and [More examples and usage](#more-examples-and-usage) sections; there is also an "I want to..." section with examples of things you may want to accomplish when using `DecoLite`.
|
95
79
|
|
96
80
|
## Use cases
|
97
81
|
|
98
|
-
###
|
99
|
-
|
82
|
+
### Generally Speaking
|
83
|
+
`DecoLite` would *most likely* thrive where the structure of the `Hashe(s)` consumed are (of course) known, relatively small to moderate in size, and not *terribly* deep nested-hash-wise. This is because of the way `DecoLite` mangles loaded Hash key names to create unique `attr_accessors` on the model (see the Introduction section). However, I'm sure there are some geniuses out there that would find other contexts where `DecoLite` may thrive. Assuming the former is the case, `DecoLite` would be ideal to consume Model attributes, Webservice JSON results (converted to Ruby `Hash`), JSON Web Token (JWT) payloads, etc. to create a cohesive data model to be used in any scenario.
|
100
84
|
|
101
85
|
### Rails
|
102
|
-
Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a
|
86
|
+
Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a *decorator pattern* might be used, and decorator methods provided for use in Rails views; for example:
|
103
87
|
|
104
88
|
```ruby
|
105
89
|
class ViewModel < DecoLite::Model
|
@@ -127,7 +111,7 @@ view_model.salutation
|
|
127
111
|
```
|
128
112
|
### Etc., etc., etc.
|
129
113
|
|
130
|
-
Get creative. Please pop me an email and let me know how
|
114
|
+
Get creative. Please pop me an email and let me know how *you're* using `DecoLite`.
|
131
115
|
|
132
116
|
## Examples and usage
|
133
117
|
|
@@ -152,12 +136,12 @@ wife = {
|
|
152
136
|
},
|
153
137
|
}
|
154
138
|
|
155
|
-
class Couple < DecoLite::Model
|
139
|
+
class Couple < DecoLite::Model
|
156
140
|
def live_together?
|
157
141
|
husband_info_address == wife_info_address
|
158
142
|
end
|
159
143
|
|
160
|
-
def
|
144
|
+
def bread_winner
|
161
145
|
case
|
162
146
|
when husband_info_salary > wife_info_salary
|
163
147
|
husband_name
|
@@ -176,7 +160,7 @@ couple.load!(hash: wife, options: { namespace: :wife })
|
|
176
160
|
|
177
161
|
# Will produce the following:
|
178
162
|
model.live_together? #=> true
|
179
|
-
model.
|
163
|
+
model.bread_winner #=> John Doe
|
180
164
|
|
181
165
|
model.husband_name #=> John Doe
|
182
166
|
model.husband_info_age #=> 21
|
@@ -192,7 +176,7 @@ model.wife_info_address #=> 1 street, boonton, nj 07005
|
|
192
176
|
|
193
177
|
#### Add validators to my model
|
194
178
|
|
195
|
-
Simply add your `ActiveModel` validators just like you would any other `ActiveModel::Model` validator. However, be aware that
|
179
|
+
Simply add your `ActiveModel` validators just like you would any other `ActiveModel::Model` validator. However, be aware that any attribute (field) having an *explicit validation* associated with it, will automatically cause an `attr_accessor` to be created for that field; this is to avoid `NoMethodErrors` when validating the model (e.g. `#valid?`, `#validate`, etc.) *before* the data is loaded that would prompt the creation of the associated `attr_accessors` on the model:
|
196
180
|
|
197
181
|
```ruby
|
198
182
|
class Model < DecoLite::Model
|
@@ -200,7 +184,7 @@ class Model < DecoLite::Model
|
|
200
184
|
validates :age, numericality: true
|
201
185
|
end
|
202
186
|
|
203
|
-
#
|
187
|
+
# :address is missing
|
204
188
|
model = Model.new(hash: { first: 'John', last: 'Doe', age: 25 })
|
205
189
|
model.respond_to? :address
|
206
190
|
#=> true
|
@@ -211,17 +195,18 @@ model.errors.full_messages
|
|
211
195
|
#=> ["Address can't be blank"]
|
212
196
|
|
213
197
|
model.load!(hash: { address: '123 park road, anytown, nj 01234' })
|
214
|
-
model.
|
198
|
+
model.valid?
|
215
199
|
#=> true
|
216
200
|
```
|
217
201
|
|
218
202
|
#### Validate whether or not certain fields were loaded
|
219
203
|
|
220
|
-
To be clear, this example does not validate the
|
204
|
+
To be clear, this example does not validate the *data* associated with the fields loaded; rather, this example validates whether or not the *fields themselves* were loaded into your model, and as a result, `attr_accessors` created *for* them on the model. If you only want to validate the *data* loaded into your model, simply use `ActiveModel` validations, just like you would any other `ActiveModel` model (see the [Add validators to my model](#add-validators-to-my-model) section).
|
221
205
|
|
222
|
-
If you want to validate whether or not particular
|
206
|
+
If you want to validate whether or not particular *fields* were loaded into your model, as a result of `#load!`ing data into your model, you need to add the required field names to the `DecoLite::Model#required_fields` attribute, or use inheritance:
|
223
207
|
- Create a `DecoLite::Model` subclass.
|
224
|
-
- Override the `DecoLite::Model#required_fields` method
|
208
|
+
- Override the `DecoLite::Model#required_fields` method.
|
209
|
+
- Return an Array of `Symbols` that represent the fields you want to validate (e.g. `%i[first last ssn]`).
|
225
210
|
|
226
211
|
For example:
|
227
212
|
|
@@ -231,7 +216,7 @@ class Model < DecoLite::Model
|
|
231
216
|
validates :age, numericality: { only_integer: true }, allow_blank: true
|
232
217
|
|
233
218
|
def required_fields
|
234
|
-
# We want to ensure these fields were
|
219
|
+
# We want to ensure these fields were loaded.
|
235
220
|
%i[first last address]
|
236
221
|
end
|
237
222
|
end
|
@@ -240,16 +225,19 @@ model = Model.new
|
|
240
225
|
|
241
226
|
model.validate
|
242
227
|
#=> false
|
228
|
+
|
243
229
|
model.errors.full_messages
|
244
230
|
#=> ["First field is missing", "Last field is missing", "Address field is missing"]
|
231
|
+
```
|
232
|
+
|
233
|
+
If we load data that includes :first, :last, and :address Hash keys, even with nil data, our `":<field> field is missing"` errors would go away; in this scenario, we only wish to validate the *presence of the FIELDS,* not the data associated with these fields!
|
245
234
|
|
246
|
-
|
247
|
-
# nil data, our ":<field> field is missing" errors go away; in this scenario,
|
248
|
-
# we're validating the presence of the FIELDS, not the data associated with
|
249
|
-
# these fields!
|
235
|
+
```ruby
|
250
236
|
model.load!(hash: { first: nil, last: nil, address: nil })
|
237
|
+
|
251
238
|
model.validate
|
252
239
|
#=> true
|
240
|
+
|
253
241
|
model.errors.full_messages
|
254
242
|
#=> []
|
255
243
|
|
@@ -259,19 +247,20 @@ user = {
|
|
259
247
|
address: '123 anystreet, anytown, nj 01234',
|
260
248
|
age: 'x'
|
261
249
|
}
|
250
|
+
|
262
251
|
model.load!(hash: user)
|
252
|
+
|
263
253
|
model.validate
|
264
254
|
#=> false
|
255
|
+
|
265
256
|
model.errors.full_messages
|
266
257
|
#=> ["Age is not a number"]
|
267
258
|
```
|
268
|
-
#### Validate whether or not certain fields were loaded
|
259
|
+
#### Validate whether or not certain fields were loaded *and* validate the data associated with these same fields
|
269
260
|
|
270
|
-
If you simply want to validate the
|
261
|
+
If you simply want to validate the *data* loaded into your model, simply add `ActiveModel` validation, just like you would any other `ActiveModel` model (see the [Add validators to my model](#add-validators-to-my-model) section).
|
271
262
|
|
272
|
-
If you want to validate whether or not particular fields were loaded
|
273
|
-
|
274
|
-
For example:
|
263
|
+
If you want to validate whether or not particular fields were loaded *and* the field data associated with those same fields, you simply need to return the required fields from the `DecoLite#required_fields` method and add the appropriate validation(s); for example:
|
275
264
|
|
276
265
|
```ruby
|
277
266
|
class Model < DecoLite::Model
|
@@ -281,6 +270,7 @@ class Model < DecoLite::Model
|
|
281
270
|
%i[first last address age]
|
282
271
|
end
|
283
272
|
end
|
273
|
+
|
284
274
|
model = Model.new
|
285
275
|
|
286
276
|
model.validate
|
@@ -300,20 +290,27 @@ model.errors.full_messages
|
|
300
290
|
|
301
291
|
#### Manually define attributes (fields) on my model
|
302
292
|
|
303
|
-
Manually defining attributes on your subclass is possible, although there doesn't seem a valid reason to do so, since you can just use `DecoLite::Model#load!` to wire all this up for you automatically. However, if there
|
293
|
+
Manually defining attributes on your subclass is possible, although there doesn't seem a valid reason to do so, since you can just use `DecoLite::Model#load!` to wire all this up for you automatically. However, if there *were* a need to do this, you must add your `attr_reader` to the `DecoLite::Model@field_names` array, or an error will be raised _provided_ there are any conflicting field names being loaded using `DecoLite::Model#load!`. Note that the aforementioned error will be raised regardless of whether or not you set `options: { fields: :merge }`. This is because `DecoLite` considers any existing model attributes *not* added to the model via `load!`*to be native to the model object,* and therefore will not allow you to create `attr_accessors` and assign values to existing model attributes because this can potentially be dangerous.
|
304
294
|
|
305
|
-
To avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, do the following:
|
295
|
+
To avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, you could do the following:
|
306
296
|
|
307
297
|
```ruby
|
308
298
|
class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
|
309
299
|
attr_accessor :existing_field
|
310
300
|
|
311
|
-
def initialize(options: {})
|
312
|
-
|
313
|
-
|
301
|
+
def initialize(hash: {}, options: {})
|
302
|
+
# Make sure we add existing_field to @field_names before we call
|
303
|
+
# the base class initializer.
|
314
304
|
@field_names = %i[existing_field]
|
305
|
+
|
306
|
+
super
|
315
307
|
end
|
316
308
|
end
|
309
|
+
|
310
|
+
model = JustBecauseYouCanDoesntMeanYouShould.new(hash: { existing_field: :value })
|
311
|
+
|
312
|
+
model.existing_field
|
313
|
+
#=> :value
|
317
314
|
```
|
318
315
|
|
319
316
|
However, the above is unnecessary as this can be easily accomplished by passing a `Hash` to the initializer or by using `DecoLite::Model#load!`:
|
@@ -21,10 +21,8 @@ module DecoLite
|
|
21
21
|
load_service.execute(hash: hash, options: load_service_options).tap do |service_hash|
|
22
22
|
service_hash.each_pair do |field_name, value|
|
23
23
|
create_field_accessor field_name: field_name, options: deco_lite_options
|
24
|
-
|
25
|
-
|
26
|
-
field_names << field_name
|
27
|
-
end
|
24
|
+
yield field_name if block_given?
|
25
|
+
field_names << field_name unless field_names.include? field_name
|
28
26
|
set_field_value(field_name: field_name, value: value, options: deco_lite_options)
|
29
27
|
end
|
30
28
|
end
|
data/lib/deco_lite/model.rb
CHANGED
@@ -34,9 +34,9 @@ module DecoLite
|
|
34
34
|
|
35
35
|
hash ||= {}
|
36
36
|
|
37
|
-
load_hash!(hash: hash, options: options) if hash.present?
|
38
|
-
|
39
37
|
load_hash!(hash: auto_attr_accessors, options: options, add_loaded_fields: false) if auto_attr_accessors?
|
38
|
+
|
39
|
+
load_hash!(hash: hash, options: options) if hash.present?
|
40
40
|
end
|
41
41
|
|
42
42
|
validate :validate_required_fields
|
data/lib/deco_lite/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deco_lite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gene M. Angelo, Jr.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|