deco_lite 0.1.1 → 0.2.4

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: f9b6003b4b875144ddc599d65ace8b397b3bb010cacc95aebf3360b71a7ccda9
4
- data.tar.gz: 82e1860ba9e6f5762e6bc52233160fae55e4f0a715ec3bb8a555ec3b3d24ac23
3
+ metadata.gz: 2d360b0607d72711264a19f807ad4367237f0e5e69efb385728ccbda66e72e05
4
+ data.tar.gz: f90e61477cc9dfa1ba8d19070c1490fcccfce413533436ed93b532d4933fad08
5
5
  SHA512:
6
- metadata.gz: c2f7ddce5f5d6bc1367ae5da30e2043cc60b443c93f4df0f4caac435c415b0dfeeffdff424460810cb0a9996f00e3fee4e9d0138bbac4d8515138613c5b9a0a5
7
- data.tar.gz: 492a9602f01f7615049039e0dccc80debce0cbc1ec57d5927e72ca6b6f4f320fb5526b8f2384c81b4a3c12e70a278d4dcc4978c73b3ff56436d628390480e850
6
+ metadata.gz: 87d78e241cd8aada8c09c8adbcb20cdb5997e96ff4f17cc8dd3a494e973fd10f13fe73927edb604e2125209119bd881522c79995753768fc0f64d049632d1751
7
+ data.tar.gz: d4ae03338d636d82d6a67996cea9e2143a07b0319a9ad6f3a1f1e4a508b2f9a4262ec5cb7e68c8cb2b7d29a8f0e046413ad7f9470f4a20c6fa470c20d9e3c907
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### 0.2.4
2
+ * Changes
3
+ * Change DecoLite::Model#load to #load! as it alters the object, give deprecation warning when calling #load.
4
+ * 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.
5
+ * 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).
6
+ * bug fixes
7
+ * 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.
8
+
9
+ ### 0.2.3
10
+ * Fix bug that added duplcate field names to Model#field_names.
11
+
12
+ ### 0.2.2
13
+ * Fix bug requiring support codez in lib/deco_lite.rb.
14
+
15
+ ### 0.2.1
16
+ * changes
17
+ * Add mad_flatter gem runtime dependency.
18
+ * Refactor to let mad_flatter handle the Hash flattening.
19
+
1
20
  ### 0.1.1
2
21
  * changes
3
22
  * Update gems and especially rake gem version to squash CVE-2020-8130, see https://github.com/advisories/GHSA-jppv-gw3r-w3q8.
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deco_lite (0.1.1)
4
+ deco_lite (0.2.4)
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)
8
+ mad_flatter (~> 1.0.0.pre.beta)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
@@ -24,20 +25,23 @@ GEM
24
25
  docile (1.4.0)
25
26
  i18n (1.12.0)
26
27
  concurrent-ruby (~> 1.0)
27
- immutable_struct_ex (0.2.1)
28
+ immutable_struct_ex (0.2.2)
28
29
  json (2.6.2)
29
30
  kwalify (0.7.2)
31
+ mad_flatter (1.0.1.pre.beta)
32
+ activesupport (~> 7.0, >= 7.0.3.1)
33
+ immutable_struct_ex (~> 0.2.0)
30
34
  method_source (1.0.0)
31
- minitest (5.16.2)
35
+ minitest (5.16.3)
32
36
  parallel (1.22.1)
33
37
  parser (3.1.2.1)
34
38
  ast (~> 2.4.1)
35
- pry (0.13.1)
39
+ pry (0.14.1)
36
40
  coderay (~> 1.1)
37
41
  method_source (~> 1.0)
38
- pry-byebug (3.9.0)
42
+ pry-byebug (3.10.1)
39
43
  byebug (~> 11.0)
40
- pry (~> 0.13.0)
44
+ pry (>= 0.13, < 0.15)
41
45
  rainbow (3.1.1)
42
46
  rake (13.0.6)
43
47
  reek (6.1.1)
@@ -46,19 +50,19 @@ GEM
46
50
  rainbow (>= 2.0, < 4.0)
47
51
  regexp_parser (2.5.0)
48
52
  rexml (3.2.5)
49
- rspec (3.10.0)
50
- rspec-core (~> 3.10.0)
51
- rspec-expectations (~> 3.10.0)
52
- rspec-mocks (~> 3.10.0)
53
- rspec-core (3.10.2)
54
- rspec-support (~> 3.10.0)
55
- rspec-expectations (3.10.2)
53
+ rspec (3.11.0)
54
+ rspec-core (~> 3.11.0)
55
+ rspec-expectations (~> 3.11.0)
56
+ rspec-mocks (~> 3.11.0)
57
+ rspec-core (3.11.0)
58
+ rspec-support (~> 3.11.0)
59
+ rspec-expectations (3.11.0)
56
60
  diff-lcs (>= 1.2.0, < 2.0)
57
- rspec-support (~> 3.10.0)
58
- rspec-mocks (3.10.3)
61
+ rspec-support (~> 3.11.0)
62
+ rspec-mocks (3.11.1)
59
63
  diff-lcs (>= 1.2.0, < 2.0)
60
- rspec-support (~> 3.10.0)
61
- rspec-support (3.10.3)
64
+ rspec-support (~> 3.11.0)
65
+ rspec-support (3.11.0)
62
66
  rubocop (1.35.0)
63
67
  json (~> 2.3)
64
68
  parallel (~> 1.10)
data/README.md CHANGED
@@ -170,6 +170,29 @@ model.wife_info_age #=> 20
170
170
  model.wife_info_address #=> 1 street, boonton, nj 07005
171
171
  ```
172
172
 
173
+ ### Manually Defining Attributes
174
+
175
+ Manually defining attributes on your subclass is possible; however, you
176
+ must add your attr_reader name to the `DecoLite::Model@field_names` array, or an error will be reaised _if_ there are any conflicting field names being loaded
177
+ using `DecoLite::Model#load!`, regardless of setting the `{ fields: :merge }`
178
+ option. This is because DecoLite assumes any existing attributes not added to
179
+ the model via `load!`to be native to the object created, and therefore will not
180
+ allow you to create attr_accessors for existing attributes, as this can potentially be dangerous.
181
+
182
+ To avoid errors when manually defining attributes on the model that could potentially be in conflict with fields loaded using `DecoLite::Model#load!`, do the following:
183
+
184
+ ```ruby
185
+ class JustBecauseYouCanDoesntMeanYouShould < DecoLite::Model
186
+ attr_accessor :existing_field
187
+
188
+ def initialize(options: {})
189
+ super
190
+
191
+ @field_names = %i(existing_field)
192
+ end
193
+ end
194
+ ```
195
+
173
196
  ## Development
174
197
 
175
198
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/deco_lite.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_runtime_dependency 'activemodel', '~> 7.0', '>= 7.0.3.1'
37
37
  spec.add_runtime_dependency 'activesupport', '~> 7.0', '>= 7.0.3.1'
38
38
  spec.add_runtime_dependency 'immutable_struct_ex', '~> 0.2.0'
39
+ spec.add_runtime_dependency 'mad_flatter', '~> 1.0.0.pre.beta'
39
40
  spec.add_development_dependency 'bundler', '~> 2.2', '>= 2.2.17'
40
41
  spec.add_development_dependency 'pry-byebug', '~> 3.9'
41
42
  spec.add_development_dependency 'reek', '~> 6.1', '>= 6.1.1'
@@ -9,15 +9,36 @@ module DecoLite
9
9
  include FieldsOptionable
10
10
 
11
11
  def validate_field_conflicts!(field_name:, options:)
12
- return unless options.strict? && field_conflict?(field_name: field_name)
12
+ return unless field_conflict?(field_name: field_name, options: options)
13
13
 
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} }."
14
+ raise "Field :#{field_name} conflicts with existing method(s) " \
15
+ ":#{field_name} and/or :#{field_name}=; " \
16
+ 'this will raise an error when loading using strict mode ' \
17
+ "(i.e. options: { #{OPTION_FIELDS}: :#{OPTION_FIELDS_STRICT} }) " \
18
+ 'or if the method(s) are native to the object (e.g :to_s, :==, etc.).'
17
19
  end
18
20
 
19
- def field_conflict?(field_name:)
20
- respond_to? field_name
21
+ # This method returns true
22
+ def field_conflict?(field_name:, options:)
23
+ # If field_name was already added using Model#load, there is only a
24
+ # conflict if options.strict? is true.
25
+ if field_names_include?(field_name: field_name)
26
+ return options.strict?
27
+ end
28
+
29
+ # If we get here, we know that :field_name does not exist as an
30
+ # attribute on the model. If the attribute already exists on the
31
+ # model, this is a conflict because we cannot override an attribute
32
+ # that already exists on the model
33
+ attr_accessor_exist?(field_name: field_name)
34
+ end
35
+
36
+ def field_names_include?(field_name:)
37
+ field_names.include? field_name
38
+ end
39
+
40
+ def attr_accessor_exist?(field_name:)
41
+ respond_to?(field_name) || respond_to?(:"#{field_name}=")
21
42
  end
22
43
  end
23
44
  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,28 @@ 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
+ def create_field_getter(field_name:, options:)
34
+ define_singleton_method(field_name) do
35
+ instance_variable_get "@#{field_name}"
36
+ end
37
+ end
38
+
39
+ def create_field_setter(field_name:, options:)
40
+ define_singleton_method("#{field_name}=") do |value|
41
+ instance_variable_set "@#{field_name}", value
42
+ end
22
43
  end
23
44
  end
24
45
  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 = /\A(?:[a-z_]\w*[?!=]?|\[\]=?|<<|>>|\*\*|[!~+\*\/%&^|-]|[<>]=?|<=>|={2,3}|![=~]|=~)\z/i.freeze
7
+
8
+ module_function
9
+
10
+ def validate_field_name!(field_name:, options: nil)
11
+ unless field_name =~ FIELD_NAME_REGEX
12
+ raise "field_name '#{field_name}' is not a valid field name."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,24 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mad_flatter'
3
4
  require_relative 'field_assignable'
4
- require_relative 'field_informable'
5
5
 
6
6
  module DecoLite
7
7
  # Provides methods to load and return information about a given hash.
8
8
  module HashLoadable
9
9
  include FieldAssignable
10
- include FieldInformable
11
10
 
12
11
  private
13
12
 
14
- def load_hash(hash:, options:)
13
+ def load_hash(hash:, deco_lite_options:)
15
14
  raise ArgumentError, "Argument hash is not a Hash (#{hash.class})" unless hash.is_a? Hash
16
15
 
17
- return if hash.blank?
16
+ return {} if hash.blank?
18
17
 
19
- field_info = get_field_info(hash: hash, namespace: options.namespace)
20
- set_field_values(hash: hash, field_info: field_info, options: options)
21
- merge_field_info! field_info: field_info
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|
21
+ create_field_accessor field_name: field_name, options: deco_lite_options
22
+ field_names << field_name unless field_names.include? field_name
23
+ set_field_value(field_name: field_name, value: value, options: deco_lite_options)
24
+ end
25
+ end
26
+ end
27
+
28
+ def load_service
29
+ @load_service ||= MadFlatter::Service.new
30
+ end
31
+
32
+ def merge_with_load_service_options(deco_lite_options:)
33
+ load_service.options.to_h.merge \
34
+ deco_lite_options.to_h.slice(*MadFlatter::OptionsDefaultable::DEFAULT_OPTIONS.keys)
22
35
  end
23
36
  end
24
37
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
+ require_relative 'field_creatable'
4
5
  require_relative 'field_requireable'
5
- require_relative 'hashable'
6
+ require_relative 'field_names_persistable'
6
7
  require_relative 'hash_loadable'
8
+ require_relative 'hashable'
7
9
  require_relative 'model_nameable'
8
10
  require_relative 'optionable'
9
11
 
@@ -12,16 +14,19 @@ module DecoLite
12
14
  # dynamic models that can be used as decorators.
13
15
  class Model
14
16
  include ActiveModel::Model
17
+ include FieldCreatable
18
+ include FieldNamesPersistable
15
19
  include FieldRequireable
16
- include Hashable
17
20
  include HashLoadable
21
+ include Hashable
18
22
  include ModelNameable
19
23
  include Optionable
20
24
 
21
25
  validate :validate_required_fields
22
26
 
23
27
  def initialize(options: {})
24
- @field_info = {}
28
+ @field_names = []
29
+
25
30
  # Accept whatever options are sent, but make sure
26
31
  # we have defaults set up. #options_with_defaults
27
32
  # will merge options into OptionsDefaultable::DEFAULT_OPTIONS
@@ -30,16 +35,24 @@ module DecoLite
30
35
  self.options = Options.with_defaults options
31
36
  end
32
37
 
33
- def load(hash:, options: {})
38
+ def load!(hash:, options: {})
34
39
  # Merge options into the default options passed through the
35
40
  # constructor; these will override any options passed in when
36
41
  # this object was created, allowing us to retain any defaut
37
42
  # options while loading, but also provide option customization
38
43
  # of options when needed.
39
44
  options = Options.with_defaults(options, defaults: self.options)
40
- load_hash(hash: hash, options: options)
45
+
46
+ load_hash(hash: hash, deco_lite_options: options)
41
47
 
42
48
  self
43
49
  end
50
+
51
+ def load(hash:, options: {})
52
+ puts 'WARNING: DecoLite::Model#load will be deprecated in a future release;' \
53
+ ' use DecoLite::Model#load! instead!'
54
+
55
+ load!(hash: hash, options: options)
56
+ end
44
57
  end
45
58
  end
@@ -22,7 +22,7 @@ module DecoLite
22
22
  end
23
23
 
24
24
  def namespace?
25
- namespace || false
25
+ namespace.present? || false
26
26
  end
27
27
  end
28
28
  validate_options! options: immutable_struct_ex.to_h
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the version of this gem.
4
4
  module DecoLite
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.4'
6
6
  end
