deco_lite 0.2.3 → 0.3.0

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: f9cc4e271606535b0bb5daf708487f63f68c193709e4a2e2efa4e7c7fee60d49
4
- data.tar.gz: d34b33b97c7b1554c35dc064e2a52908d9e007ee90608a876cbfac944d6d79e8
3
+ metadata.gz: 40f7b315deefbdf848f9d2743d27e672d3916da793f0bd32a705c7d34acac1f9
4
+ data.tar.gz: b283948784b3fd9b1a4eaef20b7f0ca26a7de9b2971042c04a01021be6195d10
5
5
  SHA512:
6
- metadata.gz: 785614c89432bb0dd43ec4fa24a3760205c606d5a25a16e7392d569a57b2a8509c307adbe53f622a80a509cbe88b17535bc0bb484071415f51252704a096782b
7
- data.tar.gz: 28c95fb72bc380e4a568680375cbdaa03105aa0d7c05cd7f910c61bb910f0aa9a6a5056a5ed3254e11df4755b6c88a7ac6bfe932f3bfded7ebcd4003c5513749
6
+ metadata.gz: 37a1083dfa95d51ab710fff1bab946edcc5b9fa825e37bf7f29c7a0174198d913469c8ac17f82b795949425f7e068236c73ca2ed9df50aba45bdfa3ef7cf1d18
7
+ data.tar.gz: f3fbb8ab71dad6720dae8dc25179977757d55f7b802c2016ffc673fcdd423b3f86d04a406bd9a4bade6233efb5120cad1d7c2d7176045f44ec78da911311419a
data/.reek.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  exclude_paths:
2
2
  - vendor
3
3
  - spec
4
+ - scratch.rb
4
5
  detectors:
5
6
  # TooManyInstanceVariables:
6
7
  # exclude:
data/.rubocop.yml CHANGED
@@ -116,7 +116,7 @@ Layout/LineLength:
116
116
  # Avoid methods longer than 15 lines of code.
117
117
  Metrics/MethodLength:
118
118
  Max: 20
119
- IgnoredMethods:
119
+ AllowedMethods:
120
120
  - swagger_path
121
121
  - operation
122
122
 
data/CHANGELOG.md CHANGED
@@ -1,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
- * Fix bug that added duplcate field names to Model#field_names.
23
+ * Bug fixes
24
+ * Fix bug that added duplcate field names to Model#field_names.
3
25
 
4
26
  ### 0.2.2
5
- * Fix bug requiring support codez in lib/deco_lite.rb.
27
+ * Bug fixes
28
+ * Fix bug requiring support codez in lib/deco_lite.rb.
6
29
 
7
30
  ### 0.2.1
8
- * changes
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
- * changes
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (0.2.3)
4
+ deco_lite (0.3.0)
5
5
  activemodel (~> 7.0, >= 7.0.3.1)
6
6
  activesupport (~> 7.0, >= 7.0.3.1)
7
7
  immutable_struct_ex (~> 0.2.0)
data/README.md CHANGED
@@ -15,17 +15,17 @@
15
15
 
16
16
  DecoLite is in development. I wouldn't expect breaking changes before v1.0.0; however, I can't completely rule this out. Currently, DecoLite only supports Hashes whose keys are `Symbols`, contain no embedded spaces, and conform to Ruby `attr_accessor` naming conventions. However, I'll certainly work out a solution for all this in future releases.
17
17
 
18
- TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load` options: how these work, and how they play together (in the meantime, see the specs).
18
+ TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load!` options: how these work, and how they play together (in the meantime, see the specs).
19
19
 
20
20
  _Deco_ is a little gem that allows you to use the provided `DecoLite::Model` class (`include ActiveModel::Model`) to create Decorator classes which can be instantiated and used. Inherit from `DecoLite::Model` to create your own unique classes with custom functionality. A `DecoLite::Model` includes `ActiveModel::Model`, so validation can be applied using [ActiveModel validation helpers](https://api.rubyonrails.org/v6.1.3/classes/ActiveModel/Validations/HelperMethods.html) you are familiar with; or, you can roll your own - just like any other ActiveModel.
21
21
 
22
- A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the `DecoLite::Model#load` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded.
22
+ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the `DecoLite::Model#load!` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded.
23
23
 
24
24
  `attr_accessor` names created are _mangled_ to include namespacing. This creates unique attribute names for nested Hashes that may include non-unique keys. For example:
25
25
 
26
26
  ```ruby
27
27
  # NOTE: keys :name and :age are not unique across this Hash.
28
- {
28
+ family = {
29
29
  name: 'John Doe',
30
30
  age: 35,
31
31
  wife: {
@@ -34,37 +34,66 @@ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via th
34
34
  }
35
35
  }
36
36
  ```
37
- Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load`), and assign the values:
37
+ Given the above example, DecoLite will produce the following `attr_accessors` on the `DecoLite::Model` object when loaded (`DecoLite::Model#load!`), and assign the values:
38
38
 
39
39
  ```ruby
40
- name=, name #=> 'John Doe'
41
- age=, age #=> 35
42
- wife_name=, wife_name #=> 'Mary Doe'
43
- wife_age=, wife_age #=> 30
40
+ model = DecoLite::Model.new.load!(hash: family)
41
+
42
+ model.name #=> 'John Doe'
43
+ model.respond_to? :name= #=> true
44
+
45
+ model.age #=> 35
46
+ model.respond_to? :age= #=> true
47
+
48
+ model.wife_name #=> 'Mary Doe'
49
+ model.respond_to? :wife_name= #=> true
50
+
51
+ model.wife_age #=> 30
52
+ model.respond_to? :wife_age= #=> true
44
53
  ```
45
54
 
46
- `DecoLite::Model#load` can be called _multiple times_, on the same model, with different Hashes. This could potentially cause `attr_accessor` name clashes. In order to ensure unique `attr_accessor` names, a _"namespace"_ may be _explicitly_ provided to ensure uniqueness. For example, continuing from the previous example; if we were to call `DecoLite::Model#load` a _second time_ with the following Hash, it would produce `attr_accessor` name clashes:
55
+ `DecoLite::Model#load!` can be called _multiple times_, on the same model, with different Hashes. This could potentially cause `attr_accessor` name clashes. In order to ensure unique `attr_accessor` names, a _"namespace"_ may be _explicitly_ provided to ensure uniqueness. For example, continuing from the previous example; if we were to call `DecoLite::Model#load!` a _second time_ with the following Hash, it would produce `attr_accessor` name clashes:
47
56
 
48
57
  ```ruby
49
- {
58
+ grandpa = {
50
59
  name: 'Henry Doe',
51
60
  age: 85,
52
61
  }
62
+ # The :name and :age Hash keys above will produce :name/:name= and :age/:age= attr_accessors and clash because these were already added to the model when "John Doe" was loaded with the first call to DecoLite::Model#load!.
53
63
  ```
54
64
 
55
- However, passing a `namespace: :grandpa` option to the `DecoLite::Model#load` method, would produce the following `attr_accessors`, ensuring uniquess:
65
+ However, passing a `namespace:` option (for example `namespace: :grandpa`) to the `DecoLite::Model#load!` method, would produce the following `attr_accessors`, ensuring their uniqueness:
66
+
56
67
  ```ruby
57
- # Unique now that the namespace "grandpa" has been applied.
58
- grandpa_name=, grandpa_name #=> 'Henry Doe'
59
- grandpa_age=, grandpa_age #=> 85
68
+ model.load!(hash: grandpa, options: { namespace: :grandpa })
69
+
70
+ # Unique now that the namespace: :grandpa has been applied:
71
+ model.grandpa_name #=> 'Henry Doe'
72
+ model.respond_to? :grandpa_name= #=> true
73
+
74
+ model.grandpa_age #=> 85
75
+ model.respond_to? :grandpa_age= #=> true
76
+
77
+ # All the other attributes on the model remain the same, and unique:
78
+ model.name #=> 'John Doe'
79
+ model.respond_to? :name= #=> true
80
+
81
+ model.age #=> 35
82
+ model.respond_to? :age= #=> true
83
+
84
+ model.wife_name #=> 'Mary Doe'
85
+ model.respond_to? :wife_name= #=> true
86
+
87
+ model.wife_age #=> 30
88
+ model.respond_to? :wife_age= #=> true
60
89
  ```
61
- ## Use Cases
90
+ ## Use cases
62
91
 
63
92
  ### General
64
- _Deco_ would _most likely_ thrive where the structure of the Hashe(s) consumed by the `DecoLite::Model#load` method is known. This is because of the way _Deco_ mangles loaded Hash key names to create unique `attr_accessor` names (see the Introduction section) although, I'm sure there are some metaprogramming geniuses out there that might prove me wrong. Assuming this is the case, _Deco_ would be ideal to handle Model attributes, Webservice JSON results (converted to Ruby Hash), JSON Web Token (JWT) payload, etc..
93
+ _Deco_ would _most likely_ thrive where the structure of the Hashe(s) consumed by the `DecoLite::Model#load!` method is known. This is because of the way _Deco_ mangles loaded Hash key names to create unique `attr_accessor` names (see the Introduction section); although, I'm sure there are some metaprogramming geniuses out there that might prove me wrong. Assuming this is the case, _Deco_ would be ideal to handle Model attributes, Webservice JSON results (converted to Ruby Hash), JSON Web Token (JWT) payload, etc..
65
94
 
66
95
  ### Rails
67
- Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_ can be used, and methods provided for use in Rails views; for example:
96
+ Because `DecoLite::Model` includes `ActiveModel::Model`, it could also be ideal for use as a model in Rails applications, where a _decorator pattern_ might be used, and decorator methods provided for use in Rails views; for example:
68
97
 
69
98
  ```ruby
70
99
  class ViewModel < DecoLite::Model
@@ -81,7 +110,7 @@ end
81
110
 
82
111
  view_model = ViewModel.new
83
112
 
84
- view_model.load(hash: { first: 'John', last: 'Doe' })
113
+ view_model.load!(hash: { first: 'John', last: 'Doe' })
85
114
 
86
115
  view_model.valid?
87
116
  #=> true
@@ -96,23 +125,7 @@ view_model.salutation
96
125
 
97
126
  Get creative. Please pop me an email and let me know how _you're_ using _Deco_.
98
127
 
99
- ## Installation
100
-
101
- Add this line to your application's Gemfile:
102
-
103
- ```ruby
104
- gem 'deco_lite'
105
- ```
106
-
107
- And then execute:
108
-
109
- $ bundle
110
-
111
- Or install it yourself as:
112
-
113
- $ gem install deco_lite
114
-
115
- ## Examples and Usage
128
+ ## Examples and usage
116
129
 
117
130
  ```ruby
118
131
  require 'deco_lite'
@@ -154,8 +167,8 @@ end
154
167
 
155
168
  couple = Couple.new
156
169
 
157
- couple.load(hash: husband, options: { namespace: :husband })
158
- couple.load(hash: wife, options: { namespace: :wife })
170
+ couple.load!(hash: husband, options: { namespace: :husband })
171
+ couple.load!(hash: wife, options: { namespace: :wife })
159
172
 
160
173
  # Will produce the following:
161
174
  model.live_together? #=> true
@@ -169,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
@@ -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,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 options.strict? && field_conflict?(field_name: field_name)
14
+ return unless field_conflict?(field_name: field_name, options: options)
13
15
 
14
- raise "Field '#{field_name}' conflicts with existing attribute; " \
15
- 'this will raise an error when running in strict mode: ' \
16
- "options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }."
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 field_conflict?(field_name:)
20
- respond_to? field_name
39
+ def field_names_include?(field_name:, options:)
40
+ field_name = field_name_or_field_name_with_namespace field_name: field_name, options: options
41
+
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
- self.class.attr_accessor(field_name) if field_name.present?
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 |h|
20
- h.each_pair do |field_name, value|
19
+ load_service.execute(hash: hash, options: load_service_options).tap do |service_hash|
20
+ service_hash.each_pair do |field_name, value|
21
21
  create_field_accessor field_name: field_name, options: deco_lite_options
22
22
  field_names << field_name unless field_names.include? field_name
23
23
  set_field_value(field_name: field_name, value: value, options: deco_lite_options)
@@ -4,7 +4,7 @@ module DecoLite
4
4
  # Provides methods to convert the object to a Hash.
5
5
  module Hashable
6
6
  def to_h
7
- field_names.each.each_with_object({}) do |field_name, hash|
7
+ field_names.each_with_object({}) do |field_name, hash|
8
8
  hash[field_name] = public_send field_name
9
9
  end
10
10
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
- require_relative 'field_creatable'
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 FieldCreatable
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
- attr_reader :field_names
49
-
50
- private
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
- attr_writer :field_names
60
+ load!(hash: hash, options: options)
61
+ end
53
62
  end
54
63
  end
@@ -24,6 +24,10 @@ module DecoLite
24
24
  def namespace?
25
25
  namespace.present? || false
26
26
  end
27
+
28
+ def required_fields_auto?
29
+ required_fields == OPTION_REQUIRED_FIELDS_AUTO
30
+ end
27
31
  end
28
32
  validate_options! options: immutable_struct_ex.to_h
29
33
  immutable_struct_ex
@@ -2,16 +2,19 @@
2
2
 
3
3
  require_relative 'fields_optionable'
4
4
  require_relative 'namespace_optionable'
5
+ require_relative 'required_fields_optionable'
5
6
 
6
7
  module DecoLite
7
8
  # Defines default options and their optionn values.
8
9
  module OptionsDefaultable
9
10
  include DecoLite::FieldsOptionable
10
11
  include DecoLite::NamespaceOptionable
12
+ include DecoLite::RequiredFieldsOptionable
11
13
 
12
14
  DEFAULT_OPTIONS = {
13
15
  OPTION_FIELDS => OPTION_FIELDS_DEFAULT,
14
- OPTION_NAMESPACE => OPTION_NAMESPACE_DEFAULT
16
+ OPTION_NAMESPACE => OPTION_NAMESPACE_DEFAULT,
17
+ OPTION_REQUIRED_FIELDS => OPTION_REQUIRED_FIELDS_DEFAULT
15
18
  }.freeze
16
19
  end
17
20
  end
@@ -2,14 +2,16 @@
2
2
 
3
3
  require_relative 'fields_optionable'
4
4
  require_relative 'namespace_optionable'
5
+ require_relative 'required_fields_optionable'
5
6
 
6
7
  module DecoLite
7
8
  # Methods to validate options.
8
9
  module OptionsValidatable
9
10
  include DecoLite::FieldsOptionable
10
11
  include DecoLite::NamespaceOptionable
12
+ include DecoLite::RequiredFieldsOptionable
11
13
 
12
- OPTIONS = [OPTION_FIELDS, OPTION_NAMESPACE].freeze
14
+ OPTIONS = [OPTION_FIELDS, OPTION_NAMESPACE, OPTION_REQUIRED_FIELDS].freeze
13
15
 
14
16
  def validate_options!(options:)
15
17
  raise ArgumentError, 'options is not a Hash' unless options.is_a? Hash
@@ -17,8 +19,9 @@ module DecoLite
17
19
  validate_options_present! options: options
18
20
 
19
21
  validate_option_keys! options: options
20
- validate_option_fields! fields: options[:fields]
21
- validate_option_namespace! namespace: options[:namespace]
22
+ validate_option_fields! fields: options[OPTION_FIELDS]
23
+ validate_option_namespace! namespace: options[OPTION_NAMESPACE]
24
+ validate_option_required_fields! required_fields: options[OPTION_REQUIRED_FIELDS]
22
25
  end
23
26
 
24
27
  def validate_options_present!(options:)
@@ -35,7 +38,7 @@ module DecoLite
35
38
 
36
39
  raise ArgumentError,
37
40
  "option :fields value or type is invalid. #{OPTION_FIELDS_VALUES} (Symbol) " \
38
- "was expected, but '#{fields}' (#{fields.class}) was received."
41
+ "was expected, but '#{fields}' (#{fields.class}) was received."
39
42
  end
40
43
 
41
44
  def validate_option_namespace!(namespace:)
@@ -43,7 +46,16 @@ module DecoLite
43
46
  return if namespace.blank? || namespace.is_a?(Symbol)
44
47
 
45
48
  raise ArgumentError, 'option :namespace value or type is invalid. A Symbol was expected, ' \
46
- "but '#{namespace}' (#{namespace.class}) was received."
49
+ "but '#{namespace}' (#{namespace.class}) was received."
50
+ end
51
+
52
+ def validate_option_required_fields!(required_fields:)
53
+ # :required_fields is optional.
54
+ return if required_fields.blank? || OPTION_REQUIRED_FIELDS_VALUES.include?(required_fields)
55
+
56
+ raise ArgumentError,
57
+ "option :fields_required value or type is invalid. #{OPTION_REQUIRED_FIELDS_VALUES} (Symbol) " \
58
+ "was expected, but '#{required_fields}' (#{required_fields.class}) was received."
47
59
  end
48
60
  end
49
61
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoLite
4
+ # Defines the fields option hash key and acceptable hash key values.
5
+ module RequiredFieldsOptionable
6
+ # The option hash key for this option.
7
+ OPTION_REQUIRED_FIELDS = :required_fields
8
+ # The valid option values for this option key.
9
+ OPTION_REQUIRED_FIELDS_AUTO = :auto
10
+ # The default value for this option.
11
+ OPTION_REQUIRED_FIELDS_DEFAULT = OPTION_REQUIRED_FIELDS_AUTO
12
+ # The valid option key values for this option.
13
+ OPTION_REQUIRED_FIELDS_VALUES = [OPTION_REQUIRED_FIELDS_AUTO].freeze
14
+ end
15
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the version of this gem.
4
4
  module DecoLite
5
- VERSION = '0.2.3'
5
+ VERSION = '0.3.0'
6
6
  end
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.2.3
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-18 00:00:00.000000000 Z
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: