deco_lite 0.2.4 → 0.3.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: 2d360b0607d72711264a19f807ad4367237f0e5e69efb385728ccbda66e72e05
4
- data.tar.gz: f90e61477cc9dfa1ba8d19070c1490fcccfce413533436ed93b532d4933fad08
3
+ metadata.gz: 8a87d5a79f738b49d45b51c0128b4c252853c683363e7525df836fc7c46e4953
4
+ data.tar.gz: 1f07c897d0f88cd3609a8e6a880d0ce91b71210ee8952eb29148370b2f49f092
5
5
  SHA512:
6
- metadata.gz: 87d78e241cd8aada8c09c8adbcb20cdb5997e96ff4f17cc8dd3a494e973fd10f13fe73927edb604e2125209119bd881522c79995753768fc0f64d049632d1751
7
- data.tar.gz: d4ae03338d636d82d6a67996cea9e2143a07b0319a9ad6f3a1f1e4a508b2f9a4262ec5cb7e68c8cb2b7d29a8f0e046413ad7f9470f4a20c6fa470c20d9e3c907
6
+ metadata.gz: afc6320e0a1e9060dda231249fe16e62bf62d56f11ad7ffa9c4c6a0f094ef866173655c4ee57a419b3c4b83d0d7b7639230db22459f3c2d81d78cc6c4da2dde2
7
+ data.tar.gz: 6db8e3cb9a634669667eb288f4a4343106ad9be71630c1a9e95af366649b86e88459d0b79f1c71e09345a75805e643aa1e1e3a2836e2045bd1191bd78484685c
data/.reek.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  exclude_paths:
2
2
  - vendor
3
3
  - spec
4
+ - scratch.rb
4
5
  detectors:
5
6
  # TooManyInstanceVariables:
6
7
  # exclude:
data/.rubocop.yml CHANGED
@@ -116,7 +116,7 @@ Layout/LineLength:
116
116
  # Avoid methods longer than 15 lines of code.
117
117
  Metrics/MethodLength:
118
118
  Max: 20
119
- IgnoredMethods:
119
+ AllowedMethods:
120
120
  - swagger_path
121
121
  - operation
122
122
 
data/CHANGELOG.md CHANGED
@@ -1,24 +1,44 @@
1
+ ### 0.3.1
2
+ * Changes
3
+ * Added `DecoLite::FieldRequireable::MISSING_REQUIRED_FIELD_ERROR_TYPE` for required field type errors.
4
+ * Update README.md with more examples.
5
+
6
+ ### 0.3.0
7
+ * Changes
8
+ * `DecoLite::Model#new` how accepts a :hash named parameter that will load the Hash as if calling `DecoLite::Model.new.load!(hash: <hash>)`.
9
+ * `DecoLite::Model#new now creates attr_accessors (fields) for any attribute that has an ActiveModel validator associated with it. This prevents errors raised when #validate is called before data is #load!ed.
10
+ * `DecoLite::Model#new` now creates attr_accessors (fields) for any field returned from `DecoLite::Model#reqired_fields` IF the required_fields: :auto option is set.
11
+ * bin/console now starts a pry-byebug session.
12
+
13
+ ### 0.2.5
14
+ * Changes
15
+ * Remove init of `@field_names = []` in `Model#initialize` as unnecessary - FieldNamesPersistable takes care of this.
16
+ * Bug fixes
17
+ * Fix but that does not take into account option :namespace when determining whether or not a field name conflicts with an existing attribute (already exists).
18
+
1
19
  ### 0.2.4
2
20
  * Changes
3
21
  * Change DecoLite::Model#load to #load! as it alters the object, give deprecation warning when calling #load.
4
22
  * FieldConflictable now expliticly prohibits loading fields that conflict with attributes that are native to the receiver. In other words, you cannot load fields with names like :to_s, :tap, :hash, etc.
5
23
  * FieldCreatable now creates attr_accessors on the instance using #define_singleton_method, not at the class level (i.e. self.class.attr_accessor) (see bug fixes).
6
- * bug fixes
24
+ * Bug fixes
7
25
  * Fix bug that used self.class.attr_accessor in DecoLite::FieldCreatable to create attributes, which forced every object of that class subsequently created have the accessors created which caused field name conflicts across DecoLite::Model objects.
8
26
 
9
27
  ### 0.2.3
10
- * Fix bug that added duplcate field names to Model#field_names.
28
+ * Bug fixes
29
+ * Fix bug that added duplcate field names to Model#field_names.
11
30
 
