deco_lite 0.3.0 → 0.3.3

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