astruct 2.11.0 → 3.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6990e238c22d0d907f210e88381630a930b20355
4
+ data.tar.gz: 028d8e80cc9b222c3383c511f514426a3d888eea
5
+ SHA512:
6
+ metadata.gz: 64a93ec3749af729bd2d2bce28a225f22ec46e8ad415e4ba8004f69ad82dd81a0cf9b925a6040b76fb316a4bbf6e954542bce07540503d5ce6f9a29289e418fd
7
+ data.tar.gz: 5708d234e2e1cea7f644dc7be147ebc267d3c931aacc5a93b7a4386a2cb588ed1949eb4acfaf0d360871843dbe1a0eefba09f4ba6d258fd897be26639ff43e67
@@ -1,6 +1,5 @@
1
- require_relative 'astruct/module'
1
+ require_relative "astruct/behavior"
2
2
 
3
- #
4
3
  # = astruct.rb: AltStruct implementation
5
4
  #
6
5
  # Author:: Kurtis Rainbolt-Greene
@@ -10,85 +9,11 @@ require_relative 'astruct/module'
10
9
  # create hash-like classes. Allowing you to create an object that can
11
10
  # dynamically accept accessors and behaves very much like a Hash.
12
11
  #
13
-
14
- #
15
- # An AltStruct is a data structure, similar to a Hash, that allows the
16
- # definition of arbitrary attributes with their accompanying values. This is
17
- # accomplished by using Ruby's metaprogramming to define methods on the class
18
- # itself.
19
- #
20
-
21
- #
22
- # == Examples:
23
- #
24
- # require 'astruct'
25
- #
26
- # class Profile < AltStruct
27
- #
28
- # end
29
- #
30
- # person = Profile.new name: "John Smith"
31
- # person.age = 70
32
- #
33
- # puts person.name # => "John Smith"
34
- # puts person.age # => 70
35
- # puts person.dump # => { :name => "John Smith", :age => 70 }
36
- #
37
-
38
- #
39
- # An AltStruct employs a Hash internally to store the methods and values and
40
- # can even be initialized with one:
41
- #
42
- # australia = AltStruct.new country: "Australia", population: 20_000_000
43
- # puts australia.inspect # => <AltStruct country="Australia", population=20000000>
44
- #
45
-
46
- #
47
- # Hash keys with spaces or characters that would normally not be able to use for
48
- # method calls (e.g. ()[]*) will not be immediately available on the
49
- # AltStruct object as a method for retrieval or assignment, but can be still be
50
- # reached through the Object#send method.
51
- #
52
- # measurements = AltStruct.new "length (in inches)" => 24
53
- # measurements.send "length (in inches)" # => 24
54
- #
55
- # data_point = AltStruct.new :queued? => true
56
- # data_point.queued? # => true
57
- # data_point.send "queued?=", false
58
- # data_point.queued? # => false
59
- #
60
-
61
- #
62
- # Removing the presence of a method requires the execution the delete_field
63
- # or delete (like a hash) method as setting the property value to +nil+
64
- # will not remove the method.
65
- #
66
- # first_pet = AltStruct.new :name => 'Rowdy', :owner => 'John Smith'
67
- # first_pet.owner = nil
68
- # second_pet = AltStruct.new :name => 'Rowdy'
69
- #
70
- # first_pet == second_pet # -> false
71
- #
72
- # first_pet.delete_field(:owner)
73
- # first_pet == second_pet # -> true
74
- #
75
-
76
- #
77
- # == Implementation:
78
- #
79
- # An AltStruct utilizes Ruby's method lookup structure to and find and define
80
- # the necessary methods for properties. This is accomplished through the method
81
- # method_missing and define_method.
82
- #
83
-
84
- #
85
- # This should be a consideration if there is a concern about the performance of
86
- # the objects that are created, as there is much more overhead in the setting
87
- # of these properties compared to using a Hash or a Struct.
88
- #
89
12
  class AltStruct
90
- # We include all of the AltStruct::M Module in order to give AltStruct
13
+ # We include all of the AltStruct::Behavior Module in order to give AltStruct
91
14
  # the same behavior as OpenStruct. It's better, however, to simply
92
- # include AltStruct::M into your own class.
93
- include AltStruct::M
15
+ # include AltStruct::Behavior into your own class.
16
+ include AltStruct::Behavior
94
17
  end
18
+
19
+ require_relative "astruct/version"
@@ -0,0 +1,251 @@
1
+ require "set"
2
+
3
+ class AltStruct
4
+ # An AltStruct is a data structure, similar to a Hash, that allows the
5
+ # definition of arbitrary attributes with their accompanying values. This is
6
+ # accomplished by using Ruby's meta-programming to define methods on the
7
+ # class itself.
8
+ #
9
+ #
10
+ # == Examples:
11
+ #
12
+ # require 'astruct'
13
+ #
14
+ # class Profile < AltStruct
15
+ #
16
+ # end
17
+ #
18
+ # person = Profile.new name: "John Smith"
19
+ # person.age = 70
20
+ #
21
+ # puts person.name # => "John Smith"
22
+ # puts person.age # => 70
23
+ # puts person.dump # => { :name => "John Smith", :age => 70 }
24
+ #
25
+ # An AltStruct employs a Hash internally to store the methods and values and
26
+ # can even be initialized with one:
27
+ #
28
+ # australia = AltStruct.new(
29
+ # country: "Australia",
30
+ # population: 20_000_000
31
+ # )
32
+ # puts australia.inspect
33
+ # # => <AltStruct country="Australia", population=20000000>
34
+ #
35
+ # Hash keys with spaces or characters that would normally not be able to use
36
+ # for method calls (e.g. ()[]*) will not be immediately available on the
37
+ # AltStruct object as a method for retrieval or assignment, but can be still
38
+ # be reached through the `Object#send` method.
39
+ #
40
+ # measurements = AltStruct.new "length (in inches)" => 24
41
+ # measurements.send "length (in inches)" # => 24
42
+ #
43
+ # data_point = AltStruct.new :queued? => true
44
+ # data_point.queued? # => true
45
+ # data_point.send "queued?=", false
46
+ # data_point.queued? # => false
47
+ #
48
+ # Removing the presence of a method requires the execution the delete_field
49
+ # or delete (like a hash) method as setting the property value to +nil+
50
+ # will not remove the method.
51
+ #
52
+ # first_pet = AltStruct.new :name => 'Rowdy', :owner => 'John Smith'
53
+ # first_pet.owner = nil
54
+ # second_pet = AltStruct.new :name => 'Rowdy'
55
+ #
56
+ # first_pet == second_pet # -> false
57
+ #
58
+ # first_pet.delete_field(:owner)
59
+ # first_pet == second_pet # -> true
60
+ #
61
+ #
62
+ # == Implementation:
63
+ #
64
+ # An AltStruct utilizes Ruby's method lookup structure to and find and define
65
+ # the necessary methods for properties. This is accomplished through the
66
+ # method `method_missing` and `define_singleton_method`.
67
+ #
68
+ # This should be a consideration if there is a concern about the performance
69
+ # of the objects that are created, as there is much more overhead in the
70
+ # setting of these properties compared to using a Hash or a Struct.
71
+ module Behavior
72
+ THREAD_KEY = :__as_ids__ # :nodoc:
73
+ NESTED_INSPECT = "...".freeze
74
+ INSPECT_DELIMITER = ", ".freeze
75
+ WRAP_PATTERN = /__/.freeze
76
+ UNSETABLE_PATTERN = /\@|\[|\]|\=\=|\~|\>|\<|\!\=/.freeze
77
+ SUFFIX_PATTERN = /(\?|\!)$/.freeze
78
+
79
+ # We want to give easy access to the table
80
+ attr_reader :table
81
+
82
+ # We want to automatically wrap important Ruby object methods
83
+ Object.instance_methods.each do |meth|
84
+ case meth
85
+
86
+ # Don't bother with already wrapped methods
87
+ when WRAP_PATTERN then next
88
+
89
+ # Skip methods that can't be set anyways
90
+ when UNSETABLE_PATTERN then next
91
+
92
+ # Get around Ruby's stupid method signature problems with ? and !
93
+ # suffixes
94
+ when SUFFIX_PATTERN then alias_method("__#{meth[0...-1]}__#{meth[-1]}".to_sym, meth)
95
+
96
+ # Finally, wrap regular methods
97
+ else alias_method("__#{meth}__".to_sym, meth)
98
+ end
99
+ end
100
+
101
+ # Create a new field for each of the key/value pairs passed.
102
+ # By default the resulting OpenStruct object will have no
103
+ # attributes. If no pairs are passed avoid any work.
104
+ #
105
+ # require "astruct"
106
+ # hash = { "country" => "Australia", :population => 20_000_000 }
107
+ # data = AltStruct.new hash
108
+ #
109
+ # p data # => <AltStruct country="Australia" population=20000000>
110
+ #
111
+ # If you happen to be inheriting then you can define your own
112
+ # `@table` ivar before the `super()` call. AltStruct will respect
113
+ # your `@table`.
114
+ #
115
+ def initialize(pairs = {})
116
+ @table ||= {}
117
+ __iterate_set_over__(pairs) unless pairs.empty?
118
+ end
119
+
120
+ # This is the `load()` method, which works like initialize in that it
121
+ # will create new fields for each pair passed. It mimics the behavior of a
122
+ # Hash#merge.
123
+ def __load__(pairs)
124
+ __iterate_set_over__(pairs) unless pairs.empty?
125
+ end
126
+ alias_method :marshal_load, :__load__
127
+ alias_method :load, :__load__
128
+ alias_method :merge, :__load__
129
+
130
+ # This is the `load!()` method, which works like Hash#merge!
131
+ # See: `AltStruct#load()`
132
+ def __load__!(pairs)
133
+ __iterate_set_over__(pairs, true)
134
+ end
135
+ alias_method :marshal_load!, :__load__!
136
+ alias_method :load!, :__load__!
137
+ alias_method :merge!, :__load__!
138
+
139
+ # The `dump()` takes the table and out puts in it's natural hash
140
+ # format. In addition you can pass along a specific set of keys to
141
+ # dump.
142
+ def __dump__(*keys)
143
+ if keys.empty? then @table else __dump_specific__(keys) end
144
+ end
145
+ alias_method :marshal_dump, :__dump__
146
+ alias_method :dump, :__dump__
147
+ alias_method :to_hash, :__dump__
148
+
149
+ def __inspect__
150
+ "#<#{__class__}#{__dump_inspect__}>"
151
+ end
152
+ alias_method :inspect, :__inspect__
153
+ alias_method :to_sym, :__inspect__
154
+
155
+ # The `delete()` method removes a key/value pair on the @table
156
+ # and on the singleton class. It also mimics the Hash#delete method.
157
+ def __delete__(key)
158
+ __singleton_class__.send(:remove_method, key)
159
+ __singleton_class__.send(:remove_method, "#{key}=")
160
+ @table.delete(key.to_sym)
161
+ end
162
+ alias_method :delete_field, :__delete__
163
+ alias_method :delete, :__delete__
164
+
165
+ # The `method_missing()` method catches all non-tabled method calls.
166
+ # The AltStruct object will return two specific errors depending on
167
+ # the call.
168
+ def method_missing(method, *arguments)
169
+ name = method.to_s
170
+ if name.split("").last == "=" && arguments.size == 1
171
+ __define_field__(name.chomp!("="), arguments.first)
172
+ else
173
+ if name.split.last != "="
174
+ super
175
+ else arguments.size > 1
176
+ raise(ArgumentError,"wrong number of arguments (#{arguments.size} for 1)")
177
+ end
178
+ end
179
+ end
180
+
181
+ def ==(other)
182
+ if other.respond_to?(:table)
183
+ table == other.table
184
+ else
185
+ false
186
+ end
187
+ end
188
+
189
+ def freeze
190
+ super
191
+ @table.freeze
192
+ end
193
+ alias_method :__freeze__, :freeze
194
+ alias_method :__frozen?, :frozen?
195
+
196
+ private
197
+
198
+ def __dump_inspect__
199
+ Thread.current[THREAD_KEY] ||= Set.new
200
+
201
+ if __dump__.any? then " #{__dump_subinspect__}" else "" end.tap do
202
+ __thread_ids__.delete(__object_id__)
203
+ end
204
+ end
205
+
206
+ def __dump_subinspect__
207
+ if __thread_ids__.add?(__object_id__)
208
+ __dump_string__.join(INSPECT_DELIMITER)
209
+ else
210
+ NESTED_INSPECT
211
+ end
212
+ end
213
+
214
+ def __thread_ids__
215
+ Thread.current[THREAD_KEY]
216
+ end
217
+
218
+ def __define_field__(key, value)
219
+ __define_accessor__(key)
220
+ __set_table__(key, value)
221
+ end
222
+
223
+ def __define_accessor__(key)
224
+ singleton_class.send(:define_method, key) { @table[key] }
225
+ singleton_class.send(:define_method, "#{key}=") { |v| @table[key] = v }
226
+ end
227
+
228
+ def __set_table__(key, value)
229
+ @table.merge!(key => value) unless key.nil?
230
+ end
231
+
232
+ def __dump_specific__(keys)
233
+ @table.keep_if { |key| keys.include?(key.to_sym) }
234
+ end
235
+
236
+ def __dump_string__
237
+ __dump__.map { |key, value| "#{key}=#{value.inspect}" }
238
+ end
239
+
240
+ def __iterate_set_over__(pairs, force = false)
241
+ pairs.each do |key, value|
242
+ if force && respond_to?(key)
243
+ __set_table__(key, value)
244
+ else
245
+ __define_accessor__(key)
246
+ __set_table__(key, value)
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -1,4 +1,3 @@
1
1
  class AltStruct
2
- # This is the AltStruct version
3
- VERSION = "2.11.0"
2
+ VERSION = "3.0.0"
4
3
  end
@@ -0,0 +1,10 @@
1
+ require "spec_helper"
2
+
3
+ describe AltStruct::Behavior do
4
+ let(:pairs) { { a: 1, b: 2, c: 3} }
5
+ let(:astruct) { ExampleAltStruct.new(pairs) }
6
+
7
+ it "reponds to each key as a method" do
8
+ expect(astruct).to have_attributes(pairs)
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe AltStruct::VERSION do
4
+ it "should be a string" do
5
+ expect(AltStruct::VERSION).to be_kind_of(String)
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe AltStruct do
4
+ let(:astruct) { described_class.new }
5
+
6
+ it "should behave like AltStruct::Behavior" do
7
+ expect(astruct).to respond_to(*AltStruct::Behavior.instance_methods)
8
+ end
9
+
10
+ it "should have aliases for standard methods" do
11
+ expect(astruct).to respond_to(:__object_id__)
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ require "codeclimate-test-reporter"
2
+ require "pry"
3
+ require "rspec"
4
+ require "astruct"
5
+
6
+ RSpec.configure do |let|
7
+ let.before("suite") do
8
+ CodeClimate::TestReporter.start
9
+ end
10
+
11
+ # Exit the spec after the first failure
12
+ let.fail_fast = true
13
+
14
+ # Only run a specific file, using the ENV variable
15
+ # Example: FILE=spec/blankgem/version_spec.rb bundle exec rake spec
16
+ let.pattern = ENV["FILE"]
17
+
18
+ # Show the slowest examples in the suite
19
+ let.profile_examples = true
20
+
21
+ # Colorize the output
22
+ let.color = true
23
+
24
+ # Output as a document string
25
+ let.default_formatter = "doc"
26
+ end
27
+
28
+ class ExampleAltStruct
29
+ include AltStruct::Behavior
30
+ end
@@ -1,10 +1,10 @@
1
- require 'helper'
2
- require 'minitest/autorun'
1
+ require "test_helper"
2
+ require "minitest/autorun"
3
3
 
4
4
  class TestAltStruct < MiniTest::Unit::TestCase
5
5
  def setup
6
6
  @empty = AltStruct.new
7
- @example = AltStruct.new name: "Kurtis", age: 24
7
+ @example = AltStruct.new(name: "Kurtis", age: 24)
8
8
  end
9
9
 
10
10
  def test_equality_with_two_empty_astructs
@@ -39,9 +39,9 @@ class TestAltStruct < MiniTest::Unit::TestCase
39
39
  end
40
40
 
41
41
  def test_inspect_with_sub_struct_duplicate
42
- @empty.struct2 = AltStruct.new
43
- @empty.struct2.struct3 = @empty
44
- expected = '#<AltStruct struct2=#<AltStruct struct3=#<AltStruct ...>>>'
42
+ @empty.substruct = AltStruct.new
43
+ @empty.substruct.subsubstruct = @empty
44
+ expected = "#<AltStruct substruct=#<AltStruct subsubstruct=#<AltStruct ...>>>"
45
45
  actual = @empty.inspect
46
46
  assert_equal expected, actual
47
47
  end
@@ -50,7 +50,7 @@ class TestAltStruct < MiniTest::Unit::TestCase
50
50
  @example.friends = AltStruct.new name: "Jason", age: 24
51
51
  @example.friends.friends = AltStruct.new name: "John", age: 15
52
52
  @example.friends.friends.friends = AltStruct.new name: "Ally", age: 32
53
- expected = '#<AltStruct name="Kurtis", age=24, friends=#<AltStruct name="Jason", age=24, friends=#<AltStruct name="John", age=15, friends=#<AltStruct name="Ally", age=32>>>>'
53
+ expected = "#<AltStruct name=\"Kurtis\", age=24, friends=#<AltStruct name=\"Jason\", age=24, friends=#<AltStruct name=\"John\", age=15, friends=#<AltStruct name=\"Ally\", age=32>>>>"
54
54
  actual = @example.inspect
55
55
  assert_equal expected, actual
56
56
  end
@@ -58,14 +58,14 @@ class TestAltStruct < MiniTest::Unit::TestCase
58
58
  def test_inspect_with_twice_inspected_struct
59
59
  @example.inspect
60
60
  @example.inspect
61
- expected = '#<AltStruct name="Kurtis", age=24>'
61
+ expected = "#<AltStruct name=\"Kurtis\", age=24>"
62
62
  actual = @example.inspect
63
63
  assert_equal expected, actual
64
64
  end
65
65
 
66
66
  def test_inspect_with_empty_sub_struct
67
67
  @empty.struct2 = AltStruct.new
68
- expected = '#<AltStruct struct2=#<AltStruct>>'
68
+ expected = "#<AltStruct struct2=#<AltStruct>>"
69
69
  actual = @empty.inspect
70
70
  assert_equal expected, actual
71
71
  end
@@ -91,7 +91,7 @@ class TestAltStruct < MiniTest::Unit::TestCase
91
91
  # @example.freeze
92
92
  # def @example.frozen?; nil end
93
93
  # @example.freeze
94
- # message = '[ruby-core:22559]'
94
+ # message = "[ruby-core:22559]"
95
95
  # assert_raises(RuntimeError, message) { @example.name = "Jazzy" }
96
96
  # # assert_raises(TypeError, message) { @example.name = "Jazzy" }
97
97
  # end
@@ -105,13 +105,13 @@ class TestAltStruct < MiniTest::Unit::TestCase
105
105
  end
106
106
 
107
107
  def test_delete_field_removes_getter_method
108
- bug = '[ruby-core:33010]'
108
+ bug = "[ruby-core:33010]"
109
109
  @example.delete_field :name
110
110
  refute_respond_to @example, :name, bug
111
111
  end
112
112
 
113
113
  def test_delete_field_removes_setter_method
114
- bug = '[ruby-core:33010]'
114
+ bug = "[ruby-core:33010]"
115
115
  @example.delete_field :name
116
116
  refute_respond_to @example, :name=, bug
117
117
  end
@@ -130,7 +130,7 @@ class TestAltStruct < MiniTest::Unit::TestCase
130
130
  end
131
131
 
132
132
  def test_method_missing_handles_square_bracket_equals
133
- assert_raises(ArgumentError) { @empty[:foo] = :bar }
133
+ assert_raises(NoMethodError) { @empty[:foo] = :bar }
134
134
  end
135
135
 
136
136
  def test_method_missing_handles_square_brackets