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 +4 -4
- data/.gitignore +1 -1
- data/.reek.yml +1 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +200 -48
- data/bin/console +2 -3
- data/lib/deco_lite/field_assignable.rb +2 -4
- data/lib/deco_lite/field_conflictable.rb +4 -1
- data/lib/deco_lite/field_requireable.rb +3 -1
- data/lib/deco_lite/fields_auto_attr_accessable.rb +30 -0
- data/lib/deco_lite/hash_loadable.rb +4 -2
- data/lib/deco_lite/hashable.rb +6 -2
- data/lib/deco_lite/model.rb +11 -5
- data/lib/deco_lite/options.rb +4 -0
- data/lib/deco_lite/options_defaultable.rb +4 -1
- data/lib/deco_lite/options_validatable.rb +15 -3
- data/lib/deco_lite/required_fields_optionable.rb +15 -0
- data/lib/deco_lite/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5b5ed040bd367962dd57760a6a0e572b60485ba1d2d3630980ff1a090ca9075
|
4
|
+
data.tar.gz: 76402762f17d93fd53495ddabf86487803b0014971db007f61f505727e933f56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 584e1c07a160370bc9548e7a41dbaa9adcbee757a35a37925d02accf4ebe71d24cd9d2932e706724f5d8d64e8d98ff77f223224de27e5a0d817eb2675fea81ca
|
7
|
+
data.tar.gz: 6a49a3ab8bdcb947341697577e130dd667c508095578536e8d003cfa1424f8e090f3c40e9ddc1376a3337cdcc9a26a02e836bc901c0ae36da39a8acaaae8cc46
|
data/.gitignore
CHANGED
data/.reek.yml
CHANGED
data/.rubocop.yml
CHANGED
@@ -13,7 +13,7 @@ AllCops:
|
|
13
13
|
- '*.gemspec'
|
14
14
|
- 'spec/**/*'
|
15
15
|
- 'vendor/**/*'
|
16
|
-
- 'scratch
|
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
|
-
|
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
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
|
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
|
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#
|
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
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
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`
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
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
|
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_
|
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
|
-
##
|
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
|
-
|
301
|
+
#### Manually define attributes (fields) on my model
|
174
302
|
|
175
|
-
Manually defining attributes on your subclass is possible
|
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
|
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
@@ -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:
|
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 |
|
20
|
-
|
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)
|
data/lib/deco_lite/hashable.rb
CHANGED
@@ -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.
|
8
|
-
|
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
|
data/lib/deco_lite/model.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'active_model'
|
4
|
-
require_relative '
|
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
|
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
|
data/lib/deco_lite/options.rb
CHANGED
@@ -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[
|
21
|
-
validate_option_namespace! namespace: options[
|
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
|
data/lib/deco_lite/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deco_lite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2
|
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-
|
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:
|