deco_lite 0.2.5 → 0.3.2

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: 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: