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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67309b0dd1feaa0f5022e255841a5f9d86c7362facce49e78fac594b74c3fd4e
4
- data.tar.gz: 5e7fe616878edf0872ffac7fb292248b3292d4be88f6e9b4f6848f46aacf04e6
3
+ metadata.gz: d5e504d4b1c9442022ff1d9dc1727f1116b3be5bef76cce7c3f0b4373e1d5531
4
+ data.tar.gz: c92ec939a0264e410d01d847521ecbd6432fd67e949858c8cc5183354e4aabd2
5
5
  SHA512:
6
- metadata.gz: 5626e25b3a24d52c934ee9e538d13b71f7709a991d6042604a56d65f6a2a77392fa48ed16d71d93551e155b0307f11773f612038e344bb6d0e87b9bb2fa9782e
7
- data.tar.gz: a876c92d0e679377113a1cf0d96af3aa2e3aef90da8f92a3e3ca693983e614911c9c1c599680c29efe9373d60c5ac5f2deeb5ba4210ad8d1a9e0dfbc8612438d
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (1.1.0)
4
+ deco_lite (1.2.1)
5
5
  activemodel (~> 7.0, >= 7.0.3.1)
6
6
  activesupport (~> 7.0, >= 7.0.3.1)
7
7
  immutable_struct_ex (~> 0.2.0)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DecoLite
2
2
 
3
- [![GitHub version](http://badge.fury.io/gh/gangelo%2Fdeco.svg)](https://badge.fury.io/gh/gangelo%2Fdeco)
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
- _Deco_ is a little gem that allows you to use the provided `DecoLite::Model` class (`include ActiveModel::Model`) to dynamically create Decorator class objects. Inherit from `DecoLite::Model` to create your own unique classes with custom functionality. A `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 are familiar with; or, you can roll your own - just like any other ActiveModel.
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
- A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the initializer (`DecoLite::Model#new`) or via the `DecoLite::Model#load!` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded. Any number of hashes can be consumed using the `DecoLite::Model#load!` method.
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
- `attr_accessor` names created are _mangled_ to include namespacing. This creates unique attribute names for nested Hashes that may include non-unique keys. For example:
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
- # Or DecoLite::Model.new.load!(hash: family)
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
- `DecoLite::Model#load!` can be called _multiple times_, on the same model, with 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 uniqueness.
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
- For example, **continuing from the previous example;** if we were to call `DecoLite::Model#load!` a _second time_ with the following Hash, this would potentially produce `attr_accessor` name clashes:
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
- However, passing a `:namespace` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method, would produce the following `attr_accessors`, ensuring their uniqueness:
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
- ### For more examples and usage
76
+ ### More examples and usage
93
77
 
94
- For more examples and usage, see the [Examples and usage](#examples-and-usage) and [Mode examples and usage](#more-examples-and-usage) sections; there is also an "I want to..." section with examples you might encounter when using `DecoLite`.
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
- ### General
99
- _Deco_ would _most likely_ thrive where the structure of the Hashe(s) consumed by the `DecoLite::Model#load!` method is known. This is because of the way _Deco_ mangles loaded Hash key names to create unique `attr_accessor` names (see the Introduction section); although, I'm sure there are some metaprogramming geniuses out there that might prove me wrong. Assuming this is the case, _Deco_ would be ideal to handle Model attributes, Webservice JSON results (converted to Ruby Hash), JSON Web Token (JWT) payload, etc..
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 _decorator pattern_ might be used, and decorator methods provided for use in Rails views; for example:
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 _you're_ using _Deco_.
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 breadwinner
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.breadwinner #=> John Doe
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 (currently), 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 calling a validation method on the model (e.g. `#valid?`, `#validate`, etc.) *before* the data is loaded to create the associated `attr_accessors`:
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
- # No :address
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.validate
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 _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. If you only 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.
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 _fields_ were added to your model as attributes (`attr_accessor`), 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:
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 and return an Array of field names represented by `Symbols` you want to validate.
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 included as Hash keys during loading.
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
- # If we load data that includes :first, :last, and :address Hash keys even with
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 _and_ validate the data associated with these same fields
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 _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.
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 _and_ field data associated with these same fields, you simply need to add the required fields and any other validation(s).
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 _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 for existing model attributes because this can potentially be dangerous.
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
- super
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
- unless field_names.include? field_name
25
- yield field_name if block_given?
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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the version of this gem.
4
4
  module DecoLite
5
- VERSION = '1.1.0'
5
+ VERSION = '1.2.1'
6
6
  end
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.0
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-09-30 00:00:00.000000000 Z
11
+ date: 2022-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel