pure-struct 1.0.1 → 1.0.2

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: 62640381beae69f1f6332211fa9569853d48822e84c6c243b81d49312f059cb6
4
- data.tar.gz: 70cb2eeb0153967d2864594ebdf504ee31014f6c9e7da8d733eb1f6b7a51b1b0
3
+ metadata.gz: 8f67ccb48a2f07b33076622ac6493bbb6911097996551228b404a236d3bd8116
4
+ data.tar.gz: 7421eac80b3a68e526fbc76cb7b61c3a3fc0bf83494fdb6b2c41cab8be07505a
5
5
  SHA512:
6
- metadata.gz: 407a0c254b2b8178e11626d73f64f6fc7ba31b1d27ba7e4157029073d543dba3fdf98e2ccf1c3a8ed97ba278b8f77ce729c736ff90cd2fd729e1994d425ab00e
7
- data.tar.gz: 174c6340a217b17904fa37e44747929f8d87fa1a3aac81d1da1d546aa0ad33b570eb564b7e4503dd254ba1aa2e15d47dbe94cc7921b22e4ea3dae76af1191ea5
6
+ metadata.gz: b09942a08d7dec8df2ada8eb67c1be6045cdc1d3db7093b37dcc67501f579d48650846030f04aa5cb1a2b64b82b8a2b3836ef8db5f26b5050eae8ec2b760359b
7
+ data.tar.gz: cd27449d4554b02ac94c941637691a9309e2e3a7f2813ad8a2fd17c252c6fbef296ed51c00a57177beed89ba96c8d77cbf6f2cac6e54110b58a6b9d7e5b21ab5
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.2
4
+
5
+ - Ensure StructClass.inspect shows `(keyword_init: true)` (e.g. `Person(keyword_init: true) `)
6
+ - Implement to_s (e.g. `#<struct Person full_name=nil, age=nil>` for `Person` class )
7
+ - Fix issue with ability to handle cycles in to_s/inspect implementation (printing #<struct #<Class:0x00007ff874073590>:...> when self object is encountered within attributes)
8
+ - Fix issue with ability to handle cycles in hash implementation, including Opal
9
+ - Fix issue with ability to handle cycles in equality
10
+ - Fix issue with ability to handle cycles in equivalence
11
+
3
12
  ## 1.0.1
4
13
 
5
14
  - Include `Enumerable` just as in native `Struct`
data/README.md CHANGED
@@ -22,7 +22,13 @@ Run:
22
22
  Or add to [Gemfile](https://bundler.io/man/gemfile.5.html):
23
23
 
24
24
  ```ruby
25
- gem 'pure-struct', '~> 1.0.1'
25
+ gem 'pure-struct', '~> 1.0.2'
26
+ ```
27
+
28
+ If you want to use in [Opal](https://opalrb.com/) only inside [Rails](https://rubyonrails.org/), suffix with `require: false`:
29
+
30
+ ```ruby
31
+ gem 'pure-struct', '~> 1.0.2', require: false
26
32
  ```
27
33
 
28
34
  And, run:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.2
@@ -19,16 +19,48 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
+ # Native Ruby built-in Struct implementation aliased as NativeStruct (with Struct pointing to the new pure Ruby implementation)
22
23
  NativeStruct = Struct
23
24
  Object.send(:remove_const, :Struct) if Object.constants.include?(:Struct)
24
25
 
25
- # Optional re-implmentation of Struct in Pure Ruby (to get around JS issues in Opal Struct)
26
+ # Pure Ruby re-implementation of Struct to ensure cross-Ruby functionality where needed (e.g. Opal)
27
+ #
28
+ # Struct class object instances store members in @members Array and member values in @member_values Hash
29
+ #
30
+ # API perfectly matches that of Native Ruby built-in Struct.
31
+ #
32
+ # Native Ruby built-in Struct implementation is aliased as NativeStruct
26
33
  class Struct
27
34
  include Enumerable
28
35
 
29
36
  class << self
37
+ alias __new__ new
38
+ send(:private, :__new__)
39
+
40
+ def new(class_name_or_attribute, *attributes, keyword_init: false)
41
+ raise 'Arguments cannot be nil' if ARG_VALIDATION[class_name_or_attribute, *attributes]
42
+ class_name = CLASS_NAME_EXTRACTION[class_name_or_attribute]
43
+ attributes.unshift(class_name_or_attribute) if class_name.nil?
44
+ attributes = attributes.map(&:to_sym)
45
+ struct_class = Class.new(self, &CLASS_DEFINITION_FOR_ATTRIBUTES[attributes, keyword_init])
46
+ class_name.nil? ? struct_class : const_set(class_name, struct_class)
47
+ end
48
+
49
+ private
50
+
30
51
  CLASS_DEFINITION_FOR_ATTRIBUTES = lambda do |attributes, keyword_init|
31
52
  lambda do |defined_class|
53
+ defined_class.class_variable_set(:@@attributes, attributes)
54
+ defined_class.class_variable_set(:@@keyword_init, keyword_init)
55
+
56
+ defined_class.singleton_class.define_method(:new) {|*args, &block| __new__(*args, &block)}
57
+ defined_class.singleton_class.alias_method(:__inspect__, :inspect)
58
+ defined_class.private_class_method :__inspect__
59
+
60
+ defined_class.singleton_class.define_method(:inspect) do
61
+ "#{__inspect__}#{'(keyword_init: true)' if keyword_init}"
62
+ end
63
+
32
64
  attributes.each do |attribute|
33
65
  define_method(attribute) do
34
66
  self[attribute]
@@ -38,74 +70,6 @@ class Struct
38
70
  end
39
71
  end
40
72
 
41
- define_method(:members) do
42
- (@members ||= attributes).clone
43
- end
44
-
45
- def []=(attribute, value)
46
- normalized_attribute = attribute.to_sym
47
- raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
48
- @member_values[normalized_attribute] = value
49
- end
50
-
51
- def [](attribute)
52
- normalized_attribute = attribute.to_sym
53
- raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
54
- @member_values[normalized_attribute]
55
- end
56
-
57
- def each(&block)
58
- to_a.each(&block)
59
- end
60
-
61
- def each_pair(&block)
62
- @member_values.each_pair(&block)
63
- end
64
-
65
- def to_h
66
- @member_values.clone
67
- end
68
-
69
- def to_a
70
- @member_values.values
71
- end
72
-
73
- def size
74
- @members.size
75
- end
76
- alias length size
77
-
78
- def dig(*args)
79
- @member_values.dig(*args)
80
- end
81
-
82
- def select(&block)
83
- to_a.select(&block)
84
- end
85
-
86
- def eql?(other)
87
- instance_of?(other.class) &&
88
- @members.all? { |key| self[key].eql?(other[key]) }
89
- end
90
-
91
- def ==(other)
92
- other = coerce(other).first if respond_to?(:coerce, true)
93
- other.kind_of?(self.class) &&
94
- @members.all? { |key| self[key] == other[key] }
95
- end
96
-
97
- def hash
98
- if RUBY_ENGINE == 'opal'
99
- # Opal doesn't implement hash as Integer everywhere, returning strings as themselves,
100
- # so build a String everywhere for now as the safest common denominator to be consistent.
101
- self.class.hash.to_s +
102
- to_a.each_with_index.map {|value, i| (i+1).to_s + value.hash.to_s}.reduce(:+)
103
- else
104
- self.class.hash +
105
- to_a.each_with_index.map {|value, i| i+1 * value.hash}.sum
106
- end
107
- end
108
-
109
73
  if keyword_init
110
74
  def initialize(struct_class_keyword_args = {})
111
75
  members
@@ -124,7 +88,9 @@ class Struct
124
88
  end
125
89
  end
126
90
  end
91
+
127
92
  end
93
+
128
94
  end
129
95
 
130
96
  ARG_VALIDATION = lambda do |class_name_or_attribute, *attributes|
@@ -132,22 +98,126 @@ class Struct
132
98
  end
133
99
 
134
100
  CLASS_NAME_EXTRACTION = lambda do |class_name_or_attribute|
135
- if class_name_or_attribute.is_a?(String) && RUBY_ENGINE != 'opal'
136
- raise NameError, "identifier name needs to be constant" unless class_name_or_attribute.match(/^[A-Z]/)
101
+ if class_name_or_attribute.is_a?(String) && (RUBY_ENGINE != 'opal' || class_name_or_attribute.match(/^[A-Z]/))
102
+ raise NameError, "identifier name needs to be constant" if RUBY_ENGINE != 'opal' && !class_name_or_attribute.match(/^[A-Z]/)
137
103
  class_name_or_attribute
138
104
  end
139
105
  end
140
-
141
- alias __new__ new
142
- def new(class_name_or_attribute, *attributes, keyword_init: false)
143
- raise 'Arguments cannot be nil' if ARG_VALIDATION[class_name_or_attribute, *attributes]
144
- class_name = CLASS_NAME_EXTRACTION[class_name_or_attribute]
145
- attributes.unshift(class_name_or_attribute) if class_name.nil?
146
- attributes = attributes.map(&:to_sym)
147
- struct_class = Class.new(self, &CLASS_DEFINITION_FOR_ATTRIBUTES[attributes, keyword_init])
148
- struct_class.singleton_class.define_method(:new) {|*args, &block| __new__(*args, &block)}
149
- class_name.nil? ? struct_class : const_set(class_name, struct_class)
106
+
107
+ end
108
+
109
+ # Returns member symbols (strings in Opal) representing Struct attribute names
110
+ def members
111
+ (@members ||= self.class.class_variable_get(:@@attributes)).clone
112
+ end
113
+
114
+ def []=(attribute, value)
115
+ normalized_attribute = attribute.to_sym
116
+ raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
117
+ @member_values[normalized_attribute] = value
118
+ end
119
+
120
+ def [](attribute)
121
+ normalized_attribute = attribute.to_sym
122
+ raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
123
+ @member_values[normalized_attribute]
124
+ end
125
+
126
+ # Iterates through each value
127
+ def each(&block)
128
+ to_a.each(&block)
129
+ end
130
+
131
+ # Iterates through each member value pair
132
+ def each_pair(&block)
133
+ @member_values.each_pair(&block)
134
+ end
135
+
136
+ # Returns member values Hash (includes member keys)
137
+ def to_h
138
+ @member_values.clone
139
+ end
140
+
141
+ # Returns values Array (no member keys)
142
+ def to_a
143
+ @member_values.values
144
+ end
145
+
146
+ # Original Object #inspect implementation
147
+ alias __inspect__ inspect
148
+ send(:private, :__inspect__)
149
+
150
+ # Prints Struct member values including class name if set
151
+ #
152
+ # #inspect does the same thing
153
+ #
154
+ # __inspect__ aliases the original Object inspect implementation
155
+ def to_s
156
+ member_values_string = @member_values.map do |member, value|
157
+ if value.equal?(self)
158
+ value_class_string = self.class.send(:__inspect__).split.first
159
+ value_string = "#<struct #{value_class_string}:...>"
160
+ else
161
+ value_string = value.inspect
162
+ end
163
+ "#{member}=#{value_string}"
164
+ end.join(', ')
165
+ class_name_string = "#{self.class.name} " unless self.class.name.nil?
166
+ "#<struct #{class_name_string}#{member_values_string}>"
167
+ end
168
+ alias inspect to_s
169
+
170
+ def size
171
+ @members.size
172
+ end
173
+ alias length size
174
+
175
+ def dig(*args)
176
+ @member_values.dig(*args)
177
+ end
178
+
179
+ # Selects values with block (only value is passed in as block arg)
180
+ def select(&block)
181
+ to_a.select(&block)
182
+ end
183
+
184
+ def eql?(other)
185
+ instance_of?(other.class) &&
186
+ @members.all? { |key| (self[key].equal?(self) && other[key].equal?(self)) || self[key].eql?(other[key]) }
187
+ end
188
+
189
+ def ==(other)
190
+ other = coerce(other).first if respond_to?(:coerce, true)
191
+ other.kind_of?(self.class) &&
192
+ @members.all? { |key| (self[key].equal?(self) && other[key].equal?(self)) || self[key] == other[key] }
193
+ end
194
+
195
+ # Return compound hash value consisting of Struct class hash and all indexed value hashes
196
+ #
197
+ # Opal doesn't implement hash as Integer everywhere, returning strings as themselves,
198
+ # so this returns a String in Opal as a safe common denominator for all object types.
199
+ def hash
200
+ return __hash__opal__ if RUBY_ENGINE == 'opal'
201
+ class_hash = self.class.hash
202
+ indexed_values = to_a.each_with_index
203
+ value_hashes = indexed_values.map do |value, i|
204
+ value_hash = value.equal?(self) ? class_hash : value.hash
205
+ i+1 * value_hash
150
206
  end
151
-
207
+ class_hash + value_hashes.sum
208
+ end
209
+
210
+ private
211
+
212
+ def __hash__opal__
213
+ class_hash = self.class.hash
214
+ indexed_values = to_a.each_with_index
215
+ class_hash = class_hash.to_s
216
+ value_hashes = indexed_values.map do |value, i|
217
+ value_hash_string = value.equal?(self) ? class_hash : value.hash.to_s
218
+ (i+1).to_s + value_hash_string
219
+ end
220
+ class_hash + value_hashes.reduce(:+)
152
221
  end
222
+
153
223
  end
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: pure-struct 1.0.1 ruby lib
5
+ # stub: pure-struct 1.0.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "pure-struct".freeze
9
- s.version = "1.0.1"
9
+ s.version = "1.0.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2021-01-09"
14
+ s.date = "2021-01-10"
15
15
  s.description = "Pure Ruby re-implementation of Struct to ensure cross-Ruby functionality where needed (e.g. Opal)".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -38,7 +38,7 @@ Gem::Specification.new do |s|
38
38
 
39
39
  if s.respond_to? :add_runtime_dependency then
40
40
  s.add_development_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
41
- s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
41
+ s.add_development_dependency(%q<rdoc>.freeze, [">= 6.3.0"])
42
42
  s.add_development_dependency(%q<bundler>.freeze, [">= 1.0"])
43
43
  s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
44
44
  s.add_development_dependency(%q<simplecov>.freeze, ["~> 0.16.0"])
@@ -46,7 +46,7 @@ Gem::Specification.new do |s|
46
46
  s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 0"])
47
47
  else
48
48
  s.add_dependency(%q<rspec>.freeze, ["~> 3.5.0"])
49
- s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
49
+ s.add_dependency(%q<rdoc>.freeze, [">= 6.3.0"])
50
50
  s.add_dependency(%q<bundler>.freeze, [">= 1.0"])
51
51
  s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
52
52
  s.add_dependency(%q<simplecov>.freeze, ["~> 0.16.0"])
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pure-struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-09 00:00:00.000000000 Z
11
+ date: 2021-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: rdoc
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.12'
33
+ version: 6.3.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3.12'
40
+ version: 6.3.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement