mixins 0.1.0.pre.20250527171116

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,266 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'mixins'
6
+
7
+
8
+ RSpec.describe( Mixins::DataUtilities ) do
9
+
10
+ it "doesn't try to dup immediate objects" do
11
+ expect( Mixins::DataUtilities.deep_copy( nil ) ).to be( nil )
12
+ expect( Mixins::DataUtilities.deep_copy( 112 ) ).to be( 112 )
13
+ expect( Mixins::DataUtilities.deep_copy( true ) ).to be( true )
14
+ expect( Mixins::DataUtilities.deep_copy( false ) ).to be( false )
15
+ expect( Mixins::DataUtilities.deep_copy( :a_symbol ) ).to be( :a_symbol )
16
+ end
17
+
18
+
19
+ it "doesn't try to dup modules/classes" do
20
+ klass = Class.new
21
+ expect( Mixins::DataUtilities.deep_copy( klass ) ).to be( klass )
22
+ end
23
+
24
+
25
+ it "doesn't try to dup IOs" do
26
+ data = [ $stdin ]
27
+ expect( Mixins::DataUtilities.deep_copy( data[0] ) ).to be( $stdin )
28
+ end
29
+
30
+
31
+ it "doesn't try to dup Tempfiles" do
32
+ data = Tempfile.new( 'ravn_deepcopy.XXXXX' )
33
+ expect( Mixins::DataUtilities.deep_copy( data ) ).to be( data )
34
+ end
35
+
36
+
37
+ it "makes distinct copies of arrays and their members" do
38
+ original = [ 'foom', Set.new([ 1,2 ]), :a_symbol ]
39
+
40
+ copy = Mixins::DataUtilities.deep_copy( original )
41
+
42
+ expect( copy ).to eq( original )
43
+ expect( copy ).to_not be( original )
44
+ expect( copy[0] ).to eq( original[0] )
45
+ expect( copy[0] ).to_not be( original[0] )
46
+ expect( copy[1] ).to eq( original[1] )
47
+ expect( copy[1] ).to_not be( original[1] )
48
+ expect( copy[2] ).to eq( original[2] )
49
+ expect( copy[2] ).to be( original[2] ) # Immediate
50
+ end
51
+
52
+
53
+ it "makes recursive copies of deeply-nested Arrays" do
54
+ original = [ 1, [ 2, 3, [4], 5], 6, [7, [8, 9], 0] ]
55
+
56
+ copy = Mixins::DataUtilities.deep_copy( original )
57
+
58
+ expect( copy ).to eq( original )
59
+ expect( copy ).to_not be( original )
60
+ expect( copy[1] ).to_not be( original[1] )
61
+ expect( copy[1][2] ).to_not be( original[1][2] )
62
+ expect( copy[3] ).to_not be( original[3] )
63
+ expect( copy[3][1] ).to_not be( original[3][1] )
64
+ end
65
+
66
+
67
+ it "makes distinct copies of Hashes and their members" do
68
+ original = {
69
+ :a => 1,
70
+ 'b' => 2,
71
+ 3 => 'c',
72
+ }
73
+
74
+ copy = Mixins::DataUtilities.deep_copy( original )
75
+
76
+ expect( copy ).to eq( original )
77
+ expect( copy ).to_not be( original )
78
+ expect( copy[:a] ).to eq( 1 )
79
+ expect( copy.key( 2 ) ).to eq( 'b' )
80
+ expect( copy.key( 2 ) ).to_not be( original.key(2) )
81
+ expect( copy[3] ).to eq( 'c' )
82
+ expect( copy[3] ).to_not be( original[3] )
83
+ end
84
+
85
+
86
+ it "makes distinct copies of deeply-nested Hashes" do
87
+ original = {
88
+ :a => {
89
+ :b => {
90
+ :c => 'd',
91
+ :e => 'f',
92
+ },
93
+ :g => 'h',
94
+ },
95
+ :i => 'j',
96
+ }
97
+
98
+ copy = Mixins::DataUtilities.deep_copy( original )
99
+
100
+ expect( copy ).to eq( original )
101
+ expect( copy[:a][:b][:c] ).to eq( 'd' )
102
+ expect( copy[:a][:b][:c] ).to_not be( original[:a][:b][:c] )
103
+ expect( copy[:a][:b][:e] ).to eq( 'f' )
104
+ expect( copy[:a][:b][:e] ).to_not be( original[:a][:b][:e] )
105
+ expect( copy[:a][:g] ).to eq( 'h' )
106
+ expect( copy[:a][:g] ).to_not be( original[:a][:g] )
107
+ expect( copy[:i] ).to eq( 'j' )
108
+ expect( copy[:i] ).to_not be( original[:i] )
109
+ end
110
+
111
+
112
+ it "copies the default proc of copied Hashes" do
113
+ original = Hash.new {|h,k| h[ k ] = Set.new }
114
+
115
+ copy = Mixins::DataUtilities.deep_copy( original )
116
+
117
+ expect( copy.default_proc ).to eq( original.default_proc )
118
+ end
119
+
120
+
121
+ it "preserves frozen-ness of copied objects" do
122
+ original = Object.new
123
+ original.freeze
124
+
125
+ copy = Mixins::DataUtilities.deep_copy( original )
126
+
127
+ expect( copy ).to_not be( original )
128
+ expect( copy ).to be_frozen()
129
+ end
130
+
131
+
132
+ it "can recursively transform Hash keys into Symbols" do
133
+ original = {
134
+ 'id' => 'a8fd4d6f-5c0f-45b2-8732-8b8a90b595de',
135
+ 'time' => Time.now.to_f,
136
+ 'type' => 'sparrow.order.turning',
137
+ 'data' => {
138
+ 'response_type' => 'receipt',
139
+ 'response' => 1
140
+ }
141
+ }
142
+
143
+ result = Mixins::DataUtilities.symbolify_keys( original )
144
+
145
+ expect( result.keys ).to all( be_a Symbol )
146
+ expect( result[:data].keys ).to all( be_a Symbol )
147
+ end
148
+
149
+
150
+ it "doesn't try to turn keys other than Strings into Symbols" do
151
+ original = {
152
+ 'foo' => {
153
+ 'bar' => 3,
154
+ 3 => 'bar',
155
+ }
156
+ }
157
+
158
+ result = Mixins::DataUtilities.symbolify_keys( original )
159
+
160
+ expect( result[:foo].keys ).to contain_exactly( :bar, 3 )
161
+ end
162
+
163
+
164
+ it "doesn't try to turn String keys that aren't identifiers into Symbols" do
165
+ original = {
166
+ 'foo' => {
167
+ 'an arbitrary string' => 3,
168
+ '$punctuation_string' => 8,
169
+ '_underscore_string' => 4,
170
+ }
171
+ }
172
+
173
+ result = Mixins::DataUtilities.symbolify_keys( original )
174
+
175
+ expect( result[:foo].keys ).
176
+ to contain_exactly( 'an arbitrary string', '$punctuation_string', :_underscore_string )
177
+ end
178
+
179
+
180
+ it "recurses into Arrays when transforming Hash keys into Symbols" do
181
+ original = {
182
+ 'type' => 'Vic Checkin',
183
+ 'text' => 'Vehicles check in',
184
+ 'ontological_suffix' => 'conversation.headcount',
185
+ 'components' => {
186
+ 'recipients' => {
187
+ 'to' => 'Vic Commanders',
188
+ },
189
+ 'responses' => {
190
+ 'responses_from' => 'Vic Commanders',
191
+ 'send_label' => 'Send UP',
192
+ 'steps' => [
193
+ { 'type' => 'integer', 'label' => 'PAX' },
194
+ { 'type' => 'select', 'label' => 'Ready', 'values' => ['yes', 'no'] },
195
+ ],
196
+ }
197
+ }
198
+ }
199
+
200
+ result = Mixins::DataUtilities.symbolify_keys( original )
201
+
202
+ expect( result.keys ).to all( be_a Symbol )
203
+ expect( result.dig(:components, :responses, :steps) ).to all( include(:type, :label) )
204
+ end
205
+
206
+
207
+ it "can recursively transform Hash keys into Strings" do
208
+ original = {
209
+ :id => '4cc37025-fb34-47ef-b762-6abac23e0792',
210
+ :time => Time.now.to_f,
211
+ :type => 'acme.widget.model1',
212
+ 1 => 'something',
213
+ :data => {
214
+ :response_type => 'receipt',
215
+ :response_id => 1
216
+ }
217
+ }
218
+
219
+ result = Mixins::DataUtilities.stringify_keys( original )
220
+
221
+ expect( result.keys ).to contain_exactly( 'id', 'time', 'type', 1, 'data' )
222
+ expect( result['data'].keys ).to contain_exactly( 'response_type', 'response_id' )
223
+ end
224
+
225
+
226
+ it "doesn't stringify non-Symbols when stringifying Hash keys" do
227
+ original = {
228
+ :foo => "bar",
229
+ 18 => "something",
230
+ }
231
+
232
+ result = Mixins::DataUtilities.stringify_keys( original )
233
+
234
+ expect( result.keys ).to contain_exactly( 'foo', 18 )
235
+ end
236
+
237
+
238
+ it "recurses into Arrays when transforming Hash keys into Strings" do
239
+ original = {
240
+ type: 'Teddy Bear',
241
+ text: "What's your name?",
242
+ sort: 'toy.plush.animal',
243
+ components: {
244
+ route: {
245
+ to: 'receiving@acme.com',
246
+ },
247
+ responses: {
248
+ responses_from: 'warehouse1@acme.com',
249
+ send_label: 'BZ5556',
250
+ steps: [
251
+ { type: 'integer', label: 'QC' },
252
+ { type: 'select', label: 'conv1', values: ['yes', 'no'] },
253
+ ],
254
+ }
255
+ }
256
+ }
257
+
258
+ result = Mixins::DataUtilities.stringify_keys( original )
259
+
260
+ expect( result.keys ).to all( be_a String )
261
+ expect( result.dig('components', 'responses', 'steps') ).to all( include('type', 'label') )
262
+ end
263
+
264
+ end
265
+
266
+
@@ -0,0 +1,86 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'mixins'
6
+
7
+
8
+ RSpec.describe( Mixins::Datadir ) do
9
+
10
+ let( :zebra_gemspec ) do
11
+ Gem::Specification.new do |s|
12
+ s.name = "zebra"
13
+ s.version = Gem::Version.new("0.2.1")
14
+ s.installed_by_version = Gem::Version.new("0")
15
+ s.authors = ["Zaphod Beeblebrox"]
16
+ s.date = Time.utc(2024, 6, 12)
17
+ s.description = "So many zebras."
18
+ s.email = ["zaph@example.com"]
19
+ s.files = ["zebras.rb"]
20
+ s.homepage = "https://github.com/zaph/zebras"
21
+ s.licenses = ["Ruby", "BSD-2-Clause"]
22
+ s.metadata = {
23
+ "homepage_uri"=>"https://github.com/zaph/zebras",
24
+ "source_code_uri"=>"https://github.com/zaph/zebras"
25
+ }
26
+ s.require_paths = ["lib"]
27
+ s.required_ruby_version = Gem::Requirement.new([">= 2.5.0"])
28
+ s.rubygems_version = "3.5.11"
29
+ s.specification_version = 4
30
+ s.summary = "All the zebras."
31
+ end
32
+ end
33
+ let( :zebra_datadir ) { '/path/to/installed/gem/datadir' }
34
+ let( :loaded_gemspecs ) { { 'zebra' => zebra_gemspec } }
35
+
36
+
37
+ before( :each ) do
38
+ @original_env = ENV.to_h
39
+ end
40
+ after( :each ) do
41
+ ENV.replace( @original_env )
42
+ end
43
+
44
+
45
+ it "uses the currently-loaded gem's data directory if there is one" do
46
+ expect( Gem ).to receive( :loaded_specs ).
47
+ and_return( loaded_gemspecs ).at_least( :once )
48
+ expect( zebra_gemspec ).to receive( :datadir ).
49
+ and_return( zebra_datadir ).at_least( :once )
50
+ expect( File ).to receive( :exist? ).with( zebra_datadir ).and_return( true )
51
+
52
+ target_class = Class.new do
53
+ def self::name; 'Zebra'; end
54
+ end
55
+ target_class.extend( described_class )
56
+
57
+ expect( target_class.data_dir ).to eq( Pathname(zebra_datadir) )
58
+ end
59
+
60
+
61
+ it "uses the directory at ../../data/<gemname> if no gem is loaded" do
62
+ target_class = Class.new do
63
+ def self::name; 'Panda'; end
64
+ end
65
+ target_class.extend( described_class )
66
+
67
+ local_datadir = Pathname( __FILE__ ).parent.parent.parent / 'data' / 'panda'
68
+
69
+ expect( target_class.data_dir ).to eq( local_datadir )
70
+ end
71
+
72
+
73
+ it "allows the data dir to be overridden using an environment variable" do
74
+ ocelot_data = '/path/to/ocelot/data'
75
+ ENV['OCELOT_DATADIR'] = ocelot_data
76
+
77
+ target_class = Class.new do
78
+ def self::name; 'Ocelot'; end
79
+ end
80
+ target_class.extend( described_class )
81
+
82
+ expect( target_class.data_dir ).to eq( Pathname(ocelot_data) )
83
+ end
84
+
85
+ end
86
+
@@ -0,0 +1,186 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'mixins'
6
+
7
+
8
+ RSpec.describe( Mixins::Delegation ) do
9
+
10
+ let( :testclass ) do
11
+ Class.new do
12
+ extend Mixins::Delegation
13
+
14
+ @data_dir = nil
15
+ class << self
16
+ attr_accessor :data_dir
17
+ end
18
+
19
+ def initialize( obj=nil )
20
+ @obj = obj
21
+ end
22
+
23
+ def demand_loaded_object
24
+ return @load_on_demand ||= @obj
25
+ end
26
+ end
27
+ end
28
+
29
+ let( :subobj ) { double( "delegate" ) }
30
+ let( :obj ) { testclass.new(subobj) }
31
+
32
+
33
+ describe "method delegation" do
34
+
35
+ it "can be used to set up delegation through a method" do
36
+ testclass.def_method_delegators( :demand_loaded_object, :delegated_method )
37
+
38
+ expect( subobj ).to receive( :delegated_method )
39
+
40
+ obj.delegated_method
41
+ end
42
+
43
+
44
+ it "passes any arguments through to the delegate object's method" do
45
+ testclass.def_method_delegators( :demand_loaded_object, :delegated_method )
46
+
47
+ expect( subobj ).to receive( :delegated_method ).with( :arg1, :arg2 )
48
+
49
+ obj.delegated_method( :arg1, :arg2 )
50
+ end
51
+
52
+
53
+ it "allows delegation to the delegate object's method with a block" do
54
+ testclass.def_method_delegators :demand_loaded_object, :delegated_method
55
+
56
+ expect( subobj ).to receive( :delegated_method ).with( :arg1 ).
57
+ and_yield( :the_block_argument )
58
+
59
+ blockarg = nil
60
+ obj.delegated_method( :arg1 ) {|arg| blockarg = arg }
61
+
62
+ expect( blockarg ).to eq( :the_block_argument )
63
+ end
64
+
65
+
66
+ it "reports errors from its caller's perspective", :ruby_1_8_only => true do
67
+ testclass.def_method_delegators( :nonexistant_method, :erroring_delegated_method )
68
+
69
+ begin
70
+ obj.erroring_delegated_method
71
+ rescue NoMethodError => err
72
+ expect( err.message ).to match( /nonexistant_method/ )
73
+ expect( err.backtrace.first ).to match( /#{__FILE__}/ )
74
+ rescue ::Exception => err
75
+ fail "Expected a NoMethodError, but got a %p (%s)" % [ err.class, err.message ]
76
+ else
77
+ fail "Expected a NoMethodError, but no exception was raised."
78
+ end
79
+ end
80
+
81
+
82
+ it "delegates setters correctly" do
83
+ testclass.def_method_delegators :demand_loaded_object, :delegated_setter=
84
+
85
+ expect( subobj ).to receive( :delegated_setter= ).with( 1 )
86
+
87
+ obj.delegated_setter = 1
88
+
89
+ expect( subobj ).to receive( :delegated_setter= ).with( [1, 2] )
90
+
91
+ obj.delegated_setter = 1, 2
92
+ end
93
+
94
+ end
95
+
96
+
97
+ describe "instance variable delegation (ala Forwardable)" do
98
+
99
+ let( :testclass ) do
100
+ Class.new do
101
+ extend Mixins::Delegation
102
+
103
+ def initialize( obj )
104
+ @obj = obj
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ it "can be used to set up delegation through a method" do
111
+ testclass.def_ivar_delegators( :@obj, :delegated_method )
112
+
113
+ expect( subobj ).to receive( :delegated_method )
114
+
115
+ obj.delegated_method
116
+ end
117
+
118
+
119
+ it "passes any arguments through to the delegate's method" do
120
+ testclass.def_ivar_delegators( :@obj, :delegated_method )
121
+
122
+ expect( subobj ).to receive( :delegated_method ).with( :arg1, :arg2 )
123
+
124
+ obj.delegated_method( :arg1, :arg2 )
125
+ end
126
+
127
+
128
+ it "allows delegation to the delegate's method with a block" do
129
+ testclass.def_ivar_delegators( :@obj, :delegated_method )
130
+
131
+ expect( subobj ).to receive( :delegated_method ).with( :arg1 ).
132
+ and_yield( :the_block_argument )
133
+
134
+ blockarg = nil
135
+ obj.delegated_method( :arg1 ) {|arg| blockarg = arg }
136
+
137
+ expect( blockarg ).to eq( :the_block_argument )
138
+ end
139
+
140
+
141
+ it "reports errors from its caller's perspective", :ruby_1_8_only => true do
142
+ testclass.def_ivar_delegators( :@glong, :erroring_delegated_method )
143
+
144
+ begin
145
+ obj.erroring_delegated_method
146
+ rescue NoMethodError => err
147
+ expect( err.message ).to match( /['`]erroring_delegated_method' for nil/ )
148
+ expect( err.backtrace.first ).to match( /#{__FILE__}/ )
149
+ rescue ::Exception => err
150
+ fail "Expected a NoMethodError, but got a %p (%s)" % [ err.class, err.message ]
151
+ else
152
+ fail "Expected a NoMethodError, but no exception was raised."
153
+ end
154
+ end
155
+
156
+
157
+ it "delegates setters correctly" do
158
+ testclass.def_ivar_delegators( :@obj, :delegated_setter= )
159
+
160
+ expect( subobj ).to receive( :delegated_setter= ).with( 1 )
161
+
162
+ obj.delegated_setter = 1
163
+
164
+ expect( subobj ).to receive( :delegated_setter= ).with( [1, 2] )
165
+
166
+ obj.delegated_setter = 1, 2
167
+ end
168
+
169
+ end
170
+
171
+
172
+ describe "class-method delegation" do
173
+
174
+ it "can be used to set up delegation through a method" do
175
+ testclass.def_class_delegators :data_dir
176
+
177
+ testclass.data_dir = '/path/to/the/data'
178
+ obj = testclass.new
179
+
180
+ expect( obj.data_dir ).to eq( '/path/to/the/data' )
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
@@ -0,0 +1,86 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'mixins'
6
+
7
+
8
+ RSpec.describe( Mixins::Hooks ) do
9
+
10
+ let( :extended_object ) do
11
+ obj = Object.new
12
+ obj.extend( described_class )
13
+ return obj
14
+ end
15
+
16
+
17
+ it "allows a set of hooks to be declared" do
18
+ extended_object.define_hook( :after_fork )
19
+
20
+ hook1_called = false
21
+ hook2_called = false
22
+
23
+ extended_object.after_fork do
24
+ hook1_called = true
25
+ end
26
+ extended_object.after_fork do
27
+ hook2_called = true
28
+ end
29
+
30
+ expect {
31
+ extended_object.call_after_fork_hook
32
+ }.to change { hook1_called }.to( true ).and \
33
+ change { hook2_called }.to( true ).and \
34
+ change { extended_object.after_fork_callbacks_run? }.to( true )
35
+ end
36
+
37
+
38
+ it "ensures declared hooks are run at least once" do
39
+ extended_object.define_hook( :after_fork )
40
+
41
+ hook1_called = false
42
+ hook2_called = false
43
+
44
+ extended_object.after_fork do
45
+ hook1_called = true
46
+ end
47
+ extended_object.call_after_fork_hook
48
+ extended_object.after_fork do
49
+ hook2_called = true
50
+ end
51
+
52
+ expect( hook1_called ).to be_truthy
53
+ expect( hook2_called ).to be_truthy
54
+ end
55
+
56
+
57
+ it "doesn't re-register a hook callback that already exists" do
58
+ extended_object.define_hook( :before_fork )
59
+
60
+ callback = Proc.new {}
61
+ extended_object.before_fork( &callback )
62
+ extended_object.before_fork( &callback )
63
+
64
+ expect( extended_object.before_fork_callbacks.length ).to eq( 1 )
65
+ end
66
+
67
+
68
+ it "can declare a hook that passes arguments to its callbacks" do
69
+ extended_object.define_hook( :on_event )
70
+
71
+ callback_args = []
72
+ extended_object.on_event do |ev, *args|
73
+ callback_args << [ev, args]
74
+ end
75
+
76
+ extended_object.call_on_event_hook( :slip, 4 )
77
+ extended_object.call_on_event_hook( :traverse, 16 )
78
+
79
+ expect( callback_args ).to contain_exactly(
80
+ [:slip, [4]],
81
+ [:traverse, [16]]
82
+ )
83
+ end
84
+
85
+ end
86
+
@@ -0,0 +1,41 @@
1
+ # -*- ruby -*-
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'mixins'
6
+
7
+
8
+ RSpec.describe( Mixins::Inspection ) do
9
+
10
+ it "handles empty details" do
11
+ oclass = Class.new do
12
+ def initialize( serial )
13
+ @serial = serial
14
+ end
15
+ end
16
+ oclass.include( described_class )
17
+ instance = oclass.new( 11 )
18
+
19
+ expect( instance.inspect ).to match( /#<#{oclass.inspect}:#\h+/ )
20
+ end
21
+
22
+
23
+ it "allows the inspection contents to be overridden" do
24
+ oclass = Class.new do
25
+ def initialize( serial )
26
+ @serial = serial
27
+ end
28
+ attr_reader :serial
29
+ def inspect_details
30
+ return "serial: %d" % [ self.serial ]
31
+ end
32
+ end
33
+ oclass.include( described_class )
34
+
35
+ instance = oclass.new( 13 )
36
+
37
+ expect( instance.inspect ).to match( /#<\S+ serial: 13>/ )
38
+ end
39
+
40
+ end
41
+