data/lib/deco_lite.rb CHANGED
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir[File.join('.', 'lib/deco_lite/**/*.rb')].each do |f|
4
- require f
5
- end
3
+ require_relative 'deco_lite/field_assignable'
4
+ require_relative 'deco_lite/field_conflictable'
5
+ require_relative 'deco_lite/field_creatable'
6
+ require_relative 'deco_lite/field_names_persistable'
7
+ require_relative 'deco_lite/field_requireable'
8
+ require_relative 'deco_lite/field_retrievable'
9
+ require_relative 'deco_lite/fields_optionable'
10
+ require_relative 'deco_lite/hash_loadable'
11
+ require_relative 'deco_lite/hashable'
12
+ require_relative 'deco_lite/model'
13
+ require_relative 'deco_lite/model_nameable'
14
+ require_relative 'deco_lite/namespace_optionable'
15
+ require_relative 'deco_lite/optionable'
16
+ require_relative 'deco_lite/options'
17
+ require_relative 'deco_lite/options_defaultable'
18
+ require_relative 'deco_lite/options_validatable'
19
+ require_relative 'deco_lite/version'
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.1.1
4
+ version: 0.2.4
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-14 00:00:00.000000000 Z
11
+ date: 2022-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -64,6 +64,20 @@ dependencies:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
66
  version: 0.2.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: mad_flatter
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.0.0.pre.beta
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 1.0.0.pre.beta
67
81
  - !ruby/object:Gem::Dependency
68
82
  name: bundler
69
83
  requirement: !ruby/object:Gem::Requirement
@@ -247,9 +261,10 @@ files:
247
261
  - lib/deco_lite/field_assignable.rb
248
262
  - lib/deco_lite/field_conflictable.rb
249
263
  - lib/deco_lite/field_creatable.rb
250
- - lib/deco_lite/field_informable.rb
264
+ - lib/deco_lite/field_names_persistable.rb
251
265
  - lib/deco_lite/field_requireable.rb
252
266
  - lib/deco_lite/field_retrievable.rb
267
+ - lib/deco_lite/field_validatable.rb
253
268
  - lib/deco_lite/fields_optionable.rb
254
269
  - lib/deco_lite/hash_loadable.rb
255
270
  - lib/deco_lite/hashable.rb
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DecoLite
4
- # Creates and returns a hash given the parameters that are used to
5
- # dynamically create fields and assign values to a model.
6
- module FieldInformable
7
- # This method simply navigates the payload hash received and creates qualified
8
- # hash key names that can be used to verify/map to our field names in this model.
9
- # This can be used to qualify nested hash fields and saves us some headaches
10
- # if there are nested field names with the same name:
11
- #
12
- # given:
13
- #
14
- # hash = {
15
- # first_name: 'first_name',
16
- # ...
17
- # address: {
18
- # street: '',
19
- # ...
20
- # }
21
- # }
22
- #
23
- # get_field_info(hash: hash) #=>
24
- #
25
- # {
26
- # :first_name=>{:field_name=>:first_name, :dig=>[]},
27
- # ...
28
- # :address_street=>{:field_name=>:street, :dig=>[:address]},
29
- # ...
30
- # }
31
- #
32
- # The generated, qualified field names expected to map to our model, because we named
33
- # them as such.
34
- #
35
- # :field_name is the actual, unqualified field name found in the payload hash sent.
36
- # :dig is the hash key by which :field_name can be found in the payload hash if need be -
37
- # retained across recursive calls.
38
- def get_field_info(hash:, namespace: nil, dig: [], field_info: {})
39
- hash.each do |key, value|
40
- if value.is_a? Hash
41
- get_field_info hash: value,
42
- namespace: namespace,
43
- dig: dig << key,
44
- field_info: field_info
45
- dig.pop
46
- else
47
- set_field_info!(field_info: field_info,
48
- key: key,
49
- namespace: namespace,
50
- dig: dig)
51
- end
52
- end
53
-
54
- field_info
55
- end
56
-
57
- def set_field_info!(field_info:, key:, namespace:, dig:)
58
- field_key = [namespace, *dig, key].compact.join('_').to_sym
59
-
60
- field_info[field_key] = {
61
- field_name: key,
62
- dig: dig.dup
63
- }
64
- end
65
-
66
- def merge_field_info!(field_info:)
67
- @field_info.merge!(field_info)
68
- end
69
-
70
- def field_names
71
- field_info&.keys || []
72
- end
73
-
74
- attr_reader :field_info
75
-
76
- private
77
-
78
- attr_writer :field_info
79
-
80
- module_function :get_field_info, :set_field_info!, :merge_field_info!
81
- end
82
- end