skn_utils 2.0.6 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,382 @@
1
+ ##
2
+ # <project.root>/lib/skn_utils/nested_result.rb
3
+ #
4
+ # SknUtils::NestedResult Value Container/Class for Ruby with Indifferent Hash and/or Dot.notation access
5
+ #
6
+ # Description:
7
+ #
8
+ # Creates an Object with attribute methods for dot.notation and hash.notation access
9
+ # for each hash input key/value pair.
10
+ #
11
+ # If the key's value is an hash itself, it will become an NestedResult Object.
12
+ # if the key's value is an Array of Hashes, each hash element of the Array will
13
+ # become an Object; non-hash object are left as-is
14
+ # if the key's value is an Array of Arrays-of- Hash/Object, each hash element of each Array will
15
+ # become an Object; non-hash object are left as-is. This array of array of arrays
16
+ # goes on to the end.
17
+ #
18
+ # Transforms entire input hash contents into dot.notation and hash.notation accessible key/value pairs.
19
+ # - hash
20
+ # - array of hashes
21
+ # - non hash element values are not modified,
22
+ # whether in an array or the basic value in a key/value pair
23
+ #
24
+ # The ability of the resulting Object to be YAML/Psych'ed, or Marshaled(dump/load) is preserved
25
+ #
26
+ ##
27
+ # Transforms entire input hash contents into dot.notation accessible object
28
+ # - hash
29
+ # - array of hashes
30
+ # - non hash element values are not modified, whether in an array or the basic value in a key/value pair
31
+ #
32
+ ##
33
+ # This module provides
34
+ #
35
+ # Simple Initialization Pattern
36
+ # person = SknUtils::NestedResult.new( {name: "Bob", title: {day: 'Analyst', night: 'Fireman'}} )
37
+ #
38
+ # Serializers:
39
+ # person.to_hash
40
+ # => {name: 'Bob', title: {day: 'Analyst', night: 'Fireman'}}
41
+ # person.to_json
42
+ # => "{\"name\":\"Bob\", \"title\":{\"day\":\"Analyst\", \"night\":\"Fireman\"}}"
43
+ #
44
+ # Dynamic addition of new key/values after initialization
45
+ # person.address = 'Fort Wayne Indiana'
46
+ # person.address
47
+ # => 'Fort Wayne Indiana'
48
+ #
49
+ # dot.notation feature for all instance variables
50
+ # person.title.day
51
+ # => "Analyst"
52
+ # person.name = "James"
53
+ # => "James"
54
+ #
55
+ # InDifferent String/Symbol hash[notation] feature for all instance variables
56
+ # person['title']['day']
57
+ # => "Analyst"
58
+ # person['name'] = "James"
59
+ # => "James"
60
+ # person[:name]
61
+ # => "James"
62
+ # person[:name] = "Bob"
63
+ # => "Bob"
64
+ #
65
+ # Supports <attr>? predicate method patterns, and delete_field(:attr) method
66
+ # example:
67
+ # person.title.night?
68
+ # => true true or false, like obj.name.present?
69
+ # person.delete_field(:name) only first/root level attributes can be deleted
70
+ # => 'Bob' returns last value of deleted key
71
+ # person.name_not_found
72
+ # => NoMethodFound raises exception if key is not found
73
+ #
74
+ # Exporting hash from any key starting point
75
+ # person.hash_from(:name)
76
+ # => {name: 'Bob'} the entire hash tree from that starting point
77
+ ##
78
+ # Advanced Methods
79
+ # #to_hash - returns copy of input hash
80
+ # #to_json(*args) - converts input hash into JSON
81
+ # #keys - returns the first-level keys of input hash
82
+ # #delete_field(attr_sym) - removes attribute/key and returns it's former value
83
+ # #hash_from(starting_attr_sym) - (Protected Method) returns remaining hash starting from key provided
84
+ #
85
+ ##
86
+ # Known Issues
87
+ # - Fixnum keys work as keys with the exception of #respond_to?() which does not support them
88
+ # - Entries with Fixnums or object-instance keys are accessible only via #[]=(), #[] Hash.notation
89
+ # methods and not the dot.notation feature
90
+ #
91
+ ###################################################################################################
92
+
93
+ module SknUtils
94
+ class NestedResult
95
+
96
+ def initialize(params={})
97
+ @container = {}
98
+ initialize_from_hash(params)
99
+ end
100
+
101
+ def [](attr)
102
+ container[key_as_sym(attr)]
103
+ end
104
+
105
+ #Feature: if a new attribute is added, on first read method_missing will create getters/setters
106
+ def []=(attr, value)
107
+ container.store(key_as_sym(attr), value)
108
+ end
109
+
110
+ def delete_field(name) # protect public methods
111
+ sym = key_as_sym(name)
112
+ unless !sym.is_a?(Symbol) || self.class.method_defined?(sym)
113
+ singleton_class.send(:remove_method, "#{sym.to_s}=".to_sym, sym) rescue nil
114
+ container.delete(sym)
115
+ end
116
+ end
117
+
118
+ #
119
+ # Exporters
120
+ #
121
+ def to_hash
122
+ attributes
123
+ end
124
+
125
+ alias_method :to_h, :to_hash
126
+
127
+ def to_json(*args)
128
+ attributes.to_json(*args)
129
+ end
130
+
131
+ #
132
+ # Returns a string containing a detailed summary of the keys and values.
133
+ #
134
+ InspectKey = :__inspect_key__ # :nodoc:
135
+ def inspect
136
+ package = to_hash
137
+ str = "#<#{self.class}"
138
+
139
+ ids = (Thread.current[InspectKey] ||= [])
140
+ if ids.include?(object_id)
141
+ return str << ' ...>'
142
+ end
143
+
144
+ ids << object_id
145
+ begin
146
+ first = true
147
+ for k,v in package
148
+ str << "," unless first
149
+ first = false
150
+ str << " #{k}=#{v.inspect}"
151
+ end
152
+ return str << '>'
153
+ ensure
154
+ ids.pop
155
+ end
156
+ end
157
+
158
+ alias_method :to_s, :inspect
159
+
160
+
161
+ ##
162
+ # Ruby basic Class methods
163
+ #
164
+ def ==(other)
165
+ return false unless other.is_a?(NestedResult)
166
+ to_hash.eql?(other.to_hash)
167
+ end
168
+ alias_method :===, :==
169
+
170
+ def eql?(other)
171
+ return false unless other.is_a?(NestedResult)
172
+ to_hash.eql?(other.to_hash)
173
+ end
174
+
175
+ def hash
176
+ to_hash.hash
177
+ end
178
+
179
+ # Feature: returns keys from root input Hash
180
+ def keys
181
+ container.keys
182
+ end
183
+
184
+ ##
185
+ # YAML/Psych load support, chance to re-initialize value methods
186
+ #
187
+ # Use our unwrapped/original input Hash when yaml'ing
188
+ def encode_with(coder)
189
+ coder['container'] = self.to_h
190
+ end
191
+
192
+ # Use our hash from above to fully re-initialize this instance
193
+ def init_with(coder)
194
+ case coder.tag
195
+ when '!ruby/object:SknUtils::NestedResult'
196
+ initialize_from_hash( coder.map['container'] )
197
+ end
198
+ end
199
+
200
+ protected
201
+
202
+ ##
203
+ # Marshal.load()/.dump() support, chance to re-initialize value methods
204
+ #
205
+ def marshal_dump
206
+ to_hash
207
+ end
208
+
209
+ # Using the String from above create and return an instance of this class
210
+ def marshal_load(hash)
211
+ initialize_from_hash(hash)
212
+ end
213
+
214
+ def respond_to_missing?(method, incl_private=false)
215
+ method_nsym = method.is_a?(Symbol) ? method.to_s[0..-2].to_sym : method
216
+ container[key_as_sym(method)] || container[method_nsym] || super
217
+ end
218
+
219
+ private
220
+
221
+ # Feature: attribute must exist and have a non-blank value to cause this method to return true
222
+ def attribute?(attr)
223
+ return false unless container.key?(key_as_sym(attr))
224
+ ![ "", " ", nil, [],[""], [" "], NestedResult.new({}), [[]]].any? {|a| a == container[key_as_sym(attr)] }
225
+ end
226
+
227
+ # Feature: returns a hash of all attributes and their current values
228
+ def attributes
229
+ hash_from(container)
230
+ end
231
+
232
+ def container
233
+ @container ||= {}
234
+ end
235
+
236
+ # returns hash from any root key starting point: object.root_key
237
+ # - protected to reasonably ensure key is a symbol
238
+ def hash_from(sym)
239
+ starting_sym = key_as_sym(sym)
240
+ bundle = starting_sym == container ? container : { starting_sym => container[starting_sym] }
241
+ bundle.keys.each_with_object({}) do |attr,collector|
242
+ value = bundle[attr]
243
+ case value
244
+ when Array
245
+ value = value.map {|ele| array_to_hash(ele) }
246
+ when NestedResult
247
+ value = value.to_hash
248
+ end
249
+ collector[attr] = value # new copy
250
+ end
251
+ end
252
+
253
+ # Feature: enables dot.notation and creates matching getter/setters
254
+ def enable_dot_notation(sym)
255
+ name = key_as_sym(sym)
256
+ unless !name.is_a?(Symbol) || singleton_class.method_defined?(name)
257
+ singleton_class.send(:define_method, name) do
258
+ container[name]
259
+ end
260
+
261
+ singleton_class.send(:define_method, "#{name.to_s}=".to_sym) do |x|
262
+ container[name] = x
263
+ end
264
+ end
265
+ name
266
+ end
267
+
268
+ def initialize_from_hash(hash)
269
+ hash.each_pair do |k,v|
270
+ key = key_as_sym(k)
271
+ enable_dot_notation(key)
272
+ case v
273
+ when Array
274
+ value = v.map { |element| translate_value(element) }
275
+ container.store(key, value)
276
+ when Hash
277
+ container.store(key, NestedResult.new(v))
278
+ else
279
+ container.store(key, v)
280
+ end
281
+ end
282
+ end
283
+
284
+ # Feature: unwrap array of array-of-hashes/object
285
+ def array_to_hash(array)
286
+ case array
287
+ when Array
288
+ array.map { |element| array_to_hash(element) }
289
+ when NestedResult
290
+ array.to_hash
291
+ else
292
+ array
293
+ end
294
+ end
295
+
296
+ # Feature: wrap array of array-of-hashes/object
297
+ def translate_value(value)
298
+ case value
299
+ when Array
300
+ value.map { |element| translate_value(element) }
301
+ when Hash
302
+ NestedResult.new(value)
303
+ else
304
+ value
305
+ end
306
+ end
307
+
308
+ def key_as_sym(key)
309
+ case key
310
+ when Symbol
311
+ key
312
+ when String
313
+ key.to_sym
314
+ else
315
+ key # no change, allows Fixnum and Object instances
316
+ end
317
+ end
318
+
319
+ # Feature: post-assign key/value pair, <attr>?? predicate, create getter/setter on first access
320
+ def method_missing(method, *args, &block)
321
+ method_sym = key_as_sym(method)
322
+ method_nsym = method_sym.is_a?(Symbol) ? method.to_s[0..-2].to_sym : method
323
+
324
+
325
+ if method.to_s.end_with?("=") and container[method_nsym].nil? # add new key/value pair, transform value if Hash or Array
326
+ initialize_from_hash({method_nsym => args.first})
327
+
328
+ elsif container.key?(method_sym)
329
+ puts "#{__method__}() method: #{method}"
330
+ enable_dot_notation(method_sym) # Add Reader/Writer one first need
331
+ container[method_sym]
332
+
333
+ elsif method.to_s.end_with?('?') # order of tests is significant,
334
+ attribute?(method_nsym)
335
+
336
+ else
337
+ e = NoMethodError.new "undefined method `#{method}' for #{self.class.name}", method, args
338
+ e.set_backtrace caller(1)
339
+ raise e
340
+
341
+ end
342
+ end # end method_missing: errors from enable_dot..., initialize_hash..., and attribute? are possible
343
+
344
+ end # end class
345
+ end # end module
346
+
347
+
348
+ # YAML.load(str) will trigger #init_with for each type it encounters when loading
349
+ # Psych.dump ==> "--- !ruby/object:SknUtils::NestedResult\ncontainer:\n :one: 1\n :two: two\n"
350
+ #
351
+ #
352
+ # [2] pry(main)> ay = Psych.dump a
353
+ # respond_to_missing?() checking for method: :encode_with existence.
354
+ # => "--- !ruby/object:SknUtils::NestedResult\ncontainer:\n :one: 1\n :two: two\n"
355
+ # [3] pry(main)> az = Psych.load ay
356
+ # respond_to_missing?() checking for method: :init_with existence.
357
+ # respond_to_missing?() checking for method: :yaml_initialize existence.
358
+ # => #<SknUtils::NestedResult:0x007fe410993238 @container={:one=>1, :two=>"two"}>
359
+
360
+
361
+ # YAML RTM? querys
362
+ # [:encode_with, :init_with].include?(method)
363
+
364
+
365
+ # can be accessed just like a hash
366
+ # respond_to_missing?() checking for method: :encode_with existence.
367
+ # respond_to_missing?() checking for method: :encode_with existence.
368
+ # respond_to_missing?() checking for method: :encode_with existence.
369
+ # respond_to_missing?() checking for method: :encode_with existence.
370
+ # respond_to_missing?() checking for method: :encode_with existence.
371
+ # respond_to_missing?() checking for method: :encode_with existence.
372
+ # respond_to_missing?() checking for method: :encode_with existence.
373
+ # respond_to_missing?() checking for method: :encode_with existence.
374
+ # init_with() hooking into Yaml/Psych.load for codes: {:seven=>7, :eight=>"eight"}.
375
+ # init_with() hooking into Yaml/Psych.load for codes: {:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba101740e0 @container={:seven=>7, :eight=>"eight"}>, :seven=>false}.
376
+ # init_with() hooking into Yaml/Psych.load for codes: {:any_key=>#<Tuple:0x007fba101643e8 @first="foo", @second="bar">}.
377
+ # init_with() hooking into Yaml/Psych.load for codes: {:seven=>7, :eight=>"eight"}.
378
+ # init_with() hooking into Yaml/Psych.load for codes: {:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba1014f880 @container={:seven=>7, :eight=>"eight"}>}.
379
+ # init_with() hooking into Yaml/Psych.load for codes: {:nine=>9, :ten=>"ten"}.
380
+ # init_with() hooking into Yaml/Psych.load for codes: {:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba1014cd60 @container={:nine=>9, :ten=>"ten"}>}.
381
+ # init_with() hooking into Yaml/Psych.load for codes: {:one=>"one", :two=>"two", :three=>#<SknUtils::NestedResult:0x007fba10175058 @container={:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba101740e0 @container={:seven=>7, :eight=>"eight"}>, :seven=>false}>, :four=>#<SknUtils::NestedResult:0x007fba101664b8 @container={:any_key=>#<Tuple:0x007fba101643e8 @first="foo", @second="bar">}>, :five=>[4, 5, 6], :six=>[#<SknUtils::NestedResult:0x007fba10154628 @container={:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba1014f880 @container={:seven=>7, :eight=>"eight"}>}>, #<SknUtils::NestedResult:0x007fba1014d738 @container={:four=>4, :five=>5, :six=>#<SknUtils::NestedResult:0x007fba1014cd60 @container={:nine=>9, :ten=>"ten"}>}>, #<Tuple:0x007fba10146d48 @first="another", @second="tuple">], :seven=>#<Tuple:0x007fba10145a60 @first="hello", @second="world">}.
382
+
@@ -0,0 +1,94 @@
1
+ #
2
+ #
3
+ # Ruby Notify like class
4
+ #
5
+ # Ref: https://ozone.wordpress.com/category/programming/metaprogramming/
6
+
7
+
8
+ class NotifierBase
9
+
10
+ def initialize
11
+ @listeners = []
12
+ end
13
+
14
+ def register_listener(l)
15
+ @listeners.push(l) unless @listeners.include?(l)
16
+ end
17
+
18
+ def unregister_listener(l)
19
+ @listeners.delete(l)
20
+ end
21
+
22
+ def self.attribute(*properties)
23
+ properties.each do |prop|
24
+ define_method(prop) {
25
+ instance_variable_get("@#{prop}")
26
+ }
27
+ define_method("#{prop}=") do |value|
28
+ old_value = instance_variable_get("@#{prop}")
29
+ return if (value == old_value)
30
+ @listeners.each { |listener|
31
+ listener.attribute_changed(prop, old_value, value)
32
+ }
33
+ instance_variable_set("@#{prop}", value)
34
+ end
35
+ end # loop on properties
36
+ end # end of attribute method
37
+
38
+ end # end of NotifierBase class
39
+
40
+
41
+ # Create a bean from that base
42
+ class TestBean < NotifierBase
43
+ attribute :name, :firstname
44
+ end
45
+
46
+ class LoggingPropertyChangeListener
47
+ def attribute_changed(attribute, old_value, new_value)
48
+ print attribute, " changed from ",
49
+ old_value, " to ",
50
+ new_value, "\n"
51
+ end
52
+ end
53
+
54
+ class SimpleBean < NotifierBase
55
+ attribute :name, :firstname
56
+
57
+ def impotent_name=(new_name)
58
+ @name = new_name
59
+ end
60
+ end
61
+
62
+
63
+ test = TestBean.new
64
+ listener = LoggingPropertyChangeListener.new
65
+ test.register_listener(listener)
66
+ test.name = 'James Scott'
67
+ test.firstname = "Scott"
68
+ test.firstname = "James"
69
+ test.unregister_listener(listener)
70
+
71
+
72
+
73
+ test = SimpleBean.new
74
+ listener = LoggingPropertyChangeListener.new
75
+ test.register_listener(listener)
76
+ test.name = 'James Scott'
77
+ test.firstname = 'Scott'
78
+ test.firstname = 'James'
79
+ test.unregister_listener(listener)
80
+
81
+
82
+ # output it generates:
83
+
84
+ # ==> name changed from nil to James Scott
85
+ # ==> firstname changed from nil to Scott
86
+ # ==> firstname changed from Scott to James
87
+
88
+
89
+ #
90
+ # END
91
+ #
92
+
93
+
94
+
@@ -1,9 +1,9 @@
1
1
  # A better way to say it
2
2
  module SknUtils
3
3
  class Version
4
- MAJOR = 2
4
+ MAJOR = 3
5
5
  MINOR = 0
6
- PATCH = 6
6
+ PATCH = 0
7
7
 
8
8
  def self.to_s
9
9
  [MAJOR, MINOR, PATCH].join('.')
data/lib/skn_utils.rb CHANGED
@@ -1,10 +1,5 @@
1
1
  require "skn_utils/version"
2
- require 'skn_utils/attribute_helpers'
3
- require 'skn_utils/nested_result_base'
4
- require 'skn_utils/generic_bean'
5
- require 'skn_utils/page_controls'
6
- require 'skn_utils/result_bean'
7
- require 'skn_utils/value_bean'
2
+ require 'skn_utils/nested_result'
8
3
  require 'skn_utils/null_object'
9
4
  require 'skn_utils/exploring/commander'
10
5
  require 'skn_utils/exploring/action_service'
data/skn_utils.gemspec CHANGED
@@ -9,28 +9,11 @@ Gem::Specification.new do |spec|
9
9
  spec.author = 'James Scott Jr'
10
10
  spec.email = 'skoona@gmail.com'
11
11
  spec.summary = <<EOF
12
- Ruby convenience utilities, the first being a ResultBean.
13
-
14
-
15
- ResultBean is a PORO (Plain Old Ruby Object) which inherits from NestedResultBean class (inlcuded). This class
16
- is instantiated via a hash at Ruby Runtime, allowing access to vars via dot or hash notation,
17
- and is serializable (<obj>.to_hash) using standard Hash serialization methods.
12
+ SknUtils contains a small collection of Ruby utilities, the first being a NestedResult a key/value container.
18
13
  EOF
19
14
 
20
15
  spec.description = <<EOF
21
- Creates an PORO Object with instance variables and associated getters and setters for each input key, during runtime.
22
- If a key's value is also a hash, it too can optionally become an Object.
23
- If a key's value is a Array of Hashes, each element of the Array can optionally become an Object.
24
-
25
-
26
- This nesting action is controlled by the value of the options key ':depth'. Options key :depth defaults
27
- to :multi, and has options of :single, :multi, or :multi_with_arrays
28
-
29
-
30
- The ability of the resulting Object to be Marshalled(dump/load) can be preserved by merging configuration options
31
- into the input params. Key ':enable_serialization' set to true. It defaults to false for speed purposes.
32
-
33
-
16
+ The intent of NestedResult class is to be a container of data results or key/value pairs, with easy access to its contents, and on-demand transformation back to the hash (#to_hash).
34
17
  Review the RSpec tests, and or review the README for more details.
35
18
  EOF
36
19
 
@@ -46,7 +29,8 @@ EOF
46
29
  spec.add_development_dependency "rake", ">= 0"
47
30
  spec.add_development_dependency "rspec", '~> 3.0'
48
31
  spec.add_development_dependency "pry", ">= 0"
49
-
32
+ spec.add_development_dependency "simplecov", ">= 0"
33
+
50
34
  ## Make sure you can build the gem on older versions of RubyGems too:
51
35
  spec.rubygems_version = "1.6.2"
52
36
  spec.required_rubygems_version = Gem::Requirement.new(">= 0") if spec.respond_to? :required_rubygems_version=