u-attributes 2.1.1 → 2.6.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: 7939bac74159eb5cd1e4307a80805ee0712fe5233148f550bd40c3ed99773d5d
4
- data.tar.gz: ac33edcb92e41acf77e823d61e22c2b2b6abe3b4c74036dfbf82d741ee208724
3
+ metadata.gz: 484a7b770e8ae7dd141c7538e2bc50c1b82beb63e24ee53dd9f9445cbd91f057
4
+ data.tar.gz: 3d555771a3deba711dc4995cd12674461b94c82e65ccbdd9bba215d354adaa52
5
5
  SHA512:
6
- metadata.gz: '0796ae31ef53f91844d02950c00f52fe263794d13af076b5814292bd63e73ff36939f5141888f9c0db9f7c5ff7e3a739be11d1b04e89b8e76ee0eff21068ab05'
7
- data.tar.gz: 16423f86a33677355c35c4aa5184d2356739a16d2506faf94cc45c50b033139c81ff11c261d9e7383228bd500d583caafd1596e89d411c2d21238c540dc63608
6
+ metadata.gz: fc5f65eb1a10c5172f0b6a1fa5296f4470e024fa42559cfe0d64d89d0a07c0c203ae28ce1b66afae741669a8073b1c6cbb7d05d5373874946afefca6620d7874
7
+ data.tar.gz: db8895b0a85d803de7ec04f81fc83fb98eac4b487ed4224e7fb7b6bda3d66293758ce416341dddbd40ae630dfb96d4d26cdd380990a3fca2580df931b459311a
data/README.md CHANGED
@@ -47,6 +47,15 @@ So, if you change [[1](#with_attribute)] [[2](#with_attributes)] some object att
47
47
  - [Is it possible to inherit the attributes?](#is-it-possible-to-inherit-the-attributes)
48
48
  - [`.attribute!()`](#attribute)
49
49
  - [How to query the attributes?](#how-to-query-the-attributes)
50
+ - [`.attributes`](#attributes)
51
+ - [`.attribute?()`](#attribute-1)
52
+ - [`#attribute?()`](#attribute-2)
53
+ - [`#attributes()`](#attributes-1)
54
+ - [`#attributes(keys_as:)`](#attributeskeys_as)
55
+ - [`#attributes(*names)`](#attributesnames)
56
+ - [`#attributes([names])`](#attributesnames-1)
57
+ - [`#attributes(with:, without:)`](#attributeswith-without)
58
+ - [`#defined_attributes`](#defined_attributes)
50
59
  - [Built-in extensions](#built-in-extensions)
51
60
  - [Picking specific features](#picking-specific-features)
52
61
  - [`Micro::Attributes.with`](#microattributeswith)
@@ -58,6 +67,7 @@ So, if you change [[1](#with_attribute)] [[2](#with_attributes)] some object att
58
67
  - [Diff extension](#diff-extension)
59
68
  - [Initialize extension](#initialize-extension)
60
69
  - [Strict mode](#strict-mode)
70
+ - [Keys as symbol extension](#keys-as-symbol-extension)
61
71
  - [Development](#development)
62
72
  - [Contributing](#contributing)
63
73
  - [License](#license)
@@ -75,7 +85,7 @@ gem 'u-attributes'
75
85
 
76
86
  | u-attributes | branch | ruby | activemodel |
77
87
  | -------------- | ------- | -------- | ------------- |
78
- | 2.1.1 | main | >= 2.2.0 | >= 3.2, < 6.1 |
88
+ | 2.6.0 | main | >= 2.2.0 | >= 3.2, < 6.1 |
79
89
  | 1.2.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
80
90
 
81
91
  > **Note**: The activemodel is an optional dependency, this module [can be enabled](#activemodelvalidation-extension) to validate the attributes.
@@ -320,10 +330,9 @@ class Person
320
330
  end
321
331
  ```
322
332
 
323
- There are 3 different strategies to define default values.
333
+ There are two different strategies to define default values.
324
334
  1. Pass a regular object, like in the previous example.
325
335
  2. Pass a `proc`/`lambda`, and if it has an argument you will receive the attribute value to do something before assign it.
326
- 3. Pass a **callable**, that is, a `class`, `module` or `instance` which responds to the `call` method. The behavior will be like the previous item (`proc`/`lambda`).
327
336
 
328
337
  ```ruby
329
338
  class Person
@@ -420,69 +429,155 @@ beta_person.age # 0
420
429
 
421
430
  ## How to query the attributes?
422
431
 
432
+ All of the methods that will be explained can be used with any of the built-in extensions.
433
+
434
+ **PS:** We will use the class below for all of the next examples.
435
+
423
436
  ```ruby
424
437
  class Person
425
438
  include Micro::Attributes
426
439
 
427
440
  attribute :age
428
- attribute :name, default: 'John Doe'
441
+ attribute :first_name, default: 'John'
442
+ attribute :last_name, default: 'Doe'
429
443
 
430
444
  def initialize(options)
431
445
  self.attributes = options
432
446
  end
447
+
448
+ def name
449
+ "#{first_name} #{last_name}"
450
+ end
433
451
  end
452
+ ```
434
453
 
435
- #---------------#
436
- # .attributes() #
437
- #---------------#
454
+ ### `.attributes`
438
455
 
439
- Person.attributes # ['name', 'age']
456
+ Listing all the class attributes.
440
457
 
441
- #---------------#
442
- # .attribute?() #
443
- #---------------#
458
+ ```ruby
459
+ Person.attributes # ["age", "first_name", "last_name"]
460
+ ```
444
461
 
445
- Person.attribute?(:name) # true
446
- Person.attribute?('name') # true
447
- Person.attribute?('foo') # false
448
- Person.attribute?(:foo) # false
462
+ ### `.attribute?()`
449
463
 
450
- # ---
464
+ Checking the existence of some attribute.
451
465
 
452
- person = Person.new(age: 20)
466
+ ```ruby
467
+ Person.attribute?(:first_name) # true
468
+ Person.attribute?('first_name') # true
453
469
 
454
- #---------------------#
455
- # #defined_attributes #
456
- #---------------------#
470
+ Person.attribute?('foo') # false
471
+ Person.attribute?(:foo) # false
472
+ ```
457
473
 
458
- person.defined_attributes # ['name', 'age']
474
+ ### `#attribute?()`
459
475
 
460
- #---------------#
461
- # #attribute?() #
462
- #---------------#
476
+ Checking the existence of some attribute in an instance.
477
+
478
+ ```ruby
479
+ person = Person.new(age: 20)
463
480
 
464
481
  person.attribute?(:name) # true
465
482
  person.attribute?('name') # true
483
+
466
484
  person.attribute?('foo') # false
467
485
  person.attribute?(:foo) # false
486
+ ```
487
+
488
+ ### `#attributes()`
489
+
490
+ Fetching all the attributes with their values.
491
+
492
+ ```ruby
493
+ person1 = Person.new(age: 20)
494
+ person1.attributes # {"age"=>20, "first_name"=>"John", "last_name"=>"Doe"}
495
+
496
+ person2 = Person.new(first_name: 'Rodrigo', last_name: 'Rodrigues')
497
+ person2.attributes # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}
498
+ ```
499
+
500
+ #### `#attributes(keys_as:)`
501
+
502
+ Use the `keys_as:` option with `Symbol`/`:symbol` or `String`/`:string` to transform the attributes hash keys.
503
+
504
+ ```ruby
505
+ person1 = Person.new(age: 20)
506
+ person2 = Person.new(first_name: 'Rodrigo', last_name: 'Rodrigues')
507
+
508
+ person1.attributes(keys_as: Symbol) # {:age=>20, :first_name=>"John", :last_name=>"Doe"}
509
+ person2.attributes(keys_as: String) # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}
510
+
511
+ person1.attributes(keys_as: :symbol) # {:age=>20, :first_name=>"John", :last_name=>"Doe"}
512
+ person2.attributes(keys_as: :string) # {"age"=>nil, "first_name"=>"Rodrigo", "last_name"=>"Rodrigues"}
513
+ ```
514
+
515
+ #### `#attributes(*names)`
516
+
517
+ Slices the attributes to include only the given keys (in their types).
518
+
519
+ ```ruby
520
+ person = Person.new(age: 20)
521
+
522
+ person.attributes(:age) # {:age => 20}
523
+ person.attributes(:age, :first_name) # {:age => 20, :first_name => "John"}
524
+ person.attributes('age', 'last_name') # {"age" => 20, "last_name" => "Doe"}
525
+
526
+ person.attributes(:age, 'last_name') # {:age => 20, "last_name" => "Doe"}
527
+
528
+ # You could also use the keys_as: option to ensure the same type for all of the hash keys.
529
+
530
+ person.attributes(:age, 'last_name', keys_as: Symbol) # {:age=>20, :last_name=>"Doe"}
531
+ ```
532
+
533
+ #### `#attributes([names])`
534
+
535
+ As the previous example, this methods accepts a list of keys to slice the attributes.
536
+
537
+ ```ruby
538
+ person = Person.new(age: 20)
539
+
540
+ person.attributes([:age]) # {:age => 20}
541
+ person.attributes([:age, :first_name]) # {:age => 20, :first_name => "John"}
542
+ person.attributes(['age', 'last_name']) # {"age" => 20, "last_name" => "Doe"}
543
+
544
+ person.attributes([:age, 'last_name']) # {:age => 20, "last_name" => "Doe"}
545
+
546
+ # You could also use the keys_as: option to ensure the same type for all of the hash keys.
547
+
548
+ person.attributes([:age, 'last_name'], keys_as: Symbol) # {:age=>20, :last_name=>"Doe"}
549
+ ```
550
+
551
+ #### `#attributes(with:, without:)`
552
+
553
+ Use the `with:` option to include any method value of the instance inside of the hash, and,
554
+ you can use the `without:` option to exclude one or more attribute keys from the final hash.
555
+
556
+ ```ruby
557
+ person = Person.new(age: 20)
468
558
 
469
- #---------------#
470
- # #attributes() #
471
- #---------------#
559
+ person.attributes(without: :age) # {"first_name"=>"John", "last_name"=>"Doe"}
560
+ person.attributes(without: [:age, :last_name]) # {"first_name"=>"John"}
472
561
 
473
- person.attributes # {'age'=>20, 'name'=>'John Doe'}
474
- Person.new(name: 'John').attributes # {'age'=>nil, 'name'=>'John'}
562
+ person.attributes(with: [:name], without: [:first_name, :last_name]) # {"age"=>20, "name"=>"John Doe"}
475
563
 
476
- #---------------------#
477
- # #attributes(*names) #
478
- #---------------------#
564
+ # To achieves the same output of the previous example, use the attribute names to slice only them.
479
565
 
480
- # Slices the attributes to include only the given keys.
481
- # Returns a hash containing the given keys (in their types).
566
+ person.attributes(:age, with: [:name]) # {:age=>20, "name"=>"John Doe"}
482
567
 
483
- person.attributes(:age) # {age: 20}
484
- person.attributes(:age, :name) # {age: 20, name: 'John Doe'}
485
- person.attributes('age', 'name') # {'age'=>20, 'name'=>'John Doe'}
568
+ # You could also use the keys_as: option to ensure the same type for all of the hash keys.
569
+
570
+ person.attributes(:age, with: [:name], keys_as: Symbol) # {:age=>20, :name=>"John Doe"}
571
+ ```
572
+
573
+ ### `#defined_attributes`
574
+
575
+ Listing all the available attributes.
576
+
577
+ ```ruby
578
+ person = Person.new(age: 20)
579
+
580
+ person.defined_attributes # ["age", "first_name", "last_name"]
486
581
  ```
487
582
 
488
583
  [⬆️ Back to Top](#table-of-contents-)
@@ -500,24 +595,30 @@ But, if you desire except one or more features, use the `Micro::Attributes.witho
500
595
  ```ruby
501
596
  Micro::Attributes.with(:initialize)
502
597
 
503
- Micro::Attributes.with(initialize: :strict)
598
+ Micro::Attributes.with(:initialize, :keys_as_symbol)
599
+
600
+ Micro::Attributes.with(:keys_as_symbol, initialize: :strict)
504
601
 
505
602
  Micro::Attributes.with(:diff, :initialize)
506
603
 
507
604
  Micro::Attributes.with(:diff, initialize: :strict)
508
605
 
606
+ Micro::Attributes.with(:diff, :keys_as_symbol, initialize: :strict)
607
+
509
608
  Micro::Attributes.with(:activemodel_validations)
510
609
 
511
610
  Micro::Attributes.with(:activemodel_validations, :diff)
512
611
 
513
612
  Micro::Attributes.with(:activemodel_validations, :diff, initialize: :strict)
613
+
614
+ Micro::Attributes.with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)
514
615
  ```
515
616
 
516
617
  The method `Micro::Attributes.with()` will raise an exception if no arguments/features were declared.
517
618
 
518
619
  ```ruby
519
620
  class Job
520
- include Micro::Attributes.with() # ArgumentError (Invalid feature name! Available options: :activemodel_validations, :diff, :initialize)
621
+ include Micro::Attributes.with() # ArgumentError (Invalid feature name! Available options: :accept, :activemodel_validations, :diff, :initialize, :keys_as_symbol)
521
622
  end
522
623
  ```
523
624
 
@@ -526,15 +627,19 @@ end
526
627
  Picking *except* one or more features
527
628
 
528
629
  ```ruby
529
- Micro::Attributes.without(:diff) # will load :activemodel_validations and initialize: :strict
630
+ Micro::Attributes.without(:diff) # will load :activemodel_validations, :keys_as_symbol and initialize: :strict
530
631
 
531
- Micro::Attributes.without(initialize: :strict) # will load :activemodel_validations and :diff
632
+ Micro::Attributes.without(initialize: :strict) # will load :activemodel_validations, :diff and :keys_as_symbol
532
633
  ```
533
634
 
534
635
  ## Picking all the features
535
636
 
536
637
  ```ruby
537
638
  Micro::Attributes.with_all_features
639
+
640
+ # This method returns the same of:
641
+
642
+ Micro::Attributes.with(:activemodel_validations, :diff, :keys_as_symbol, initialize: :strict)
538
643
  ```
539
644
 
540
645
  [⬆️ Back to Top](#table-of-contents-)
@@ -726,6 +831,40 @@ job.state # 'sleeping'
726
831
 
727
832
  [⬆️ Back to Top](#table-of-contents-)
728
833
 
834
+ ### Keys as symbol extension
835
+
836
+ Disables the indifferent access requiring the declaration/usage of the attributes as symbols.
837
+
838
+ The advantage of this extension over the default behavior is because it avoids an unnecessary allocation in memory of strings. All the keys are transformed into strings in the indifferent access mode, but, with this extension, this typecasting will be avoided. So, it has a better performance and reduces the usage of memory/Garbage collector, but gives for you the responsibility to always use symbols to set/access the attributes.
839
+
840
+ ```ruby
841
+ class Job
842
+ include Micro::Attributes.with(:initialize, :keys_as_symbol)
843
+
844
+ attribute :id
845
+ attribute :state, default: 'sleeping'
846
+ end
847
+
848
+ job = Job.new(id: 1)
849
+
850
+ job.attributes # {:id => 1, :state => "sleeping"}
851
+
852
+ job.attribute?(:id) # true
853
+ job.attribute?('id') # false
854
+
855
+ job.attribute(:id) # 1
856
+ job.attribute('id') # nil
857
+
858
+ job.attribute!(:id) # 1
859
+ job.attribute!('id') # NameError (undefined attribute `id)
860
+ ```
861
+
862
+ As you could see in the previous example only symbols will work to do something with the attributes.
863
+
864
+ This extension also changes the `diff extension` making everything (arguments, outputs) working only with symbols.
865
+
866
+ [⬆️ Back to Top](#table-of-contents-)
867
+
729
868
  # Development
730
869
 
731
870
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -15,7 +15,8 @@ module Micro
15
15
 
16
16
  base.class_eval do
17
17
  private_class_method :__attributes, :__attribute_reader
18
- private_class_method :__attribute_assign, :__attributes_data_to_assign
18
+ private_class_method :__attribute_assign, :__attributes_groups
19
+ private_class_method :__attributes_required_add, :__attributes_data_to_assign
19
20
  end
20
21
 
21
22
  def base.inherited(subclass)
@@ -37,8 +38,8 @@ module Micro
37
38
  Features.all
38
39
  end
39
40
 
40
- def attribute?(name)
41
- self.class.attribute?(name)
41
+ def attribute?(name, include_all = false)
42
+ self.class.attribute?(name, include_all)
42
43
  end
43
44
 
44
45
  def attribute(name)
@@ -52,69 +53,108 @@ module Micro
52
53
  def attribute!(name, &block)
53
54
  attribute(name) { |name| return block ? block[name] : name }
54
55
 
55
- raise NameError, "undefined attribute `#{name}"
56
+ raise NameError, __attribute_access_error_message(name)
57
+ end
58
+
59
+ def defined_attributes(option = nil)
60
+ return self.class.attributes_by_visibility if option == :by_visibility
61
+
62
+ @defined_attributes ||= self.class.attributes
56
63
  end
57
64
 
58
65
  def attributes(*names)
59
66
  return __attributes if names.empty?
60
67
 
61
- names.each_with_object({}) do |name, memo|
62
- memo[name] = attribute(name) if attribute?(name)
68
+ options = names.last.is_a?(Hash) ? names.pop : Kind::Empty::HASH
69
+
70
+ names.flatten!
71
+
72
+ without_option = Array(options.fetch(:without, Kind::Empty::ARRAY))
73
+
74
+ keys = names.empty? ? defined_attributes - without_option.map { |value| __attribute_key(value) } : names - without_option
75
+
76
+ data = keys.each_with_object({}) { |key, memo| memo[key] = attribute(key) if attribute?(key) }
77
+
78
+ with_option = Array(options.fetch(:with, Kind::Empty::ARRAY))
79
+
80
+ unless with_option.empty?
81
+ extra = with_option.each_with_object({}) { |key, memo| memo[__attribute_key(key)] = public_send(key) }
82
+
83
+ data.merge!(extra)
63
84
  end
64
- end
65
85
 
66
- def defined_attributes
67
- @defined_attributes ||= self.class.attributes
86
+ Utils::Hashes.keys_as(options[:keys_as], data)
68
87
  end
69
88
 
70
89
  protected
71
90
 
72
91
  def attributes=(arg)
73
- hash = Utils.stringify_hash_keys(arg)
92
+ hash = self.class.__attributes_keys_transform__(arg)
74
93
 
75
94
  __attributes_missing!(hash)
76
95
 
96
+ __call_before_attributes_assign
77
97
  __attributes_assign(hash)
98
+ __call_after_attributes_assign
99
+
100
+ __attributes
78
101
  end
79
102
 
80
103
  private
81
104
 
82
- ExtractAttribute = -> (other, key) {
83
- return Utils::HashAccess.(other, key) if other.respond_to?(:[])
84
-
85
- other.public_send(key) if other.respond_to?(key)
86
- }
105
+ def __call_before_attributes_assign; end
106
+ def __call_after_attributes_assign; end
87
107
 
88
108
  def extract_attributes_from(other)
89
- defined_attributes.each_with_object({}) do |key, memo|
90
- memo[key] = ExtractAttribute.(other, key)
91
- end
109
+ Utils::ExtractAttribute.from(other, keys: defined_attributes)
110
+ end
111
+
112
+ def __attribute_access_error_message(name)
113
+ return "tried to access a private attribute `#{name}" if attribute?(name, true)
114
+
115
+ "undefined attribute `#{name}"
116
+ end
117
+
118
+ def __attribute_key(value)
119
+ self.class.__attribute_key_transform__(value)
92
120
  end
93
121
 
94
122
  def __attributes
95
123
  @__attributes ||= {}
96
124
  end
97
125
 
98
- FetchValueToAssign = -> (value, default) do
99
- if default.respond_to?(:call)
100
- callable = default.is_a?(Proc) ? default : default.method(:call)
126
+ FetchValueToAssign = -> (value, attribute_data, keep_proc = false) do
127
+ default = attribute_data[0]
101
128
 
102
- callable.arity > 0 ? callable.call(value) : callable.call
103
- else
104
- value.nil? ? default : value
105
- end
129
+ value_to_assign =
130
+ if default.is_a?(Proc) && !keep_proc
131
+ default.arity > 0 ? default.call(value) : default.call
132
+ else
133
+ value.nil? ? default : value
134
+ end
135
+
136
+ return value_to_assign unless to_freeze = attribute_data[2]
137
+ return value_to_assign.freeze if to_freeze == true
138
+ return value_to_assign.dup.freeze if to_freeze == :after_dup
139
+ return value_to_assign.clone.freeze if to_freeze == :after_clone
140
+
141
+ raise NotImplementedError
106
142
  end
107
143
 
108
144
  def __attributes_assign(hash)
109
- self.class.__attributes_data__.each do |name, default|
110
- __attribute_assign(name, FetchValueToAssign.(hash[name], default)) if attribute?(name)
145
+ self.class.__attributes_data__.each do |name, attribute_data|
146
+ __attribute_assign(name, hash[name], attribute_data) if attribute?(name, true)
111
147
  end
112
148
 
113
149
  __attributes.freeze
114
150
  end
115
151
 
116
- def __attribute_assign(name, value)
117
- __attributes[name] = instance_variable_set("@#{name}", value)
152
+ def __attribute_assign(name, initialize_value, attribute_data)
153
+ value_to_assign = FetchValueToAssign.(initialize_value, attribute_data)
154
+
155
+ ivar_value = instance_variable_set("@#{name}", value_to_assign)
156
+
157
+ __attributes[name] = ivar_value if attribute_data[3] == :public
118
158
  end
119
159
 
120
160
  MISSING_KEYWORD = 'missing keyword'.freeze