skn_utils 2.0.6 → 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,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=