deco_lite 0.2.5 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74286df21784817e45f4d615081bfeac44d90f13d8200bc8f066107e8d0f773f
4
- data.tar.gz: 57dcc52147494eb3caf59159cbb21bc23bb6788d0b6315ebc1ddd8cf1868c779
3
+ metadata.gz: b5b5ed040bd367962dd57760a6a0e572b60485ba1d2d3630980ff1a090ca9075
4
+ data.tar.gz: 76402762f17d93fd53495ddabf86487803b0014971db007f61f505727e933f56
5
5
  SHA512:
6
- metadata.gz: b79af6a65df899227821e4a02bf17587f1f423042a4280a54525c8469c1c3344f3dd2c3b16bd0de477c711d27ed04527295ca37e811c1c829037379509971ac6
7
- data.tar.gz: 7d1b5dea80d0af672713d18675166207e2d62ab51aff84045411d02344529a32de513df76d394e3769dfb14a69d0742f18e850398dc9b4cc6bac10eb5c72ecec
6
+ metadata.gz: 584e1c07a160370bc9548e7a41dbaa9adcbee757a35a37925d02accf4ebe71d24cd9d2932e706724f5d8d64e8d98ff77f223224de27e5a0d817eb2675fea81ca
7
+ data.tar.gz: 6a49a3ab8bdcb947341697577e130dd667c508095578536e8d003cfa1424f8e090f3c40e9ddc1376a3337cdcc9a26a02e836bc901c0ae36da39a8acaaae8cc46
data/.gitignore CHANGED
@@ -16,5 +16,5 @@
16
16
  /.vscode/
17
17
  *.code-workspace
18
18
 
19
- scratch.rb
19
+ scratch*.rb
20
20
  readme.txt
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
@@ -13,7 +13,7 @@ AllCops:
13
13
  - '*.gemspec'
14
14
  - 'spec/**/*'
15
15
  - 'vendor/**/*'
16
- - 'scratch.rb'
16
+ - 'scratch*.rb'
17
17
 
18
18
  # Align the elements of a hash literal if they span more than one line.
19
19
  Layout/HashAlignment:
@@ -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,3 +1,23 @@
1
+ ### 0.3.2
2
+ * Changes
3
+ * Refactor FieldAssignable to remove call to FieldCreatable#create_field_accessor as this breaks single responsibility rule; which, in this case, makes sense to remove. FieldCreatable#create_field_accessor can be called wherever creation of a attr_accessor is needed.
4
+ * Refactor specs in keeping with above changes.
5
+ * README.md changes.
6
+ * Bugs
7
+ * Fix bug where loading fields with the options: { fields: :strict } option raises an error for field that already exists.
8
+
9
+ ### 0.3.1
10
+ * Changes
11
+ * Added `DecoLite::FieldRequireable::MISSING_REQUIRED_FIELD_ERROR_TYPE` for required field type errors.
12
+ * Update README.md with more examples.
13
+
14
+ ### 0.3.0
15
+ * Changes
16
+ * `DecoLite::Model#new` how accepts a :hash named parameter that will load the Hash as if calling `DecoLite::Model.new.load!(hash: <hash>)`.
17
+ * `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.
18
+ * `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.
19
+ * bin/console now starts a pry-byebug session.
20
+
1
21
  ### 0.2.5
2
22
  * Changes
3
23
  * Remove init of `@field_names = []` in `Model#initialize` as unnecessary - FieldNamesPersistable takes care of this.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (0.2.5)
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
@@ -13,19 +13,19 @@
13
13
 
14
14
  ## Introduction