12
31
  ### 0.2.2
13
- * Fix bug requiring support codez in lib/deco_lite.rb.
32
+ * Bug fixes
33
+ * Fix bug requiring support codez in lib/deco_lite.rb.
14
34
 
15
35
  ### 0.2.1
16
- * changes
36
+ * Changes
17
37
  * Add mad_flatter gem runtime dependency.
18
38
  * Refactor to let mad_flatter handle the Hash flattening.
19
39
 
20
40
  ### 0.1.1
21
- * changes
41
+ * Changes
22
42
  * Update gems and especially rake gem version to squash CVE-2020-8130, see https://github.com/advisories/GHSA-jppv-gw3r-w3q8.
23
43
  * Fix rubocop violations.
24
44
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (0.2.4)
4
+ deco_lite (0.3.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
@@ -15,17 +15,17 @@
15
15
 
16
16
  DecoLite is in development. I wouldn't expect breaking changes before v1.0.0; however, I can't completely rule this out. Currently, DecoLite only supports Hashes whose keys are `Symbols`, contain no embedded spaces, and conform to Ruby `attr_accessor` naming conventions. However, I'll certainly work out a solution for all this in future releases.
17
17
 
18
- TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load` options: how these work, and how they play together (in the meantime, see the specs).
18
+ TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load!` options: how these work, and how they play together (in the meantime, see the specs).
19
19
 
20
20
  _Deco_ is a little gem that allows you to use the provided `DecoLite::Model` class (`include ActiveModel::Model`) to create Decorator classes which can be instantiated and used. 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.
21
21
 
22
- A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply 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.
22
+ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply 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.
23
23
 
24
24
  `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:
25
25
 
26
26
  ```ruby
27
27
  # NOTE: keys :name and :age are not unique across this Hash.
28
- {
28
+ family = {
29
29
  name: 'John Doe',
30
30
  age: 35,
31
31
  wife: {
@@ -34,37 +34,66 @@ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via th
34
34
  }
35
35
  }
36
36
  ```
37
- Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load`), and assign the values:
37
+ Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load!`), and assign the values:
38
38
 
39
39
  ```ruby
40
- name=, name #=> 'John Doe'
41
- age=, age #=> 35
42
- wife_name=, wife_name #=> 'Mary Doe'
43
- wife_age=, wife_age #=> 30
40
+ model = DecoLite::Model.new.load!(hash: family)
41
+
42
+ model.name #=> 'John Doe'
43
+ model.respond_to? :name= #=> true
44
+
45
+ model.age #=> 35
46
+ model.respond_to? :age= #=> true
47
+
48
+ model.wife_name #=> 'Mary Doe'
49
+ model.respond_to? :wife_name= #=> true
50
+
51
+ model.wife_age #=> 30
52
+ model.respond_to? :wife_age= #=> true
44
53
  ```
45
54
 
46
- `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. For example, continuing from the previous example; if we were to call `DecoLite::Model#load` a _second time_ with the following Hash, it would produce `attr_accessor` name clashes:
55
+ `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. For example, continuing from the previous example; if we were to call `DecoLite::Model#load!` a _second time_ with the following Hash, it would produce `attr_accessor` name clashes:
47
56
 
48
57
  ```ruby
49
- {
58
+ grandpa = {
50
59
  name: 'Henry Doe',
51
60
  age: 85,
52
61
  }
62
+ # The :name and :age Hash keys above will produce :name/:name= and :age/:age= attr_accessors and clash because these were already added to the model when "John Doe" was loaded with the first call to DecoLite::Model#load!.
53
63
  ```
54
64
 
55
- However, passing a `namespace: :grandpa` option to the `DecoLite::Model#load` method, would produce the following `attr_accessors`, ensuring uniquess:
65
+ However, passing a `namespace:` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method, would produce the following `attr_accessors`, ensuring their uniqueness:
66
+
56
67
  ```ruby
57
- # Unique now that the namespace "grandpa" has been applied.
58
- grandpa_name=, grandpa_name #=> 'Henry Doe'
59
- grandpa_age=, grandpa_age #=> 85
68
+ model.load!(hash: grandpa, options: { namespace: :grandpa })
69
+
70
+ # Unique now that the namespace: :grandpa has been applied:
71
+ model.grandpa_name #=> 'Henry Doe'
72
+ model.respond_to? :grandpa_name= #=> true
73
+
74
+ model.grandpa_age #=> 85
75
+ model.respond_to? :grandpa_age= #=> true
76
+
77
+ # All the other attributes on the model remain the same, and unique:
78
+ model.name #=> 'John Doe'
79
+ model.respond_to? :name= #=> true
80
+
81
+ model.age #=> 35
82
+ model.respond_to? :age= #=> true
83
+
84
+ model.wife_name #=> 'Mary Doe'
85
+ model.respond_to? :wife_name= #=> true
86
+
87
+ model.wife_age #=> 30
88
+ model.respond_to? :wife_age= #=> true
60
89
  ```
61
- ## Use Cases
90
+ ## Use cases
62
91
 
63
92
  ### General
64
- _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..
93
+ _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..
65
94
 
66
95
  ### Rails
67
- Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_ can be used, and methods provided for use in Rails views; for example:
96
+ 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:
68
97
 
69
98
  ```ruby
70
99
  class ViewModel < DecoLite::Model
@@ -81,7 +110,7 @@ end
81
110
 
82
111
  view_model = ViewModel.new
83
112
 
84
- view_model.load(hash: { first: 'John', last: 'Doe' })
113
+ view_model.load!(hash: { first: 'John', last: 'Doe' })
85
114
 
86
115
  view_model.valid?
87
116
  #=> true
@@ -96,23 +125,7 @@ view_model.salutation
96
125
 
97
126
  Get creative. Please pop me an email and let me know how _you're_ using _Deco_.
98
127
 
99
- ## Installation
100
-
101
- Add this line to your application's Gemfile:
102
-
103
- ```ruby
104
- gem 'deco_lite'
105
- ```
106
-
107
- And then execute:
108
-
109
- $ bundle
110
-
111
- Or install it yourself as:
112
-
113
- $ gem install deco_lite
114
-
115
- ## Examples and Usage
128
+ ## Examples and usage
116
129
 
117
130
  ```ruby
118
131
  require 'deco_lite'
@@ -154,8 +167,8 @@ end
154
167
 
155
168
  couple = Couple.new
156
169
 
157
- couple.load(hash: husband, options: { namespace: :husband })
158
- couple.load(hash: wife, options: { namespace: :wife })
170
+ couple.load!(hash: husband, options: { namespace: :husband })
171
+ couple.load!(hash: wife, options: { namespace: :wife })
159
172
 
160
173
  # Will produce the following:
161
174
  model.live_together? #=> true
@@ -169,17 +182,128 @@ model.wife_name #=> Amy Doe
169
182
  model.wife_info_age #=> 20
170
183
  model.wife_info_address #=> 1 street, boonton, nj 07005
171
184
  ```
185
+ ## More examples and usage
186
+
187
+ ### I want to...
188
+
189
+ #### Add validators to my model
190
+
191
+ 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.):
192
+
193
+ ```ruby
194
+ class Model < DecoLite::Model
195
+ validates :first, :last, :address, presence: true
196
+ validates :age, numericality: true
197
+ end
198
+
199
+ # No :address
200
+ model = Model.new(hash: { first: 'John', last: 'Doe', age: 25 })
201
+ model.respond_to? :address
202
+ #=> true
203
+
204
+ model.valid?
205
+ #=> false
206
+ model.errors.full_messages
207
+ #=> ["Address can't be blank"]
208
+
209
+ model.load!(hash: { address: '123 park road, anytown, nj 01234' })
210
+ model.validate
211
+ #=> true
212
+ ```
172
213
 
173
- ### Manually Defining Attributes
214
+ #### Validate whether or not certain fields were loaded
174
215
 
175
- Manually defining attributes on your subclass is possible; however, you
176
- must add your attr_reader name to the `DecoLite::Model@field_names` array, or an error will be reaised _if_ there are any conflicting field names being loaded
177
- using `DecoLite::Model#load!`, regardless of setting the `{ fields: :merge }`
178
- option. This is because DecoLite assumes any existing attributes not added to
179
- the model via `load!`to be native to the object created, and therefore will not
180
- allow you to create attr_accessors for existing attributes, as this can potentially be dangerous.
216
+ To be clear, this has nothing to do with the _data_ associated with the fields loaded; rather, this has to do with whether or not the _fields themselves_ were created as attributes on your model as a result of loading data into your model. 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.
181
217
 
182
- To avoid errors when manually defining attributes on the model that could potentially be in conflict with fields loaded using `DecoLite::Model#load!`, do the following:
218
+ If you want to validate whether or not particular _fields_ were added to your model as attributes (i.e. `attr_accessor`), as a result of `#load!`ing data into your model, you need to do a few things:
219
+ - Create a `DecoLite::Model` subclass.
220
+ - Override the `DecoLite::Model#required_fields` method to return the field names you want to validate.
221
+ - Use the `required_fields: nil` option when instantiating your model object.
222
+ - DO NOT add `ActiveModel` validators that _explicitly_ reference any field returned from `DecoLite::Model#required_fields`; this will cause `attr_accessors` to be created for these fields; consequently, `DecoLite::FieldRequireable#validate_required_fields` will _never_ return any errors because these fields will exist as attributes on your model. In other words, do not add `validates :first, :last, :address, presence: true` to your model if you need to validate whether or not the data you load into your model included fields :first, :last and :address.
223
+
224
+ For example:
225
+
226
+ ```ruby
227
+ class Model < DecoLite::Model
228
+ # :age field is optional and it's value is optional.
229
+ validates :age, numericality: { only_integer: true }, allow_blank: true
230
+
231
+ def required_fields
232
+ # We want to ensure attr_accessors are created for these fields.
233
+ %i(first last address)
234
+ end
235
+ end
236
+
237
+ # Option "required_fields: :auto" is the default which will automatically create
238
+ # attr_accessors for fields returned from DecoLite::Model#required_fields, so we
239
+ # need to set this option to nil (i.e. required_fields: nil).
240
+ model = Model.new(options: { required_fields: nil })
241
+
242
+ model.validate
243
+ #=> false
244
+ model.errors.full_messages
245
+ #=> ["First field is missing", "Last field is missing", "Address field is missing"]
246
+
247
+ user = { first: 'John', last: 'Doe', address: '123 anystreet, anytown, nj 01234'}
248
+ model.load!(hash: user)
249
+ model.validate
250
+ #=> false
251
+ model.errors.full_messages
252
+ #=> ["Age is not a number"]
253
+ ```
254
+ #### Validate whether or not certain fields were loaded _and_ validate the data associated with these same fields
255
+
256
+ 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.
257
+
258
+ If you want to validate whether or not particular fields were loaded _and_ field data associated with these same fields, you'll have to use custom validation (e.g. override `DecoLite::FieldRequireable#validate_required_fields` and manually add your own validation and errors). This is because `DecoLite::Model#new` will automatically create `attr_accessors` for any attribute (field) that has an _explicit_ `ActiveModel` validation associated with it, and return false positives when you validate your model. In addition to this, you will need to do several other things outlined in the [Validate whether or not certain fields were loaded](#validate-whether-or-not-certain-fields-were-loaded) section.
259
+
260
+ For example:
261
+
262
+ ```ruby
263
+ class Model < DecoLite::Model
264
+ def required_fields
265
+ %i(first last address age)
266
+ end
267
+
268
+ def validate_required_fields
269
+ super
270
+
271
+ first = self.try(:first)
272
+ errors.add(:first, "can't be blank") if first.nil?
273
+
274
+ last = self.try(:last)
275
+ errors.add(:last, "can't be blank") if last.nil?
276
+
277
+ address = self.try(:address)
278
+ errors.add(:address, "can't be blank") if address.nil?
279
+
280
+ age = self.try(:age)
281
+ errors.add(:age, "can't be blank") if age.nil?
282
+ errors.add(:age, 'is not a number') unless /\d+/ =~ age
283
+ end
284
+ end
285
+ model = Model.new(options: { required_fields: nil })
286
+
287
+ model.validate
288
+ #=> false
289
+
290
+ model.errors.full_messages
291
+ #=> ["First field is missing",
292
+ "Last field is missing",
293
+ "Address field is missing",
294
+ "Age field is missing",
295
+ "First can't be blank",
296
+ "Last can't be blank",
297
+ "Address can't be blank",
298
+ "Age can't be blank",
299
+ "Age is not a number"]
300
+ ```
301
+
302
+ #### Manually define attributes (fields) on my model
303
+
304
+ 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.
305
+
306
+ To avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, do the following:
183
307
 
184
308
  ```ruby
185
309
  class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
@@ -193,6 +317,35 @@ class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
193
317
  end
194
318
  ```
195
319
 
320
+ However, the above is unnecessary as this can be easily accomplished using `DecoLite::Model#load!`:
321
+ ```ruby
322
+ model = Class.new(DecoLite::Model).new.load!(hash:{ existing_field: :existing_field_value })
323
+
324
+ model.field_names
325
+ #=> [:existing_field]
326
+
327
+ model.existing_field
328
+ #=> :existing_field_value
329
+
330
+ model.respond_to? :existing_field=
331
+ #=> true
332
+ ```
333
+ ## Installation
334
+
335
+ Add this line to your application's Gemfile:
336
+
337
+ ```ruby
338
+ gem 'deco_lite'
339
+ ```
340
+
341
+ And then execute:
342
+
343
+ $ bundle
344
+
345
+ Or install it yourself as:
346
+
347
+ $ gem install deco_lite
348
+
196
349
  ## Development
197
350
 
198
351
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/bin/console CHANGED
@@ -10,6 +10,5 @@ require 'deco_lite'
10
10
  # (If you use this, don't forget to add pry to your Gemfile!)
11
11
  # require "pry"
12
12
  # Pry.start
13
-
14
- require 'irb'
15
- IRB.start(__FILE__)
13
+ require 'pry-byebug'
14
+ Pry.start
@@ -1,43 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'field_name_namespaceable'
3
4
  require_relative 'fields_optionable'
4
5
 
5
6
  module DecoLite
6
7
  # Defines methods to to manage fields that conflict with
7
8
  # existing model attributes.
8
9
  module FieldConflictable
10
+ include FieldNameNamespaceable
9
11
  include FieldsOptionable
10
12
 
11
13
  def validate_field_conflicts!(field_name:, options:)
12
14
  return unless field_conflict?(field_name: field_name, options: options)
13
15
 
16
+ field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
17
+
14
18
  raise "Field :#{field_name} conflicts with existing method(s) " \
15
- ":#{field_name} and/or :#{field_name}=; " \
16
- 'this will raise an error when loading using strict mode ' \
17
- "(i.e. options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }) " \
18
- 'or if the method(s) are native to the object (e.g :to_s, :==, etc.).'
19
+ ":#{field_name} and/or :#{field_name}=; " \
20
+ 'this will raise an error when loading using strict mode ' \
21
+ "(i.e. options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }) " \
22
+ 'or if the method(s) are native to the object (e.g :to_s, :==, etc.). ' \
23
+ "Current options are: options: #{options.to_h}."
19
24
  end
20
25
 
21
26
  # This method returns true
22
27
  def field_conflict?(field_name:, options:)
23
28
  # If field_name was already added using Model#load, there is only a
24
29
  # conflict if options.strict? is true.
25
- if field_names_include?(field_name: field_name)
26
- return options.strict?
27
- end
30
+ return options.strict? if field_names_include?(field_name: field_name, options: options)
28
31
 
29
32
  # If we get here, we know that :field_name does not exist as an
30
33
  # attribute on the model. If the attribute already exists on the
31
34
  # model, this is a conflict because we cannot override an attribute
32
35
  # that already exists on the model
33
- attr_accessor_exist?(field_name: field_name)
36
+ attr_accessor_exist?(field_name: field_name, options: options)
34
37
  end
35
38
 
36
- def field_names_include?(field_name:)
39
+ def field_names_include?(field_name:, options:)
40
+ field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
41
+
37
42
  field_names.include? field_name
38
43
  end
39
44
 
40
- def attr_accessor_exist?(field_name:)
45
+ def attr_accessor_exist?(field_name:, options:)
46
+ field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
47
+
41
48
  respond_to?(field_name) || respond_to?(:"#{field_name}=")
42
49
  end
43
50
  end
@@ -30,6 +30,7 @@ module DecoLite
30
30
 
31
31
  private
32
32
 
33
+ # rubocop:disable Lint/UnusedMethodArgument
33
34
  def create_field_getter(field_name:, options:)
34
35
  define_singleton_method(field_name) do
35
36
  instance_variable_get "@#{field_name}"
@@ -41,5 +42,6 @@ module DecoLite
41
42
  instance_variable_set "@#{field_name}", value
42
43
  end
43
44
  end
45
+ # rubocop:enable Lint/UnusedMethodArgument
44
46
  end
45
47
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoLite
4
+ # Defines methods to transform a field name into a field name
5
+ # with a namespace.
6
+ module FieldNameNamespaceable
7
+ def field_name_or_field_name_with_namespace(field_name:, options:)
8
+ return field_name unless options.namespace?
9
+
10
+ field_name_with_namespace(field_name: field_name, namespace: options.namespace)
11
+ end
12
+
13
+ def field_name_with_namespace(field_name:, namespace:)
14
+ "#{namespace}_#{field_name}"
15
+ end
16
+ end
17
+ end
@@ -4,6 +4,8 @@ module DecoLite
4
4
  # Provides methods to manage fields that must be defined from
5
5
  # the dynamically loaded data.
6
6
  module FieldRequireable
7
+ MISSING_REQUIRED_FIELD_ERROR_TYPE = :missing_required_field
8
+
7
9
  # Returns field names that will be used to validate the presence of
8
10
  # dynamically created fields from loaded objects.
9
11
  #
@@ -23,7 +25,7 @@ module DecoLite
23
25
  required_fields.each do |field_name|
24
26
  next if required_field_exist? field_name: field_name
25
27
 
26
- errors.add(field_name, 'field is missing', type: :missing_required_field)
28
+ errors.add(field_name, 'field is missing', type: MISSING_REQUIRED_FIELD_ERROR_TYPE)
27
29
  end
28
30
  end
29
31
 
@@ -3,14 +3,14 @@
3
3
  module DecoLite
4
4
  # Defines methods validate field (attribute) names.
5
5
  module FieldValidatable
6
- FIELD_NAME_REGEX = /\A(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)\z/i.freeze
6
+ FIELD_NAME_REGEX = %r{\A(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+*/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)\z}i
7
7
 
8
8
  module_function
9
9
 
10
+ # rubocop:disable Lint/UnusedMethodArgument
10
11
  def validate_field_name!(field_name:, options: nil)
11
- unless field_name =~ FIELD_NAME_REGEX
12
- raise "field_name '#{field_name}' is not a valid field name."
13
- end
12
+ raise "field_name '#{field_name}' is not a valid field name." unless FIELD_NAME_REGEX.match?(field_name)
14
13
  end
14
+ # rubocop:enable Lint/UnusedMethodArgument
15
15
  end
16
16
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoLite
4
+ # Defines fields that may have attr_accessors automatically created for them
5
+ # on the model.
6
+ module FieldsAutoloadable
7
+ private
8
+
9
+ def auto_attr_accessors?
10
+ auto_attr_accessors.present?
11
+ end
12
+
13
+ # This method returns a Hash of fields that are implicitly defined either
14
+ # through ActiveModel validators or by returning them from the
15
+ # #required_fields Array.
16
+ def auto_attr_accessors
17
+ return @auto_attr_accessors.dup if defined?(@auto_attr_accessors)
18
+
19
+ @auto_attr_accessors = self.class.validators.map(&:attributes)
20
+ @auto_attr_accessors.concat(required_fields) if options.required_fields_auto?
21
+ @auto_attr_accessors = auto_attr_accessors_assign
22
+ end
23
+
24
+ def auto_attr_accessors_assign
25
+ @auto_attr_accessors.flatten.uniq.each_with_object({}) do |field_name, auto_attr_accessors_hash|
26
+ auto_attr_accessors_hash[field_name] = nil
27
+ end
28
+ end
29
+ end
30
+ end
@@ -16,8 +16,8 @@ module DecoLite
16
16
  return {} if hash.blank?
17
17
 
18
18
  load_service_options = merge_with_load_service_options deco_lite_options: deco_lite_options
19
- load_service.execute(hash: hash, options: load_service_options).tap do |h|
20
- h.each_pair do |field_name, value|
19
+ load_service.execute(hash: hash, options: load_service_options).tap do |service_hash|
20
+ service_hash.each_pair do |field_name, value|
21
21
  create_field_accessor field_name: field_name, options: deco_lite_options
22
22
  field_names << field_name unless field_names.include? field_name
23
23
  set_field_value(field_name: field_name, value: value, options: deco_lite_options)
@@ -4,7 +4,7 @@ module DecoLite
4
4
  # Provides methods to convert the object to a Hash.
5
5
  module Hashable
6
6
  def to_h
7
- field_names.each.each_with_object({}) do |field_name, hash|
7
+ field_names.each_with_object({}) do |field_name, hash|
8
8
  hash[field_name] = public_send field_name
9
9
  end
10
10
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
- require_relative 'field_creatable'
5
- require_relative 'field_requireable'
4
+ require_relative 'field_assignable'
6
5
  require_relative 'field_names_persistable'
6
+ require_relative 'field_requireable'
7
+ require_relative 'fields_auto_attr_accessable'
7
8
  require_relative 'hash_loadable'
8
9
  require_relative 'hashable'
9
10
  require_relative 'model_nameable'
@@ -14,9 +15,10 @@ module DecoLite
14
15
  # dynamic models that can be used as decorators.
15
16
  class Model
16
17
  include ActiveModel::Model
17
- include FieldCreatable
18
+ include FieldAssignable
18
19
  include FieldNamesPersistable
19
20
  include FieldRequireable
21
+ include FieldsAutoloadable
20
22
  include HashLoadable
21
23
  include Hashable
22
24
  include ModelNameable
@@ -24,15 +26,18 @@ module DecoLite
24
26
 
25
27
  validate :validate_required_fields
26
28
 
27
- def initialize(options: {})
28
- @field_names = []
29
-
29
+ def initialize(hash: {}, options: {})
30
30
  # Accept whatever options are sent, but make sure
31
31
  # we have defaults set up. #options_with_defaults
32
32
  # will merge options into OptionsDefaultable::DEFAULT_OPTIONS
33
33
  # so we have defaults for any options not passed in through
34
34
  # options.
35
35
  self.options = Options.with_defaults options
36
+
37
+ hash ||= {}
38
+
39
+ auto_fields = auto_attr_accessors.merge(hash)
40
+ load!(hash: auto_fields, options: options) if hash.present? || auto_attr_accessors?
36
41
  end
37
42
 
38
43
  def load!(hash:, options: {})
@@ -49,10 +54,10 @@ module DecoLite
49
54
  end
50
55
 
51
56
  def load(hash:, options: {})
52
- puts 'WARNING: DecoLite::Model#load will be deprecated in a future release;' \
53
- ' use DecoLite::Model#load! instead!'
57
+ puts 'WARNING: DecoLite::Model#load will be deprecated in a future release; ' \
58
+ 'use DecoLite::Model#load! instead!'
54
59
 
55
- load!(hash: hash, options: options)
60
+ load!(hash: hash, options: options)
56
61
  end
57
62
  end
58
63
  end
@@ -24,6 +24,10 @@ module DecoLite
24
24
  def namespace?
25
25
  namespace.present? || false
26
26
  end
27
+
28
+ def required_fields_auto?
29
+ required_fields == OPTION_REQUIRED_FIELDS_AUTO
30
+ end
27
31
  end
28
32
  validate_options! options: immutable_struct_ex.to_h
29
33
  immutable_struct_ex
@@ -2,16 +2,19 @@
2
2
 
3
3
  require_relative 'fields_optionable'
4
4
  require_relative 'namespace_optionable'
5
+ require_relative 'required_fields_optionable'
5
6
 
6
7
  module DecoLite
7
8
  # Defines default options and their optionn values.
8
9
  module OptionsDefaultable
9
10
  include DecoLite::FieldsOptionable
10
11
  include DecoLite::NamespaceOptionable
12
+ include DecoLite::RequiredFieldsOptionable
11
13
 
12
14
  DEFAULT_OPTIONS = {
13
15
  OPTION_FIELDS => OPTION_FIELDS_DEFAULT,
14
- OPTION_NAMESPACE => OPTION_NAMESPACE_DEFAULT
16
+ OPTION_NAMESPACE => OPTION_NAMESPACE_DEFAULT,
17
+ OPTION_REQUIRED_FIELDS => OPTION_REQUIRED_FIELDS_DEFAULT
15
18
  }.freeze
16
19
  end
17
20
  end
@@ -2,14 +2,16 @@
2
2
 
3
3
  require_relative 'fields_optionable'
4
4
  require_relative 'namespace_optionable'
5
+ require_relative 'required_fields_optionable'
5
6
 
6
7
  module DecoLite
7
8
  # Methods to validate options.
8
9
  module OptionsValidatable
9
10
  include DecoLite::FieldsOptionable
10
11
  include DecoLite::NamespaceOptionable
12
+ include DecoLite::RequiredFieldsOptionable
11
13
 
12
- OPTIONS = [OPTION_FIELDS, OPTION_NAMESPACE].freeze
14
+ OPTIONS = [OPTION_FIELDS, OPTION_NAMESPACE, OPTION_REQUIRED_FIELDS].freeze
13
15
 
14
16
  def validate_options!(options:)
15
17
  raise ArgumentError, 'options is not a Hash' unless options.is_a? Hash
@@ -17,8 +19,9 @@ module DecoLite
17
19
  validate_options_present! options: options
18
20
 
19
21
  validate_option_keys! options: options
20
- validate_option_fields! fields: options[:fields]
21
- validate_option_namespace! namespace: options[:namespace]
22
+ validate_option_fields! fields: options[OPTION_FIELDS]
23
+ validate_option_namespace! namespace: options[OPTION_NAMESPACE]
24
+ validate_option_required_fields! required_fields: options[OPTION_REQUIRED_FIELDS]
22
25
  end
23
26
 
24
27
  def validate_options_present!(options:)
@@ -35,7 +38,7 @@ module DecoLite
35
38
 
36
39
  raise ArgumentError,
37
40
  "option :fields value or type is invalid. #{OPTION_FIELDS_VALUES} (Symbol) " \
38
- "was expected, but '#{fields}' (#{fields.class}) was received."
41
+ "was expected, but '#{fields}' (#{fields.class}) was received."
39
42
  end
40
43
 
41
44
  def validate_option_namespace!(namespace:)
@@ -43,7 +46,16 @@ module DecoLite
43
46
  return if namespace.blank? || namespace.is_a?(Symbol)
44
47
 
45
48
  raise ArgumentError, 'option :namespace value or type is invalid. A Symbol was expected, ' \
46
- "but '#{namespace}' (#{namespace.class}) was received."
49
+ "but '#{namespace}' (#{namespace.class}) was received."
50
+ end
51
+
52
+ def validate_option_required_fields!(required_fields:)
53
+ # :required_fields is optional.
54
+ return if required_fields.blank? || OPTION_REQUIRED_FIELDS_VALUES.include?(required_fields)
55
+
56
+ raise ArgumentError,
57
+ "option :fields_required value or type is invalid. #{OPTION_REQUIRED_FIELDS_VALUES} (Symbol) " \
58
+ "was expected, but '#{required_fields}' (#{required_fields.class}) was received."
47
59
  end
48
60
  end
49
61
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoLite
4
+ # Defines the fields option hash key and acceptable hash key values.
5
+ module RequiredFieldsOptionable
6
+ # The option hash key for this option.
7
+ OPTION_REQUIRED_FIELDS = :required_fields
8
+ # The valid option values for this option key.
9
+ OPTION_REQUIRED_FIELDS_AUTO = :auto
10
+ # The default value for this option.
11
+ OPTION_REQUIRED_FIELDS_DEFAULT = OPTION_REQUIRED_FIELDS_AUTO
12
+ # The valid option key values for this option.
13
+ OPTION_REQUIRED_FIELDS_VALUES = [OPTION_REQUIRED_FIELDS_AUTO].freeze
14
+ end
15
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the version of this gem.
4
4
  module DecoLite
5
- VERSION = '0.2.4'
5
+ VERSION = '0.3.1'
6
6
  end
data/lib/deco_lite.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'deco_lite/field_assignable'
4
4
  require_relative 'deco_lite/field_conflictable'
5
5
  require_relative 'deco_lite/field_creatable'
6
+ require_relative 'deco_lite/field_name_namespaceable'
6
7
  require_relative 'deco_lite/field_names_persistable'
7
8
  require_relative 'deco_lite/field_requireable'
8
9
  require_relative 'deco_lite/field_retrievable'
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: 0.2.4
4
+ version: 0.3.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-08-21 00:00:00.000000000 Z
11
+ date: 2022-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -261,10 +261,12 @@ files:
261
261
  - lib/deco_lite/field_assignable.rb
262
262
  - lib/deco_lite/field_conflictable.rb
263
263
  - lib/deco_lite/field_creatable.rb
264
+ - lib/deco_lite/field_name_namespaceable.rb
264
265
  - lib/deco_lite/field_names_persistable.rb
265
266
  - lib/deco_lite/field_requireable.rb
266
267
  - lib/deco_lite/field_retrievable.rb
267
268
  - lib/deco_lite/field_validatable.rb
269
+ - lib/deco_lite/fields_auto_attr_accessable.rb
268
270
  - lib/deco_lite/fields_optionable.rb
269
271
  - lib/deco_lite/hash_loadable.rb
270
272
  - lib/deco_lite/hashable.rb
@@ -275,6 +277,7 @@ files:
275
277
  - lib/deco_lite/options.rb
276
278
  - lib/deco_lite/options_defaultable.rb
277
279
  - lib/deco_lite/options_validatable.rb
280
+ - lib/deco_lite/required_fields_optionable.rb
278
281
  - lib/deco_lite/version.rb
279
282
  homepage: https://github.com/gangelo/deco_lite
280
283
  licenses: