deco_lite 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40f7b315deefbdf848f9d2743d27e672d3916da793f0bd32a705c7d34acac1f9
4
- data.tar.gz: b283948784b3fd9b1a4eaef20b7f0ca26a7de9b2971042c04a01021be6195d10
3
+ metadata.gz: 77c440561731ca11332b76f0ee373e4568d4e84c11930d530d1dde9d5c097eb9
4
+ data.tar.gz: f771f70f192ae1c2b2d7a935933f7157cde64c6f44e9dc60d50e66d58e8abb05
5
5
  SHA512:
6
- metadata.gz: 37a1083dfa95d51ab710fff1bab946edcc5b9fa825e37bf7f29c7a0174198d913469c8ac17f82b795949425f7e068236c73ca2ed9df50aba45bdfa3ef7cf1d18
7
- data.tar.gz: f3fbb8ab71dad6720dae8dc25179977757d55f7b802c2016ffc673fcdd423b3f86d04a406bd9a4bade6233efb5120cad1d7c2d7176045f44ec78da911311419a
6
+ metadata.gz: 6aa192d5c0d1c68860a4e41c31c3997307e90560d293ba6d2e1f5d30a82f537358988cfba766f34415c8321041f3844c2ac88d176a818647f13474f86171c3f6
7
+ data.tar.gz: e7910463ad7ede8f002af8bc0f46c220bcb715dce09d1690c10b3a3af7db66577acd372a607698470b2fd168a8295d3b24c1fd705601064f02e2f54e0f5ef350
data/.gitignore CHANGED
@@ -16,5 +16,5 @@
16
16
  /.vscode/
17
17
  *.code-workspace
18
18
 
19
- scratch.rb
19
+ scratch*.rb
20
20
  readme.txt
data/.reek.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  exclude_paths:
2
2
  - vendor
3
3
  - spec
4
- - scratch.rb
4
+ - scratch*.rb
5
5
  detectors:
6
6
  # TooManyInstanceVariables:
7
7
  # exclude:
data/.rubocop.yml CHANGED
@@ -13,7 +13,7 @@ AllCops:
13
13
  - '*.gemspec'
14
14
  - 'spec/**/*'
15
15
  - 'vendor/**/*'
16
- - 'scratch.rb'
16
+ - 'scratch*.rb'
17
17
 
18
18
  # Align the elements of a hash literal if they span more than one line.
19
19
  Layout/HashAlignment:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
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
+
1
14
  ### 0.3.0
2
15
  * Changes
3
16
  * `DecoLite::Model#new` how accepts a :hash named parameter that will load the Hash as if calling `DecoLite::Model.new.load!(hash: <hash>)`.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (0.3.0)
4
+ deco_lite (0.3.3)
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)
@@ -25,7 +25,7 @@ GEM
25
25
  docile (1.4.0)
26
26
  i18n (1.12.0)
27
27
  concurrent-ruby (~> 1.0)
28
- immutable_struct_ex (0.2.2)
28
+ immutable_struct_ex (0.2.3)
29
29
  json (2.6.2)
30
30
  kwalify (0.7.2)
31
31
  mad_flatter (1.0.1.pre.beta)
@@ -63,7 +63,7 @@ GEM
63
63
  diff-lcs (>= 1.2.0, < 2.0)
64
64
  rspec-support (~> 3.11.0)
65
65
  rspec-support (3.11.0)
66
- rubocop (1.35.0)
66
+ rubocop (1.35.1)
67
67
  json (~> 2.3)
68
68
  parallel (~> 1.10)
69
69
  parser (>= 3.1.2.1)
data/README.md CHANGED
@@ -13,13 +13,13 @@
13
13
 
14
14
  ## Introduction
15
15
 
16
- DecoLite is in development. I wouldn't expect breaking changes before v1.0.0; however, I can't completely rule this out. Currently, DecoLite only supports Hashes whose keys are `Symbols`, contain no embedded spaces, and conform to Ruby `attr_accessor` naming conventions. However, I'll certainly work out a solution for all this in future releases.
16
+ DecoLite is in development. I wouldn't expect breaking changes before v1.0.0; however, I can't completely rule this out. Currently, DecoLite only supports Hashes whose keys are `Symbols`, contain no embedded spaces, and conform to Ruby `attr_accessor` naming conventions. However, I might try work out a reasonable solution for all this in future releases if the need is there.
17
17
 
18
- TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load!` options: how these work, and how they play together (in the meantime, see the specs).
18
+ TBD: Documentation regarding `DecoLite::Model` options, `DecoLite::Model#load!` options: how these work, and how they play together (e.g. options `fields: :merge` and `fields: :strict` for example; in the meantime, see the specs).
19
19
 
20
20
  _Deco_ is a little gem that allows you to use the provided `DecoLite::Model` class (`include ActiveModel::Model`) to create Decorator classes which can be instantiated and used. Inherit from `DecoLite::Model` to create your own unique classes with custom functionality. A `DecoLite::Model` includes `ActiveModel::Model`, so validation can be applied using [ActiveModel validation helpers](https://api.rubyonrails.org/v6.1.3/classes/ActiveModel/Validations/HelperMethods.html) you are familiar with; or, you can roll your own - just like any other ActiveModel.
21
21
 
22
- A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the `DecoLite::Model#load!` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded.
22
+ A `DecoLite::Model` will allow you to consume a Ruby Hash that you supply via the initializer (`DecoLite::Model#new`) or via the `DecoLite::Model#load!` method. Your supplied Ruby Hashes are used to create `attr_accessor` attributes (_"fields"_) on the model. Each attribute created, is then assigned its value from the Hash loaded. Any number of hashes can be consumed using the `DecoLite::Model#load!` method.
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
 
@@ -34,10 +34,11 @@ family = {
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 and assign the values:
38
38
 
39
39
  ```ruby
40
- model = DecoLite::Model.new.load!(hash: family)
40
+ # Or DecoLite::Model.new.load!(hash: family)
41
+ model = DecoLite::Model.new(hash: family)
41
42
 
42
43
  model.name #=> 'John Doe'
43
44
  model.respond_to? :name= #=> true
@@ -52,17 +53,21 @@ model.wife_age #=> 30
52
53
  model.respond_to? :wife_age= #=> true
53
54
  ```
54
55
 
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:
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.
57
+
58
+ For example, **continuing from the previous example;** if we were to call `DecoLite::Model#load!` a _second time_ with the following Hash, this would potentially produce `attr_accessor` name clashes:
56
59
 
57
60
  ```ruby
58
61
  grandpa = {
59
62
  name: 'Henry Doe',
60
63
  age: 85,
61
64
  }
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!.
65
+ # The :name and :age Hash keys above will produce :name/:name= and :age/:age= attr_accessors
66
+ # and clash because these were already added to the model when "John Doe" was loaded with
67
+ # the first call to DecoLite::Model.new(hash: family).
63
68
  ```
64
69
 
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:
70
+ 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
71
 
67
72
  ```ruby
68
73
  model.load!(hash: grandpa, options: { namespace: :grandpa })
@@ -87,6 +92,11 @@ model.respond_to? :wife_name= #=> true
87
92
  model.wife_age #=> 30
88
93
  model.respond_to? :wife_age= #=> true
89
94
  ```
95
+
96
+ ### For more examples and usage
97
+
98
+ For more examples and usage, see the [Examples and usage](#examples-and-usage) and [Mode examples and usage](#more-examples-and-usage) sections; there is also an "I want to..." section with examples you might encounter when using `DecoLite`.
99
+
90
100
  ## Use cases
91
101
 
92
102
  ### General
@@ -108,9 +118,7 @@ class ViewModel < DecoLite::Model
108
118
  end
109
119
  end
110
120
 
111
- view_model = ViewModel.new
112
-
113
- view_model.load!(hash: { first: 'John', last: 'Doe' })
121
+ view_model = ViewModel.new(hash: { first: 'John', last: 'Doe' })
114
122
 
115
123
  view_model.valid?
116
124
  #=> true
@@ -188,22 +196,131 @@ model.wife_info_address #=> 1 street, boonton, nj 07005
188
196
 
189
197
  #### Add validators to my model
190
198
 
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:
199
+ 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.) *before* the data is loaded to create the associated `attr_accessors`:
192
200
 
193
201
  ```ruby
194
202
  class Model < DecoLite::Model
195
203
  validates :first, :last, :address, presence: true
204
+ validates :age, numericality: true
196
205
  end
197
206
 
198
- model = Model.new(hash: { first: 'John', last: 'Doe' })
207
+ # No :address
208
+ model = Model.new(hash: { first: 'John', last: 'Doe', age: 25 })
209
+ model.respond_to? :address
210
+ #=> true
211
+
212
+ model.valid?
213
+ #=> false
214
+ model.errors.full_messages
215
+ #=> ["Address can't be blank"]
199
216
 
200
- # No :address; a NoMethodError error for :address will be raised.
217
+ model.load!(hash: { address: '123 park road, anytown, nj 01234' })
201
218
  model.validate
202
- #=> undefined method 'address' for #<Model:0x00007f95931568c0 ... @field_names=[:first, :last], @first="John", @last="Doe", ...> (NoMethodError)
219
+ #=> true
220
+ ```
203
221
 
204
- model.load!(hash: { address: '123 park road, anytown, nj 01234' })
222
+ #### Validate whether or not certain fields were loaded
223
+
224
+ To be clear, this example does not validate the _data_ associated with the fields loaded; rather, this example validates whether or not the _fields themselves_ (`attr_accessors`) were created on your model as a result of loading data into your model. If you only 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.
225
+
226
+ If you want to validate whether or not particular _fields_ were added to your model as attributes (`attr_accessor`), as a result of `#load!`ing data into your model, you need to do a few things:
227
+ - Create a `DecoLite::Model` subclass.
228
+ - Override the `DecoLite::Model#required_fields` method to return the field names you want to validate.
229
+ - Use the `required_fields: nil` option when instantiating your model object.
230
+ - 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 (for example) `validates :first, :last, :address, presence: true` to your model if you need to validate whether or not the data you load into your model includs fields `:first`, `:last` and `:address`.
231
+
232
+ For example:
233
+
234
+ ```ruby
235
+ class Model < DecoLite::Model
236
+ # :age field is optional and it's value is optional.
237
+ validates :age, numericality: { only_integer: true }, allow_blank: true
238
+
239
+ def required_fields
240
+ # We want to ensure these fields were included as Hash keys during loading.
241
+ %i(first last address)
242
+ end
243
+ end
244
+ ```
245
+
246
+ Option `required_fields: :auto` is the default which will automatically create `attr_accessors` for any field returned from the `DecoLite::Model#required_fields` method; therefore, we need to set the `:required_fields` option to `nil` (i.e. `required_fields: nil`). This will prohibit `DecoLite::Model` from automatically creating `attr_accessors` for `:first`, `:last` and `:address`, and achieve the results we want:
247
+
248
+ ```ruby
249
+ model = Model.new(options: { required_fields: nil })
250
+
251
+ model.validate
252
+ #=> false
253
+ model.errors.full_messages
254
+ #=> ["First field is missing", "Last field is missing", "Address field is missing"]
255
+
256
+ # If we load data that includes :first, :last, and :address Hash keys even with
257
+ # nil data, our ":<field> field is missing" errors go away; in this scenario,
258
+ # we're validating the presence of the FIELDS, not the data associated with
259
+ # these fields!
260
+ model.load!(hash: { first: nil, last: nil, address: nil })
205
261
  model.validate
206
262
  #=> true
263
+ model.errors.full_messages
264
+ #=> []
265
+
266
+ user = {
267
+ first: 'John',
268
+ last: 'Doe',
269
+ address: '123 anystreet, anytown, nj 01234',
270
+ age: 'x'
271
+ }
272
+ model.load!(hash: user)
273
+ model.validate
274
+ #=> false
275
+ model.errors.full_messages
276
+ #=> ["Age is not a number"]
277
+ ```
278
+ #### Validate whether or not certain fields were loaded _and_ validate the data associated with these same fields
279
+
280
+ 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.
281
+
282
+ 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.
283
+
284
+ For example:
285
+
286
+ ```ruby
287
+ class Model < DecoLite::Model
288
+ def required_fields
289
+ %i(first last address age)
290
+ end
291
+
292
+ def validate_required_fields
293
+ super
294
+
295
+ first = self.try(:first)
296
+ errors.add(:first, "can't be blank") if first.nil?
297
+
298
+ last = self.try(:last)
299
+ errors.add(:last, "can't be blank") if last.nil?
300
+
301
+ address = self.try(:address)
302
+ errors.add(:address, "can't be blank") if address.nil?
303
+
304
+ age = self.try(:age)
305
+ errors.add(:age, "can't be blank") if age.nil?
306
+ errors.add(:age, 'is not a number') unless /\d+/ =~ age
307
+ end
308
+ end
309
+ model = Model.new(options: { required_fields: nil })
310
+
311
+ model.validate
312
+ #=> false
313
+
314
+ model.errors.full_messages
315
+ #=> ["First field is missing",
316
+ "Last field is missing",
317
+ "Address field is missing",
318
+ "Age field is missing",
319
+ "First can't be blank",
320
+ "Last can't be blank",
321
+ "Address can't be blank",
322
+ "Age can't be blank",
323
+ "Age is not a number"]
207
324
  ```
208
325
 
209
326
  #### Manually define attributes (fields) on my model
@@ -224,19 +341,21 @@ class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
224
341
  end
225
342
  ```
226
343
 
227
- However, the above is unnecessary as this can be easily accomplished using `DecoLite::Model#load!`:
344
+ However, the above is unnecessary as this can be easily accomplished by passing a `Hash` to the initializer or by using `DecoLite::Model#load!`:
345
+
228
346
  ```ruby
229
- model = Class.new(DecoLite::Model).new.load!(hash:{ existing_field: :existing_field_value })
347
+ model = Class.new(DecoLite::Model).new(hash:{ existing_field: :value })
230
348
 
231
349
  model.field_names
232
350
  #=> [:existing_field]
233
351
 
234
352
  model.existing_field
235
- #=> :existing_field_value
353
+ #=> :value
236
354
 
237
355
  model.respond_to? :existing_field=
238
356
  #=> true
239
357
  ```
358
+
240
359
  ## Installation
241
360
 
242
361
  Add this line to your application's Gemfile:
@@ -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)
@@ -4,6 +4,8 @@ module DecoLite
4
4
  # Provides methods to manage fields that must be defined from
5
5
  # the dynamically loaded data.
6
6
  module FieldRequireable
7
+ MISSING_REQUIRED_FIELD_ERROR_TYPE = :missing_required_field
8
+
7
9
  # Returns field names that will be used to validate the presence of
8
10
  # dynamically created fields from loaded objects.
9
11
  #
@@ -23,7 +25,7 @@ module DecoLite
23
25
  required_fields.each do |field_name|
24
26
  next if required_field_exist? field_name: field_name
25
27
 
26
- errors.add(field_name, 'field is missing', type: :missing_required_field)
28
+ errors.add(field_name, 'field is missing', type: MISSING_REQUIRED_FIELD_ERROR_TYPE)
27
29
  end
28
30
  end
29
31
 
@@ -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
 
@@ -5,7 +5,11 @@ module DecoLite
5
5
  module Hashable
6
6
  def to_h
7
7
  field_names.each_with_object({}) do |field_name, hash|
8
- hash[field_name] = public_send field_name
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
@@ -47,7 +47,6 @@ module DecoLite
47
47
  # options while loading, but also provide option customization
48
48
  # of options when needed.
49
49
  options = Options.with_defaults(options, defaults: self.options)
50
-
51
50
  load_hash(hash: hash, deco_lite_options: options)
52
51
 
53
52
  self
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the version of this gem.
4
4
  module DecoLite
5
- VERSION = '0.3.0'
5
+ VERSION = '0.3.3'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deco_lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.3
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-26 00:00:00.000000000 Z
11
+ date: 2022-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel