deco_lite 0.2.3 → 0.3.0
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 +4 -4
- data/.reek.yml +1 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +27 -4
- data/Gemfile.lock +1 -1
- data/README.md +120 -37
- data/bin/console +2 -3
- data/lib/deco_lite/field_conflictable.rb +34 -6
- data/lib/deco_lite/field_creatable.rb +24 -1
- data/lib/deco_lite/field_name_namespaceable.rb +17 -0
- data/lib/deco_lite/field_names_persistable.rb +14 -0
- data/lib/deco_lite/field_validatable.rb +16 -0
- data/lib/deco_lite/fields_auto_attr_accessable.rb +30 -0
- data/lib/deco_lite/hash_loadable.rb +2 -2
- data/lib/deco_lite/hashable.rb +1 -1
- data/lib/deco_lite/model.rb +18 -9
- data/lib/deco_lite/options.rb +4 -0
- data/lib/deco_lite/options_defaultable.rb +4 -1
- data/lib/deco_lite/options_validatable.rb +17 -5
- data/lib/deco_lite/required_fields_optionable.rb +15 -0
- data/lib/deco_lite/version.rb +1 -1
- data/lib/deco_lite.rb +2 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40f7b315deefbdf848f9d2743d27e672d3916da793f0bd32a705c7d34acac1f9
|
|
4
|
+
data.tar.gz: b283948784b3fd9b1a4eaef20b7f0ca26a7de9b2971042c04a01021be6195d10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37a1083dfa95d51ab710fff1bab946edcc5b9fa825e37bf7f29c7a0174198d913469c8ac17f82b795949425f7e068236c73ca2ed9df50aba45bdfa3ef7cf1d18
|
|
7
|
+
data.tar.gz: f3fbb8ab71dad6720dae8dc25179977757d55f7b802c2016ffc673fcdd423b3f86d04a406bd9a4bade6233efb5120cad1d7c2d7176045f44ec78da911311419a
|
data/.reek.yml
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
### 0.3.0
|
|
2
|
+
* Changes
|
|
3
|
+
* `DecoLite::Model#new` how accepts a :hash named parameter that will load the Hash as if calling `DecoLite::Model.new.load!(hash: <hash>)`.
|
|
4
|
+
* `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.
|
|
5
|
+
* `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.
|
|
6
|
+
* bin/console now starts a pry-byebug session.
|
|
7
|
+
|
|
8
|
+
### 0.2.5
|
|
9
|
+
* Changes
|
|
10
|
+
* Remove init of `@field_names = []` in `Model#initialize` as unnecessary - FieldNamesPersistable takes care of this.
|
|
11
|
+
* Bug fixes
|
|
12
|
+
* Fix but that does not take into account option :namespace when determining whether or not a field name conflicts with an existing attribute (already exists).
|
|
13
|
+
|
|
14
|
+
### 0.2.4
|
|
15
|
+
* Changes
|
|
16
|
+
* Change DecoLite::Model#load to #load! as it alters the object, give deprecation warning when calling #load.
|
|
17
|
+
* FieldConflictable now expliticly prohibits loading fields that conflict with attributes that are native to the receiver. In other words, you cannot load fields with names like :to_s, :tap, :hash, etc.
|
|
18
|
+
* FieldCreatable now creates attr_accessors on the instance using #define_singleton_method, not at the class level (i.e. self.class.attr_accessor) (see bug fixes).
|
|
19
|
+
* Bug fixes
|
|
20
|
+
* Fix bug that used self.class.attr_accessor in DecoLite::FieldCreatable to create attributes, which forced every object of that class subsequently created have the accessors created which caused field name conflicts across DecoLite::Model objects.
|
|
21
|
+
|
|
1
22
|
### 0.2.3
|
|
2
|
-
*
|
|
23
|
+
* Bug fixes
|
|
24
|
+
* Fix bug that added duplcate field names to Model#field_names.
|
|
3
25
|
|
|
4
26
|
### 0.2.2
|
|
5
|
-
*
|
|
27
|
+
* Bug fixes
|
|
28
|
+
* Fix bug requiring support codez in lib/deco_lite.rb.
|
|
6
29
|
|
|
7
30
|
### 0.2.1
|
|
8
|
-
*
|
|
31
|
+
* Changes
|
|
9
32
|
* Add mad_flatter gem runtime dependency.
|
|
10
33
|
* Refactor to let mad_flatter handle the Hash flattening.
|
|
11
34
|
|
|
12
35
|
### 0.1.1
|
|
13
|
-
*
|
|
36
|
+
* Changes
|
|
14
37
|
* Update gems and especially rake gem version to squash CVE-2020-8130, see https://github.com/advisories/GHSA-jppv-gw3r-w3q8.
|
|
15
38
|
* Fix rubocop violations.
|
|
16
39
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
|
|
16
16
|
DecoLite is in development. I wouldn't expect breaking changes before v1.0.0; however, I can't completely rule this out. Currently, DecoLite only supports Hashes whose keys are `Symbols`, contain no embedded spaces, and conform to Ruby `attr_accessor` naming conventions. However, I'll certainly work out a solution for all this in future releases.
|
|
17
17
|
|
|
18
|
-
TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load
|
|
18
|
+
TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load!` options: how these work, and how they play together (in the meantime, see the specs).
|
|
19
19
|
|
|
20
20
|
_Deco_ is a little gem that allows you to use the provided `DecoLite::Model` class (`include ActiveModel::Model`) to create Decorator classes which can be instantiated and used. Inherit from `DecoLite::Model` to create your own unique classes with custom functionality. A `DecoLite::Model` includes `ActiveModel::Model`, so validation can be applied using [ActiveModel validation helpers](https://api.rubyonrails.org/v6.1.3/classes/ActiveModel/Validations/HelperMethods.html) you are familiar with; or, you can roll your own - just like any other ActiveModel.
|
|
21
21
|
|
|
22
|
-
A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the `DecoLite::Model#load
|
|
22
|
+
A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the `DecoLite::Model#load!` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded.
|
|
23
23
|
|
|
24
24
|
`attr_accessor` names created are _mangled_ to include namespacing. This creates unique attribute names for nested Hashes that may include non-unique keys. For example:
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
27
|
# NOTE: keys :name and :age are not unique across this Hash.
|
|
28
|
-
{
|
|
28
|
+
family = {
|
|
29
29
|
name: 'John Doe',
|
|
30
30
|
age: 35,
|
|
31
31
|
wife: {
|
|
@@ -34,37 +34,66 @@ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via th
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
|
-
Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load
|
|
37
|
+
Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load!`), and assign the values:
|
|
38
38
|
|
|
39
39
|
```ruby
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
model = DecoLite::Model.new.load!(hash: family)
|
|
41
|
+
|
|
42
|
+
model.name #=> 'John Doe'
|
|
43
|
+
model.respond_to? :name= #=> true
|
|
44
|
+
|
|
45
|
+
model.age #=> 35
|
|
46
|
+
model.respond_to? :age= #=> true
|
|
47
|
+
|
|
48
|
+
model.wife_name #=> 'Mary Doe'
|
|
49
|
+
model.respond_to? :wife_name= #=> true
|
|
50
|
+
|
|
51
|
+
model.wife_age #=> 30
|
|
52
|
+
model.respond_to? :wife_age= #=> true
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
`DecoLite::Model#load
|
|
55
|
+
`DecoLite::Model#load!` can be called _multiple times_, on the same model, with different Hashes. This could potentially cause `attr_accessor` name clashes. In order to ensure unique `attr_accessor` names, a _"namespace"_ may be _explicitly_ provided to ensure uniqueness. For example, continuing from the previous example; if we were to call `DecoLite::Model#load!` a _second time_ with the following Hash, it would produce `attr_accessor` name clashes:
|
|
47
56
|
|
|
48
57
|
```ruby
|
|
49
|
-
{
|
|
58
|
+
grandpa = {
|
|
50
59
|
name: 'Henry Doe',
|
|
51
60
|
age: 85,
|
|
52
61
|
}
|
|
62
|
+
# The :name and :age Hash keys above will produce :name/:name= and :age/:age= attr_accessors and clash because these were already added to the model when "John Doe" was loaded with the first call to DecoLite::Model#load!.
|
|
53
63
|
```
|
|
54
64
|
|
|
55
|
-
However, passing a `namespace: :grandpa`
|
|
65
|
+
However, passing a `namespace:` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method, would produce the following `attr_accessors`, ensuring their uniqueness:
|
|
66
|
+
|
|
56
67
|
```ruby
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
model.load!(hash: grandpa, options: { namespace: :grandpa })
|
|
69
|
+
|
|
70
|
+
# Unique now that the namespace: :grandpa has been applied:
|
|
71
|
+
model.grandpa_name #=> 'Henry Doe'
|
|
72
|
+
model.respond_to? :grandpa_name= #=> true
|
|
73
|
+
|
|
74
|
+
model.grandpa_age #=> 85
|
|
75
|
+
model.respond_to? :grandpa_age= #=> true
|
|
76
|
+
|
|
77
|
+
# All the other attributes on the model remain the same, and unique:
|
|
78
|
+
model.name #=> 'John Doe'
|
|
79
|
+
model.respond_to? :name= #=> true
|
|
80
|
+
|
|
81
|
+
model.age #=> 35
|
|
82
|
+
model.respond_to? :age= #=> true
|
|
83
|
+
|
|
84
|
+
model.wife_name #=> 'Mary Doe'
|
|
85
|
+
model.respond_to? :wife_name= #=> true
|
|
86
|
+
|
|
87
|
+
model.wife_age #=> 30
|
|
88
|
+
model.respond_to? :wife_age= #=> true
|
|
60
89
|
```
|
|
61
|
-
## Use
|
|
90
|
+
## Use cases
|
|
62
91
|
|
|
63
92
|
### General
|
|
64
|
-
_Deco_ would _most likely_ thrive where the structure of the Hashe(s) consumed by the `DecoLite::Model#load
|
|
93
|
+
_Deco_ would _most likely_ thrive where the structure of the Hashe(s) consumed by the `DecoLite::Model#load!` method is known. This is because of the way _Deco_ mangles loaded Hash key names to create unique `attr_accessor` names (see the Introduction section); although, I'm sure there are some metaprogramming geniuses out there that might prove me wrong. Assuming this is the case, _Deco_ would be ideal to handle Model attributes, Webservice JSON results (converted to Ruby Hash), JSON Web Token (JWT) payload, etc..
|
|
65
94
|
|
|
66
95
|
### Rails
|
|
67
|
-
Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_
|
|
96
|
+
Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_ might be used, and decorator methods provided for use in Rails views; for example:
|
|
68
97
|
|
|
69
98
|
```ruby
|
|
70
99
|
class ViewModel < DecoLite::Model
|
|
@@ -81,7 +110,7 @@ end
|
|
|
81
110
|
|
|
82
111
|
view_model = ViewModel.new
|
|
83
112
|
|
|
84
|
-
view_model.load(hash: { first: 'John', last: 'Doe' })
|
|
113
|
+
view_model.load!(hash: { first: 'John', last: 'Doe' })
|
|
85
114
|
|
|
86
115
|
view_model.valid?
|
|
87
116
|
#=> true
|
|
@@ -96,23 +125,7 @@ view_model.salutation
|
|
|
96
125
|
|
|
97
126
|
Get creative. Please pop me an email and let me know how _you're_ using _Deco_.
|
|
98
127
|
|
|
99
|
-
##
|
|
100
|
-
|
|
101
|
-
Add this line to your application's Gemfile:
|
|
102
|
-
|
|
103
|
-
```ruby
|
|
104
|
-
gem 'deco_lite'
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
And then execute:
|
|
108
|
-
|
|
109
|
-
$ bundle
|
|
110
|
-
|
|
111
|
-
Or install it yourself as:
|
|
112
|
-
|
|
113
|
-
$ gem install deco_lite
|
|
114
|
-
|
|
115
|
-
## Examples and Usage
|
|
128
|
+
## Examples and usage
|
|
116
129
|
|
|
117
130
|
```ruby
|
|
118
131
|
require 'deco_lite'
|
|
@@ -154,8 +167,8 @@ end
|
|
|
154
167
|
|
|
155
168
|
couple = Couple.new
|
|
156
169
|
|
|
157
|
-
couple.load(hash: husband, options: { namespace: :husband })
|
|
158
|
-
couple.load(hash: wife, options: { namespace: :wife })
|
|
170
|
+
couple.load!(hash: husband, options: { namespace: :husband })
|
|
171
|
+
couple.load!(hash: wife, options: { namespace: :wife })
|
|
159
172
|
|
|
160
173
|
# Will produce the following:
|
|
161
174
|
model.live_together? #=> true
|
|
@@ -169,6 +182,76 @@ model.wife_name #=> Amy Doe
|
|
|
169
182
|
model.wife_info_age #=> 20
|
|
170
183
|
model.wife_info_address #=> 1 street, boonton, nj 07005
|
|
171
184
|
```
|
|
185
|
+
## More examples and usage
|
|
186
|
+
|
|
187
|
+
### I want to...
|
|
188
|
+
|
|
189
|
+
#### Add validators to my model
|
|
190
|
+
|
|
191
|
+
Simply add your `ActiveModel` validators just like you would any other `ActiveModel::Model` validator. However, be aware that (currently), any attribute (field) being validated that does not exist on the model, will raise a `NoMethodError` error:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
class Model < DecoLite::Model
|
|
195
|
+
validates :first, :last, :address, presence: true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
model = Model.new(hash: { first: 'John', last: 'Doe' })
|
|
199
|
+
|
|
200
|
+
# No :address; a NoMethodError error for :address will be raised.
|
|
201
|
+
model.validate
|
|
202
|
+
#=> undefined method 'address' for #<Model:0x00007f95931568c0 ... @field_names=[:first, :last], @first="John", @last="Doe", ...> (NoMethodError)
|
|
203
|
+
|
|
204
|
+
model.load!(hash: { address: '123 park road, anytown, nj 01234' })
|
|
205
|
+
model.validate
|
|
206
|
+
#=> true
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Manually define attributes (fields) on my model
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
To avoid errors when manually defining model attributes that could potentially conflict with fields loaded using `DecoLite::Model#load!`, do the following:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
|
|
217
|
+
attr_accessor :existing_field
|
|
218
|
+
|
|
219
|
+
def initialize(options: {})
|
|
220
|
+
super
|
|
221
|
+
|
|
222
|
+
@field_names = %i(existing_field)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
However, the above is unnecessary as this can be easily accomplished using `DecoLite::Model#load!`:
|
|
228
|
+
```ruby
|
|
229
|
+
model = Class.new(DecoLite::Model).new.load!(hash:{ existing_field: :existing_field_value })
|
|
230
|
+
|
|
231
|
+
model.field_names
|
|
232
|
+
#=> [:existing_field]
|
|
233
|
+
|
|
234
|
+
model.existing_field
|
|
235
|
+
#=> :existing_field_value
|
|
236
|
+
|
|
237
|
+
model.respond_to? :existing_field=
|
|
238
|
+
#=> true
|
|
239
|
+
```
|
|
240
|
+
## Installation
|
|
241
|
+
|
|
242
|
+
Add this line to your application's Gemfile:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
gem 'deco_lite'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
And then execute:
|
|
249
|
+
|
|
250
|
+
$ bundle
|
|
251
|
+
|
|
252
|
+
Or install it yourself as:
|
|
253
|
+
|
|
254
|
+
$ gem install deco_lite
|
|
172
255
|
|
|
173
256
|
## Development
|
|
174
257
|
|
data/bin/console
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'field_name_namespaceable'
|
|
3
4
|
require_relative 'fields_optionable'
|
|
4
5
|
|
|
5
6
|
module DecoLite
|
|
6
7
|
# Defines methods to to manage fields that conflict with
|
|
7
8
|
# existing model attributes.
|
|
8
9
|
module FieldConflictable
|
|
10
|
+
include FieldNameNamespaceable
|
|
9
11
|
include FieldsOptionable
|
|
10
12
|
|
|
11
13
|
def validate_field_conflicts!(field_name:, options:)
|
|
12
|
-
return unless
|
|
14
|
+
return unless field_conflict?(field_name: field_name, options: options)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
|
|
17
|
+
|
|
18
|
+
raise "Field :#{field_name} conflicts with existing method(s) " \
|
|
19
|
+
":#{field_name} and/or :#{field_name}=; " \
|
|
20
|
+
'this will raise an error when loading using strict mode ' \
|
|
21
|
+
"(i.e. options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }) " \
|
|
22
|
+
'or if the method(s) are native to the object (e.g :to_s, :==, etc.). ' \
|
|
23
|
+
"Current options are: options: #{options.to_h}."
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# This method returns true
|
|
27
|
+
def field_conflict?(field_name:, options:)
|
|
28
|
+
# If field_name was already added using Model#load, there is only a
|
|
29
|
+
# conflict if options.strict? is true.
|
|
30
|
+
return options.strict? if field_names_include?(field_name: field_name, options: options)
|
|
31
|
+
|
|
32
|
+
# If we get here, we know that :field_name does not exist as an
|
|
33
|
+
# attribute on the model. If the attribute already exists on the
|
|
34
|
+
# model, this is a conflict because we cannot override an attribute
|
|
35
|
+
# that already exists on the model
|
|
36
|
+
attr_accessor_exist?(field_name: field_name, options: options)
|
|
17
37
|
end
|
|
18
38
|
|
|
19
|
-
def
|
|
20
|
-
|
|
39
|
+
def field_names_include?(field_name:, options:)
|
|
40
|
+
field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
|
|
41
|
+
|
|
42
|
+
field_names.include? field_name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def attr_accessor_exist?(field_name:, options:)
|
|
46
|
+
field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
|
|
47
|
+
|
|
48
|
+
respond_to?(field_name) || respond_to?(:"#{field_name}=")
|
|
21
49
|
end
|
|
22
50
|
end
|
|
23
51
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'field_conflictable'
|
|
4
|
+
require_relative 'field_validatable'
|
|
4
5
|
|
|
5
6
|
module DecoLite
|
|
6
7
|
# Takes an array of symbols and creates attr_accessors.
|
|
7
8
|
module FieldCreatable
|
|
8
9
|
include FieldConflictable
|
|
10
|
+
include FieldValidatable
|
|
9
11
|
|
|
10
12
|
def create_field_accessors(field_names:, options:)
|
|
11
13
|
return if field_names.blank?
|
|
@@ -16,9 +18,30 @@ module DecoLite
|
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def create_field_accessor(field_name:, options:)
|
|
21
|
+
validate_field_name!(field_name: field_name, options: options)
|
|
19
22
|
validate_field_conflicts!(field_name: field_name, options: options)
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
# If we want to set a class-level attr_accessor
|
|
25
|
+
# self.class.attr_accessor(field_name) if field_name.present?
|
|
26
|
+
|
|
27
|
+
create_field_getter field_name: field_name, options: options
|
|
28
|
+
create_field_setter field_name: field_name, options: options
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
34
|
+
def create_field_getter(field_name:, options:)
|
|
35
|
+
define_singleton_method(field_name) do
|
|
36
|
+
instance_variable_get "@#{field_name}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_field_setter(field_name:, options:)
|
|
41
|
+
define_singleton_method("#{field_name}=") do |value|
|
|
42
|
+
instance_variable_set "@#{field_name}", value
|
|
43
|
+
end
|
|
22
44
|
end
|
|
45
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
23
46
|
end
|
|
24
47
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecoLite
|
|
4
|
+
# Defines methods to transform a field name into a field name
|
|
5
|
+
# with a namespace.
|
|
6
|
+
module FieldNameNamespaceable
|
|
7
|
+
def field_name_or_field_name_with_namespace(field_name:, options:)
|
|
8
|
+
return field_name unless options.namespace?
|
|
9
|
+
|
|
10
|
+
field_name_with_namespace(field_name: field_name, namespace: options.namespace)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def field_name_with_namespace(field_name:, namespace:)
|
|
14
|
+
"#{namespace}_#{field_name}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecoLite
|
|
4
|
+
# Takes an array of symbols and creates attr_accessors.
|
|
5
|
+
module FieldNamesPersistable
|
|
6
|
+
def field_names
|
|
7
|
+
@field_names ||= instance_variable_get(:@field_names) || []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_writer :field_names
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecoLite
|
|
4
|
+
# Defines methods validate field (attribute) names.
|
|
5
|
+
module FieldValidatable
|
|
6
|
+
FIELD_NAME_REGEX = %r{\A(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+*/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)\z}i
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
11
|
+
def validate_field_name!(field_name:, options: nil)
|
|
12
|
+
raise "field_name '#{field_name}' is not a valid field name." unless FIELD_NAME_REGEX.match?(field_name)
|
|
13
|
+
end
|
|
14
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecoLite
|
|
4
|
+
# Defines fields that may have attr_accessors automatically created for them
|
|
5
|
+
# on the model.
|
|
6
|
+
module FieldsAutoloadable
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def auto_attr_accessors?
|
|
10
|
+
auto_attr_accessors.present?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# This method returns a Hash of fields that are implicitly defined either
|
|
14
|
+
# through ActiveModel validators or by returning them from the
|
|
15
|
+
# #required_fields Array.
|
|
16
|
+
def auto_attr_accessors
|
|
17
|
+
return @auto_attr_accessors.dup if defined?(@auto_attr_accessors)
|
|
18
|
+
|
|
19
|
+
@auto_attr_accessors = self.class.validators.map(&:attributes)
|
|
20
|
+
@auto_attr_accessors.concat(required_fields) if options.required_fields_auto?
|
|
21
|
+
@auto_attr_accessors = auto_attr_accessors_assign
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def auto_attr_accessors_assign
|
|
25
|
+
@auto_attr_accessors.flatten.uniq.each_with_object({}) do |field_name, auto_attr_accessors_hash|
|
|
26
|
+
auto_attr_accessors_hash[field_name] = nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -16,8 +16,8 @@ module DecoLite
|
|
|
16
16
|
return {} if hash.blank?
|
|
17
17
|
|
|
18
18
|
load_service_options = merge_with_load_service_options deco_lite_options: deco_lite_options
|
|
19
|
-
load_service.execute(hash: hash, options: load_service_options).tap do |
|
|
20
|
-
|
|
19
|
+
load_service.execute(hash: hash, options: load_service_options).tap do |service_hash|
|
|
20
|
+
service_hash.each_pair do |field_name, value|
|
|
21
21
|
create_field_accessor field_name: field_name, options: deco_lite_options
|
|
22
22
|
field_names << field_name unless field_names.include? field_name
|
|
23
23
|
set_field_value(field_name: field_name, value: value, options: deco_lite_options)
|
data/lib/deco_lite/hashable.rb
CHANGED
|
@@ -4,7 +4,7 @@ module DecoLite
|
|
|
4
4
|
# Provides methods to convert the object to a Hash.
|
|
5
5
|
module Hashable
|
|
6
6
|
def to_h
|
|
7
|
-
field_names.
|
|
7
|
+
field_names.each_with_object({}) do |field_name, hash|
|
|
8
8
|
hash[field_name] = public_send field_name
|
|
9
9
|
end
|
|
10
10
|
end
|
data/lib/deco_lite/model.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_model'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative 'field_assignable'
|
|
5
|
+
require_relative 'field_names_persistable'
|
|
5
6
|
require_relative 'field_requireable'
|
|
7
|
+
require_relative 'fields_auto_attr_accessable'
|
|
6
8
|
require_relative 'hash_loadable'
|
|
7
9
|
require_relative 'hashable'
|
|
8
10
|
require_relative 'model_nameable'
|
|
@@ -13,8 +15,10 @@ module DecoLite
|
|
|
13
15
|
# dynamic models that can be used as decorators.
|
|
14
16
|
class Model
|
|
15
17
|
include ActiveModel::Model
|
|
16
|
-
include
|
|
18
|
+
include FieldAssignable
|
|
19
|
+
include FieldNamesPersistable
|
|
17
20
|
include FieldRequireable
|
|
21
|
+
include FieldsAutoloadable
|
|
18
22
|
include HashLoadable
|
|
19
23
|
include Hashable
|
|
20
24
|
include ModelNameable
|
|
@@ -22,17 +26,21 @@ module DecoLite
|
|
|
22
26
|
|
|
23
27
|
validate :validate_required_fields
|
|
24
28
|
|
|
25
|
-
def initialize(options: {})
|
|
26
|
-
@field_names = []
|
|
29
|
+
def initialize(hash: {}, options: {})
|
|
27
30
|
# Accept whatever options are sent, but make sure
|
|
28
31
|
# we have defaults set up. #options_with_defaults
|
|
29
32
|
# will merge options into OptionsDefaultable::DEFAULT_OPTIONS
|
|
30
33
|
# so we have defaults for any options not passed in through
|
|
31
34
|
# options.
|
|
32
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?
|
|
33
41
|
end
|
|
34
42
|
|
|
35
|
-
def load(hash:, options: {})
|
|
43
|
+
def load!(hash:, options: {})
|
|
36
44
|
# Merge options into the default options passed through the
|
|
37
45
|
# constructor; these will override any options passed in when
|
|
38
46
|
# this object was created, allowing us to retain any defaut
|
|
@@ -45,10 +53,11 @@ module DecoLite
|
|
|
45
53
|
self
|
|
46
54
|
end
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
def load(hash:, options: {})
|
|
57
|
+
puts 'WARNING: DecoLite::Model#load will be deprecated in a future release; ' \
|
|
58
|
+
'use DecoLite::Model#load! instead!'
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
load!(hash: hash, options: options)
|
|
61
|
+
end
|
|
53
62
|
end
|
|
54
63
|
end
|
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:)
|
|
@@ -35,7 +38,7 @@ module DecoLite
|
|
|
35
38
|
|
|
36
39
|
raise ArgumentError,
|
|
37
40
|
"option :fields value or type is invalid. #{OPTION_FIELDS_VALUES} (Symbol) " \
|
|
38
|
-
|
|
41
|
+
"was expected, but '#{fields}' (#{fields.class}) was received."
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def validate_option_namespace!(namespace:)
|
|
@@ -43,7 +46,16 @@ module DecoLite
|
|
|
43
46
|
return if namespace.blank? || namespace.is_a?(Symbol)
|
|
44
47
|
|
|
45
48
|
raise ArgumentError, 'option :namespace value or type is invalid. A Symbol was expected, ' \
|
|
46
|
-
|
|
49
|
+
"but '#{namespace}' (#{namespace.class}) was received."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_option_required_fields!(required_fields:)
|
|
53
|
+
# :required_fields is optional.
|
|
54
|
+
return if required_fields.blank? || OPTION_REQUIRED_FIELDS_VALUES.include?(required_fields)
|
|
55
|
+
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"option :fields_required value or type is invalid. #{OPTION_REQUIRED_FIELDS_VALUES} (Symbol) " \
|
|
58
|
+
"was expected, but '#{required_fields}' (#{required_fields.class}) was received."
|
|
47
59
|
end
|
|
48
60
|
end
|
|
49
61
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DecoLite
|
|
4
|
+
# Defines the fields option hash key and acceptable hash key values.
|
|
5
|
+
module RequiredFieldsOptionable
|
|
6
|
+
# The option hash key for this option.
|
|
7
|
+
OPTION_REQUIRED_FIELDS = :required_fields
|
|
8
|
+
# The valid option values for this option key.
|
|
9
|
+
OPTION_REQUIRED_FIELDS_AUTO = :auto
|
|
10
|
+
# The default value for this option.
|
|
11
|
+
OPTION_REQUIRED_FIELDS_DEFAULT = OPTION_REQUIRED_FIELDS_AUTO
|
|
12
|
+
# The valid option key values for this option.
|
|
13
|
+
OPTION_REQUIRED_FIELDS_VALUES = [OPTION_REQUIRED_FIELDS_AUTO].freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/deco_lite/version.rb
CHANGED
data/lib/deco_lite.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative 'deco_lite/field_assignable'
|
|
4
4
|
require_relative 'deco_lite/field_conflictable'
|
|
5
5
|
require_relative 'deco_lite/field_creatable'
|
|
6
|
+
require_relative 'deco_lite/field_name_namespaceable'
|
|
7
|
+
require_relative 'deco_lite/field_names_persistable'
|
|
6
8
|
require_relative 'deco_lite/field_requireable'
|
|
7
9
|
require_relative 'deco_lite/field_retrievable'
|
|
8
10
|
require_relative 'deco_lite/fields_optionable'
|
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.
|
|
4
|
+
version: 0.3.0
|
|
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-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activemodel
|
|
@@ -261,8 +261,12 @@ files:
|
|
|
261
261
|
- lib/deco_lite/field_assignable.rb
|
|
262
262
|
- lib/deco_lite/field_conflictable.rb
|
|
263
263
|
- lib/deco_lite/field_creatable.rb
|
|
264
|
+
- lib/deco_lite/field_name_namespaceable.rb
|
|
265
|
+
- lib/deco_lite/field_names_persistable.rb
|
|
264
266
|
- lib/deco_lite/field_requireable.rb
|
|
265
267
|
- lib/deco_lite/field_retrievable.rb
|
|
268
|
+
- lib/deco_lite/field_validatable.rb
|
|
269
|
+
- lib/deco_lite/fields_auto_attr_accessable.rb
|
|
266
270
|
- lib/deco_lite/fields_optionable.rb
|
|
267
271
|
- lib/deco_lite/hash_loadable.rb
|
|
268
272
|
- lib/deco_lite/hashable.rb
|
|
@@ -273,6 +277,7 @@ files:
|
|
|
273
277
|
- lib/deco_lite/options.rb
|
|
274
278
|
- lib/deco_lite/options_defaultable.rb
|
|
275
279
|
- lib/deco_lite/options_validatable.rb
|
|
280
|
+
- lib/deco_lite/required_fields_optionable.rb
|
|
276
281
|
- lib/deco_lite/version.rb
|
|
277
282
|
homepage: https://github.com/gangelo/deco_lite
|
|
278
283
|
licenses:
|