15
15
 
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.
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 might try work out a reasonable solution for all this in future releases if the need is there.
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 (e.g. options `fields: :merge` and `fields: :strict` for example; 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 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.
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,67 @@ 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, 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
+ # Or DecoLite::Model.new.load!(hash: family)
41
+ model = DecoLite::Model.new(hash: family)
42
+
43
+ model.name #=> 'John Doe'
44
+ model.respond_to? :name= #=> true
45
+
46
+ model.age #=> 35
47
+ model.respond_to? :age= #=> true
48
+
49
+ model.wife_name #=> 'Mary Doe'
50
+ model.respond_to? :wife_name= #=> true
51
+
52
+ model.wife_age #=> 30
53
+ model.respond_to? :wife_age= #=> true
44
54
  ```
45
55
 
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:
56
+ `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
57
 
48
58
  ```ruby
49
- {
59
+ grandpa = {
50
60
  name: 'Henry Doe',
51
61
  age: 85,
52
62
  }
63
+ # 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.new(hash: family).
53
64
  ```
54
65
 
55
- However, passing a `namespace: :grandpa` option to the `DecoLite::Model#load` method, would produce the following `attr_accessors`, ensuring uniquess:
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:
67
+
56
68
  ```ruby
57
- # Unique now that the namespace "grandpa" has been applied.
58
- grandpa_name=, grandpa_name #=> 'Henry Doe'
59
- grandpa_age=, grandpa_age #=> 85
69
+ model.load!(hash: grandpa, options: { namespace: :grandpa })
70
+
71
+ # Unique now that the namespace: :grandpa has been applied:
72
+ model.grandpa_name #=> 'Henry Doe'
73
+ model.respond_to? :grandpa_name= #=> true
74
+
75
+ model.grandpa_age #=> 85
76
+ model.respond_to? :grandpa_age= #=> true
77
+
78
+ # All the other attributes on the model remain the same, and unique:
79
+ model.name #=> 'John Doe'
80
+ model.respond_to? :name= #=> true
81
+
82
+ model.age #=> 35
83
+ model.respond_to? :age= #=> true
84
+
85
+ model.wife_name #=> 'Mary Doe'
86
+ model.respond_to? :wife_name= #=> true
87
+
88
+ model.wife_age #=> 30
89
+ model.respond_to? :wife_age= #=> true
60
90
  ```
61
- ## Use Cases
91
+ ## Use cases
62
92
 
63
93
  ### 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..
94
+ _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
95
 
66
96
  ### 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:
97
+ 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
98
 
69
99
  ```ruby
70
100
  class ViewModel < DecoLite::Model
@@ -79,9 +109,7 @@ class ViewModel < DecoLite::Model
79
109
  end
80
110
  end
81
111
 
82
- view_model = ViewModel.new
83
-
84
- view_model.load(hash: { first: 'John', last: 'Doe' })
112
+ view_model = ViewModel.new(hash: { first: 'John', last: 'Doe' })
85
113
 
86
114
  view_model.valid?
87
115
  #=> true
@@ -96,23 +124,7 @@ view_model.salutation
96
124
 
97
125
  Get creative. Please pop me an email and let me know how _you're_ using _Deco_.
98
126
 
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
127
+ ## Examples and usage
116
128
 
117
129
  ```ruby
118
130
  require 'deco_lite'
@@ -154,8 +166,8 @@ end
154
166
 
155
167
  couple = Couple.new
156
168
 
157
- couple.load(hash: husband, options: { namespace: :husband })
158
- couple.load(hash: wife, options: { namespace: :wife })
169
+ couple.load!(hash: husband, options: { namespace: :husband })
170
+ couple.load!(hash: wife, options: { namespace: :wife })
159
171
 
160
172
  # Will produce the following:
161
173
  model.live_together? #=> true
@@ -169,17 +181,128 @@ model.wife_name #=> Amy Doe
169
181
  model.wife_info_age #=> 20
170
182
  model.wife_info_address #=> 1 street, boonton, nj 07005
171
183
  ```
184
+ ## More examples and usage
185
+
186
+ ### I want to...
187
+
188
+ #### Add validators to my model
189
+
190
+ 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.):
191
+
192
+ ```ruby
193
+ class Model < DecoLite::Model
194
+ validates :first, :last, :address, presence: true
195
+ validates :age, numericality: true
196
+ end
197
+
198
+ # No :address
199
+ model = Model.new(hash: { first: 'John', last: 'Doe', age: 25 })
200
+ model.respond_to? :address
201
+ #=> true
202
+
203
+ model.valid?
204
+ #=> false
205
+ model.errors.full_messages
206
+ #=> ["Address can't be blank"]
207
+
208
+ model.load!(hash: { address: '123 park road, anytown, nj 01234' })
209
+ model.validate
210
+ #=> true
211
+ ```
212
+
213
+ #### Validate whether or not certain fields were loaded
214
+
215
+ 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.
216
+
217
+ 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:
218
+ - Create a `DecoLite::Model` subclass.
219
+ - Override the `DecoLite::Model#required_fields` method to return the field names you want to validate.
220
+ - Use the `required_fields: nil` option when instantiating your model object.
221
+ - 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.
222
+
223
+ For example:
224
+
225
+ ```ruby
226
+ class Model < DecoLite::Model
227
+ # :age field is optional and it's value is optional.
228
+ validates :age, numericality: { only_integer: true }, allow_blank: true
229
+
230
+ def required_fields
231
+ # We want to ensure attr_accessors are created for these fields.
232
+ %i(first last address)
233
+ end
234
+ end
235
+
236
+ # Option "required_fields: :auto" is the default which will automatically create
237
+ # attr_accessors for fields returned from DecoLite::Model#required_fields, so we
238
+ # need to set this option to nil (i.e. required_fields: nil).
239
+ model = Model.new(options: { required_fields: nil })
240
+
241
+ model.validate
242
+ #=> false
243
+ model.errors.full_messages
244
+ #=> ["First field is missing", "Last field is missing", "Address field is missing"]
245
+
246
+ user = { first: 'John', last: 'Doe', address: '123 anystreet, anytown, nj 01234'}
247
+ model.load!(hash: user)
248
+ model.validate
249
+ #=> false
250
+ model.errors.full_messages
251
+ #=> ["Age is not a number"]
252
+ ```
253
+ #### Validate whether or not certain fields were loaded _and_ validate the data associated with these same fields
254
+
255
+ 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.
256
+
257
+ 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.
258
+
259
+ For example:
260
+
261
+ ```ruby
262
+ class Model < DecoLite::Model
263
+ def required_fields
264
+ %i(first last address age)
265
+ end
266
+
267
+ def validate_required_fields
268
+ super
269
+
270
+ first = self.try(:first)
271
+ errors.add(:first, "can't be blank") if first.nil?
272
+
273
+ last = self.try(:last)
274
+ errors.add(:last, "can't be blank") if last.nil?
275
+
276
+ address = self.try(:address)
277
+ errors.add(:address, "can't be blank") if address.nil?
278
+
279
+ age = self.try(:age)
280
+ errors.add(:age, "can't be blank") if age.nil?
281
+ errors.add(:age, 'is not a number') unless /\d+/ =~ age
282
+ end
283
+ end
284
+ model = Model.new(options: { required_fields: nil })
285
+
286
+ model.validate
287
+ #=> false
288
+
289
+ model.errors.full_messages
290
+ #=> ["First field is missing",
291
+ "Last field is missing",
292
+ "Address field is missing",
293
+ "Age field is missing",
294
+ "First can't be blank",
295
+ "Last can't be blank",
296
+ "Address can't be blank",
297
+ "Age can't be blank",
298
+ "Age is not a number"]
299
+ ```
172
300
 
173
- ### Manually Defining Attributes
301
+ #### Manually define attributes (fields) on my model
174
302
 
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.
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.
181
304
 
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:
305
+ To avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, do the following:
183
306
 
184
307
  ```ruby
185
308
  class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
@@ -193,6 +316,35 @@ class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
193
316
  end
194
317
  ```
195
318
 
319
+ However, the above is unnecessary as this can be easily accomplished using `DecoLite::Model#load!`:
320
+ ```ruby
321
+ model = Class.new(DecoLite::Model).new.load!(hash:{ existing_field: :existing_field_value })
322
+
323
+ model.field_names
324
+ #=> [:existing_field]
325
+
326
+ model.existing_field
327
+ #=> :existing_field_value
328
+
329
+ model.respond_to? :existing_field=
330
+ #=> true
331
+ ```
332
+ ## Installation
333
+
334
+ Add this line to your application's Gemfile:
335
+
336
+ ```ruby
337
+ gem 'deco_lite'
338
+ ```
339
+
340
+ And then execute:
341
+
342
+ $ bundle
343
+
344
+ Or install it yourself as:
345
+
346
+ $ gem install deco_lite
347
+
196
348
  ## Development
197
349
 
198
350
  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,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'field_creatable'
4
3
  require_relative 'field_retrievable'
5
4
 
6
5
  module DecoLite
7
6
  # Defines methods to assign model field values dynamically.
8
7
  module FieldAssignable
9
- include FieldCreatable
10
8
  include FieldRetrievable
11
9
 
12
10
  def set_field_values(hash:, field_info:, options:)
@@ -16,10 +14,10 @@ module DecoLite
16
14
  end
17
15
  end
18
16
 
17
+ # rubocop:disable Lint/UnusedMethodArgument
19
18
  def set_field_value(field_name:, value:, options:)
20
- # Create our fields before we send.
21
- create_field_accessor field_name: field_name, options: options
22
19
  send("#{field_name}=", value)
23
20
  end
21
+ # rubocop:enable Lint/UnusedMethodArgument
24
22
  end
25
23
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'field_name_namespaceable'
4
+ require_relative 'field_requireable'
4
5
  require_relative 'fields_optionable'
5
6
 
6
7
  module DecoLite
@@ -9,6 +10,7 @@ module DecoLite
9
10
  module FieldConflictable
10
11
  include FieldNameNamespaceable
11
12
  include FieldsOptionable
13
+ include FieldRequireable
12
14
 
13
15
  def validate_field_conflicts!(field_name:, options:)
14
16
  return unless field_conflict?(field_name: field_name, options: options)
@@ -19,7 +21,8 @@ module DecoLite
19
21
  ":#{field_name} and/or :#{field_name}=; " \
20
22
  'this will raise an error when loading using strict mode ' \
21
23
  "(i.e. options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }) " \
22
- 'or if the method(s) are native to the object (e.g :to_s, :==, etc.).'
24
+ 'or if the method(s) are native to the object (e.g :to_s, :==, etc.). ' \
25
+ "Current options are: options: #{options.to_h}."
23
26
  end
24
27
 
25
28
  # This method returns true
@@ -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
 
@@ -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
@@ -2,11 +2,13 @@
2
2
 
3
3
  require 'mad_flatter'
4
4
  require_relative 'field_assignable'
5
+ require_relative 'field_creatable'
5
6
 
6
7
  module DecoLite
7
8
  # Provides methods to load and return information about a given hash.
8
9
  module HashLoadable
9
10
  include FieldAssignable
11
+ include FieldCreatable
10
12
 
11
13
  private
12
14
 
@@ -16,8 +18,8 @@ module DecoLite
16
18
  return {} if hash.blank?
17
19
 
18
20
  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|
21
+ load_service.execute(hash: hash, options: load_service_options).tap do |service_hash|
22
+ service_hash.each_pair do |field_name, value|
21
23
  create_field_accessor field_name: field_name, options: deco_lite_options
22
24
  field_names << field_name unless field_names.include? field_name
23
25
  set_field_value(field_name: field_name, value: value, options: deco_lite_options)
@@ -4,8 +4,12 @@ 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|
8
- hash[field_name] = public_send field_name
7
+ field_names.each_with_object({}) do |field_name, hash|
8
+ field_value = public_send(field_name)
9
+
10
+ field_name, field_value = yield [field_name, field_value] if block_given?
11
+
12
+ hash[field_name] = field_value
9
13
  end
10
14
  end
11
15
  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,13 +26,18 @@ module DecoLite
24
26
 
25
27
  validate :validate_required_fields
26
28
 
27
- def initialize(options: {})
29
+ def initialize(hash: {}, options: {})
28
30
  # Accept whatever options are sent, but make sure
29
31
  # we have defaults set up. #options_with_defaults
30
32
  # will merge options into OptionsDefaultable::DEFAULT_OPTIONS
31
33
  # so we have defaults for any options not passed in through
32
34
  # options.
33
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?
34
41
  end
35
42
 
36
43
  def load!(hash:, options: {})
@@ -40,7 +47,6 @@ module DecoLite
40
47
  # options while loading, but also provide option customization
41
48
  # of options when needed.
42
49
  options = Options.with_defaults(options, defaults: self.options)
43
-
44
50
  load_hash(hash: hash, deco_lite_options: options)
45
51
 
46
52
  self
@@ -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:)
@@ -45,5 +48,14 @@ module DecoLite
45
48
  raise ArgumentError, 'option :namespace value or type is invalid. A Symbol was expected, ' \
46
49
  "but '#{namespace}' (#{namespace.class}) was received."
47
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."
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.5'
5
+ VERSION = '0.3.2'
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: 0.2.5
4
+ version: 0.3.2
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-22 00:00:00.000000000 Z
11
+ date: 2022-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -266,6 +266,7 @@ files:
266
266
  - lib/deco_lite/field_requireable.rb
267
267
  - lib/deco_lite/field_retrievable.rb
268
268
  - lib/deco_lite/field_validatable.rb
269
+ - lib/deco_lite/fields_auto_attr_accessable.rb
269
270
  - lib/deco_lite/fields_optionable.rb
270
271
  - lib/deco_lite/hash_loadable.rb
271
272
  - lib/deco_lite/hashable.rb
@@ -276,6 +277,7 @@ files:
276
277
  - lib/deco_lite/options.rb
277
278
  - lib/deco_lite/options_defaultable.rb
278
279
  - lib/deco_lite/options_validatable.rb
280
+ - lib/deco_lite/required_fields_optionable.rb
279
281
  - lib/deco_lite/version.rb
280
282
  homepage: https://github.com/gangelo/deco_lite
281
283
  licenses: