blobject 0.2.3 → 0.3.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.
data/.gitignore CHANGED
@@ -1,4 +1,4 @@
1
1
  *.gem
2
2
  .bundle
3
- Gemfile.lock
4
3
  pkg/*
4
+ TODO
data/.pryrc CHANGED
@@ -1,5 +1,5 @@
1
1
  $:.push File.expand_path('../lib', __FILE__)
2
- require 'ruby-debug'
2
+ require 'debugger'
3
3
  require 'blobject'
4
4
 
5
5
  def reload!
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ blobject (0.3.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ ansi (1.4.2)
10
+ builder (3.0.0)
11
+ coderay (1.0.6)
12
+ columnize (0.3.6)
13
+ debugger (1.1.3)
14
+ columnize (>= 0.3.1)
15
+ debugger-linecache (~> 1.1.1)
16
+ debugger-ruby_core_source (~> 1.1.2)
17
+ debugger-linecache (1.1.1)
18
+ debugger-ruby_core_source (>= 1.1.1)
19
+ debugger-ruby_core_source (1.1.3)
20
+ method_source (0.7.1)
21
+ minitest (3.0.1)
22
+ minitest-reporters (0.7.1)
23
+ ansi
24
+ builder
25
+ minitest (>= 2.0, < 4.0)
26
+ ruby-progressbar
27
+ pry (0.9.9.6)
28
+ coderay (~> 1.0.5)
29
+ method_source (~> 0.7.1)
30
+ slop (>= 2.4.4, < 3)
31
+ ruby-progressbar (0.0.10)
32
+ slop (2.4.4)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ blobject!
39
+ debugger
40
+ minitest
41
+ minitest-reporters
42
+ pry
data/README.markdown ADDED
@@ -0,0 +1,157 @@
1
+ ![](https://github.com/sjltaylor/blobject/raw/master/blobject.png)
2
+ ![](https://github.com/sjltaylor/blobject/raw/master/blob_defn.png)
3
+
4
+ Data structures which __just work__
5
+
6
+ ## About
7
+
8
+ A Blobject is a thin wrapper around a hash
9
+
10
+
11
+ They are *freeform* which means you can do this...
12
+
13
+ data = Blobject.new
14
+
15
+ data.name = "Johnny"
16
+ data.number = 316
17
+
18
+ like an OpenStruct, the members are not predefined attributes
19
+
20
+ unlike OpenStruct, Blobjects can be arbitrarily *complex* which means you can do this...
21
+
22
+ data = Blobject.new
23
+
24
+ data.name.first = "Johnny"
25
+ data.name.surname = "Begood"
26
+
27
+ data.my.object.with.deep.nested.members = "happy place"
28
+
29
+ You can test to see if a member is defined:
30
+
31
+ data.something_here?
32
+ => false
33
+
34
+
35
+
36
+ ## Used for Configuration
37
+
38
+ Consider a configuration object which contains credentials for a third-party api.
39
+
40
+ third_party_api:
41
+ secret_key: 'S3CR3T'
42
+ endpoint: 'http://services.thirdparty.net/api'
43
+
44
+ With a hash, usage looks like this:
45
+
46
+ CONFIG[:third_party_api][:endpoint]
47
+
48
+ With a Blobject, usage looks like this:
49
+
50
+ CONFIG.third_party_api.endpoint
51
+
52
+ References to the endpoint are scattered throughout the codebase, then one day the endpoint is separated into its constituent parts to aide in testing and staging.
53
+
54
+ third_party_api:
55
+ secret_key: 'S3CR3T'
56
+ protocol: 'http'
57
+ hostname: 'services.thirdparty.net'
58
+ path: '/api'
59
+
60
+ Using a blobject we can easily avoid having to refactor our code...
61
+
62
+ CONFIG = Blobject.from_yaml(File.read('./config.yml'))
63
+
64
+ CONFIG.third_party_api.instance_eval do
65
+ def endpoint
66
+ "#{protocol}://#{hostname}#{path}"
67
+ end
68
+ end
69
+
70
+
71
+ ## Serialization
72
+
73
+ Blobjects can be used to easily build complex payloads.
74
+
75
+ person = Blobject.new
76
+
77
+ person.name = first: 'David', last: 'Platt'
78
+
79
+ person.address.tap do |address|
80
+ address.street = "..."
81
+ address.city = "..."
82
+ end
83
+
84
+ person.next_of_kin.address.city = '...'
85
+
86
+ # after the payload is constructed it can be frozen to prevent modification
87
+ person.freeze
88
+
89
+ A nice pattern in most cases is to use an initialization block...
90
+
91
+ Blobject.new optional_hash_of_initial_data do |b|
92
+ b.name = ...
93
+ end.freeze
94
+
95
+
96
+ ## Deserialization
97
+
98
+
99
+ Suppose you receive a payload from an api which may or may not contain an address and city...
100
+
101
+ payload = Blobject.from_json request[:payload]
102
+
103
+ # if the payload does have an address...
104
+ city = payload.address.city
105
+ => 'Liverpool'
106
+
107
+ # if the payload does not have an address or city
108
+ city = payload.address.city
109
+ => nil
110
+ # rather than request[:payload][:address][:city] which would raise
111
+ # NoMethodError: undefined method `[]' for nil:NilClass
112
+
113
+
114
+ Also, you don't need to concern yourself whether hash keys are symbols or strings.
115
+
116
+
117
+
118
+ ## Performance
119
+
120
+ The runtime performance of something as low level as blobject deserves consideration.
121
+
122
+ see `/benchmarks`
123
+
124
+ ITERATIONS: 1000000
125
+
126
+
127
+ BENCHMARK: assign
128
+
129
+ user system total real
130
+ Object: 0.190000 0.000000 0.190000 ( 0.229685)
131
+ Hash: 0.220000 0.000000 0.220000 ( 0.230500)
132
+ OpenStruct: 0.520000 0.000000 0.520000 ( 0.529861)
133
+ Blobject: 0.790000 0.000000 0.790000 ( 0.808610)
134
+ Hashie: 8.270000 0.030000 8.300000 ( 9.291184)
135
+
136
+
137
+ BENCHMARK: read
138
+
139
+ user system total real
140
+ Hash: 0.160000 0.000000 0.160000 ( 0.165141)
141
+ Object: 0.170000 0.000000 0.170000 ( 0.170228)
142
+ OpenStruct: 0.340000 0.000000 0.340000 ( 0.342430)
143
+ Blobject: 0.410000 0.000000 0.410000 ( 0.410574)
144
+ Hashie: 1.880000 0.000000 1.880000 ( 1.921718)
145
+
146
+ Host CPU: 2.13GHz Core2
147
+
148
+ A Blobject is three-four times slower than an equivalent Object.
149
+
150
+
151
+ ## Limitations
152
+
153
+ * will not work with basic objects unless #class and #freeze are implemented
154
+ * cyclic blobject graphs result in infinite recursion StackOverflow
155
+ * Ruby 1.8.7 is not supported. Testing rubies...
156
+ * mri 1.9.3-p194
157
+ * mri 1.9.2-p290
File without changes
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
1
  require 'bundler'
2
- Bundler::GemHelper.install_tasks
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,100 @@
1
+ require 'benchmark'
2
+ require 'hashie'
3
+ require 'ostruct'
4
+ require_relative '../lib/blobject'
5
+
6
+
7
+ iterations = ARGV[0] || 1000000
8
+
9
+
10
+ class A # a foo-bar class the we'll use when benchmarking objects
11
+ attr_accessor :member1
12
+ end
13
+
14
+
15
+ object = A.new
16
+ blobject = Blobject.new
17
+ hash = {}
18
+ hashie = Hashie::Mash.new
19
+ ostruct = OpenStruct.new
20
+
21
+ value = "data"
22
+
23
+
24
+
25
+ puts "\n\nITERATIONS: #{iterations}\n\n"
26
+ puts "\nBENCHMARK: assign\n=====================\n\n"
27
+
28
+
29
+
30
+ Benchmark.bm do |benchmark|
31
+
32
+ benchmark.report("Object: ") do
33
+ iterations.times do
34
+ object.member1 = value
35
+ end
36
+ end
37
+
38
+ benchmark.report("Hash: ") do
39
+ iterations.times do
40
+ hash[:member1] = value
41
+ end
42
+ end
43
+
44
+ benchmark.report("Blobject: ") do
45
+ iterations.times do
46
+ blobject.member1 = value
47
+ end
48
+ end
49
+
50
+ benchmark.report("Hashie: ") do
51
+ iterations.times do
52
+ hashie.member1 = value
53
+ end
54
+ end
55
+
56
+ benchmark.report("OpenStruct: ") do
57
+ iterations.times do
58
+ ostruct.member1 = value
59
+ end
60
+ end
61
+ end
62
+
63
+
64
+
65
+ puts "\n\nBENCHMARK: read\n=====================\n\n"
66
+
67
+
68
+
69
+ Benchmark.bm do |benchmark|
70
+
71
+ benchmark.report("Object: ") do
72
+ iterations.times do
73
+ value = object.member1
74
+ end
75
+ end
76
+
77
+ benchmark.report("Hash: ") do
78
+ iterations.times do
79
+ value = hash[:member1]
80
+ end
81
+ end
82
+
83
+ benchmark.report("Blobject: ") do
84
+ iterations.times do
85
+ value = blobject.member1
86
+ end
87
+ end
88
+
89
+ benchmark.report("Hashie: ") do
90
+ iterations.times do
91
+ value = hashie.member1
92
+ end
93
+ end
94
+
95
+ benchmark.report("OpenStruct: ") do
96
+ iterations.times do
97
+ value = ostruct.member1
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,12 @@
1
+
2
+
3
+ ITERATIONS: 1000000
4
+
5
+
6
+ BENCHMARK: assign
7
+ =====================
8
+
9
+ user system total real
10
+ Object: 0.180000 0.000000 0.180000 ( 0.229544)
11
+ Hash: 0.230000 0.000000 0.230000 ( 0.241922)
12
+ Hashie:
data/blob_defn.png ADDED
Binary file
data/blobject.gemspec CHANGED
@@ -14,9 +14,10 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "blobject"
16
16
 
17
-
18
- s.add_development_dependency 'rspec'
19
- s.add_development_dependency 'ruby-debug19'
17
+ s.add_development_dependency 'minitest'
18
+ s.add_development_dependency 'minitest-reporters'
19
+ s.add_development_dependency 'pry'
20
+ s.add_development_dependency 'debugger'
20
21
 
21
22
  s.files = `git ls-files`.split("\n")
22
23
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
data/console ADDED
@@ -0,0 +1,7 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'ruby-debug' if ARGV.delete 'debug'
4
+ require './lib/blobject'
5
+ require 'pry'
6
+
7
+ Pry.start
data/lib/blobject.rb CHANGED
@@ -1,209 +1,259 @@
1
- require 'blobject/version'
2
1
  require 'json'
3
-
4
- def blobject *parameters, &block
5
- Blobject.new *parameters, &block
6
- end
2
+ require 'yaml'
3
+ require_relative 'blobject/version'
7
4
 
8
5
  class Blobject
9
6
 
10
- def initialize hash = {}, &block
11
- @hash = {}
12
- merge hash
13
-
14
- @modifying = false
15
- self.modify &block if block_given?
16
- end
17
-
18
- def modify &block
19
-
20
- __r_modify_set__ true
21
-
22
- exception = nil
23
-
24
- begin
25
- self.instance_eval &block
26
- rescue Exception => e
27
- exception = e
7
+ # filter :to_ary else Blobject#to_ary returns a
8
+ # blobject which is not cool, especially if you are puts.
9
+ ProhibitedNames = [:to_ary]
10
+
11
+ module Error; end
12
+
13
+ def initialize hash = {}
14
+
15
+ @hash = hash
16
+
17
+ @hash.keys.each do |key|
18
+ unless key.class <= Symbol
19
+ value = @hash.delete key
20
+ key = key.to_sym
21
+ @hash[key] = value
22
+ end
23
+ end
24
+
25
+ __visit_subtree__ do |name, node|
26
+ if node.class <= Hash
27
+ @hash[name] = Blobject.new node
28
+ end
28
29
  end
30
+
31
+ yield self if block_given?
32
+ end
33
+
34
+ def inspect
29
35
 
30
- __r_modify_set__ false
36
+ @hash.inspect
37
+ end
38
+
39
+ def hash
31
40
 
32
- raise exception unless exception.nil?
33
- return self
41
+ @hash
34
42
  end
35
-
36
- def method_missing sym, *params, &block
43
+
44
+ def to_hash
37
45
 
38
- if match = /^has_(.+)\?/.match(sym)
39
- return @hash.has_key? match[1].to_sym
46
+ h = hash.dup
47
+ __visit_subtree__ do |name, node|
48
+ h[name] = node.to_hash if node.respond_to? :to_hash
40
49
  end
41
-
42
- if @modifying
43
-
44
- case params.length
45
- when 0 # get
46
- value = @hash[sym]
47
-
48
- if value
49
- value.modify(&block) if block_given? and value.instance_of?(Blobject)
50
- return value
51
- end
52
-
53
- child = Blobject.new
54
- parent = self
55
-
56
- child.__r_modify_set__ true
57
-
58
- store_in_parent = lambda {
59
-
60
- parent_hash = parent.instance_variable_get '@hash'
61
- parent_hash[sym] = child
62
-
63
- parent_store_in_parent = parent.instance_variable_get :@__store_in_parent__
64
- parent_store_in_parent.call unless parent_store_in_parent.nil?
65
-
66
- child.method(:remove_instance_variable).call(:@__store_in_parent__)
67
- }
68
-
69
- child.instance_variable_set :@__store_in_parent__, store_in_parent
70
-
71
- return block_given? ? child.modify(&block) : child
72
- when 1 # set
73
- @hash[sym] = params[0]
74
-
75
- store_in_parent = @__store_in_parent__
76
- store_in_parent.call unless store_in_parent.nil?
77
-
78
- return self
79
- end
80
- else
81
- return @hash[sym] if @hash.has_key? sym
50
+ h
51
+ end
52
+
53
+ # method_missing is only called the first time an attribute is used. successive calls use
54
+ # memoized getters, setters and checkers
55
+ def method_missing method, *params, &block
56
+
57
+ __tag_and_raise__ NoMethodError.new(method) if ProhibitedNames.include?(method)
58
+
59
+ case
60
+ # assignment in conditionals is usually a bad smell, here it helps minimize regex matching
61
+ when (name = method[/^\w+$/, 0]) && params.length == 0
62
+ # the call is an attribute reader
63
+ return nil if frozen? and not @hash.has_key?(method)
64
+
65
+ self.class.send :__define_attribute__, name
66
+
67
+ return send(method) if @hash.has_key? method
68
+
69
+ parent = self
70
+ nested_blobject = self.class.new
71
+
72
+ store_in_parent = lambda do
73
+ parent.send "#{name}=", nested_blobject
74
+ nested_blobject.send :remove_instance_variable, :@store_in_parent
75
+ end
76
+
77
+ nested_blobject.instance_variable_set :@store_in_parent, store_in_parent
78
+
79
+ return nested_blobject
80
+
81
+ when (name = method[/^(\w+)=$/, 1]) && params.length == 1
82
+ # the call is an attribute writer
83
+
84
+ self.class.send :__define_attribute__, name
85
+ return send method, params.first
86
+
87
+ when (name = method[/^(\w+)\?$/, 1]) && params.length == 0
88
+ # the call is an attribute checker
89
+
90
+ self.class.send :__define_attribute__, name
91
+ return send method
82
92
  end
83
-
93
+
84
94
  super
85
95
  end
86
-
87
- def merge hash
88
-
89
- hash.each do |key, value|
90
- @hash[key.to_s.to_sym] = self.class.__blobjectify__ value
91
- end
96
+
97
+ def respond_to? method
92
98
 
93
- self
94
- end
95
-
96
- def empty?
97
- @hash.empty?
99
+ return true if self.methods.include?(method)
100
+ return false if ProhibitedNames.include?(method)
101
+
102
+ method = method.to_s
103
+
104
+ [/^(\w+)=$/, /^(\w+)\?$/, /^\w+$/].any? do |r|
105
+ r.match(method)
106
+ end
98
107
  end
99
-
100
- def [] key
101
- @hash[key.to_s.to_sym]
108
+
109
+ def == other
110
+ return @hash == other.hash if other.class <= Blobject
111
+ return @hash == other if other.class <= Hash
112
+ super
102
113
  end
103
-
104
- def keys
105
- @hash.keys
114
+
115
+ def [] name
116
+
117
+ send name
106
118
  end
107
-
108
- def values
109
- @hash.values
119
+
120
+ def []= name, value
121
+
122
+ send "#{name.to_s}=", value
110
123
  end
111
124
 
112
- def each &block
113
- return @hash.each &block
125
+ def freeze
126
+ __visit_subtree__ { |name, node| node.freeze }
127
+ @hash.freeze
128
+ super
114
129
  end
115
-
116
- def []= key, value
117
- send key, value
130
+
131
+ def as_json
132
+
133
+ to_hash
118
134
  end
119
-
120
- def dup
121
- Marshal.load(Marshal.dump(self))
135
+
136
+ def to_json
137
+
138
+ as_json.to_json
122
139
  end
123
-
124
- def to_hash
125
- hash = @hash
140
+
141
+ def as_yaml
126
142
 
127
- hash.each do |key, value|
128
- hash[key] = value.to_hash if (value.instance_of? Blobject)
129
-
130
- if value.instance_of? Array
131
- hash[key] = value.map do |v|
132
- v.instance_of?(Blobject) ? v.to_hash : v
133
- end
134
- end
135
- end
143
+ to_hash
144
+ end
145
+
146
+ def to_yaml
136
147
 
137
- hash
148
+ as_yaml.to_yaml
138
149
  end
139
-
140
- def from_hash hash
141
- Blobject.new hash
150
+
151
+ def self.from_json json
152
+
153
+ from_json!(json).freeze
142
154
  end
143
-
144
- def to_yaml *params
145
- to_hash.to_yaml *params
155
+
156
+ def self.from_json! json
157
+
158
+ __from_hash_or_array__(JSON.parse(json))
146
159
  end
147
-
160
+
148
161
  def self.from_yaml yaml
149
- __blobjectify__ YAML.load(yaml)
150
- end
151
-
152
- def to_json *params
153
- @hash.to_json *params
162
+
163
+ from_yaml!(yaml).freeze
154
164
  end
155
-
156
- def self.from_json json
157
- __blobjectify__ JSON.load(json)
165
+
166
+ def self.from_yaml! yaml
167
+
168
+ __from_hash_or_array__(YAML.load(yaml))
158
169
  end
159
-
160
- def self.read path
161
- case File.extname(path).downcase
162
- when /\.y(a)?ml$/
163
- from_yaml File.read(path)
164
- when /\.js(on)?$/
165
- from_json File.read(path)
166
- else
167
- raise "Cannot handle file format of #{path}"
170
+
171
+ private
172
+ # to avoid naming collisions private method names are prefixed and suffix with double unerscores (__)
173
+
174
+ def __visit_subtree__ &block
175
+
176
+ @hash.each do |name, node|
177
+
178
+ if node.class <= Array
179
+ node.flatten.each do |node_node|
180
+ block.call(nil, node_node, &block)
181
+ end
182
+ end
183
+
184
+ block.call name, node, &block
168
185
  end
169
186
  end
170
-
171
- def dup
172
- Blobject.new to_hash
173
- end
174
-
175
- def inspect
176
- @hash.inspect
187
+
188
+ # errors from this library can be handled with rescue Blobject::Error
189
+ def __tag_and_raise__ e
190
+ raise e
191
+ rescue
192
+ e.extend Blobject::Error
193
+ raise e
177
194
  end
178
195
 
179
- protected
180
-
181
- def self.__blobjectify__ obj
182
-
183
- if obj.instance_of?(Hash)
184
-
185
- obj.each do |key, value|
186
- obj[key] = __blobjectify__ value
196
+ class << self
197
+
198
+ private
199
+
200
+ def __from_hash_or_array__ hash_or_array
201
+
202
+ if hash_or_array.class <= Array
203
+ return hash_or_array.map do |e|
204
+ if e.class <= Hash
205
+ Blobject.new e
206
+ else
207
+ e
208
+ end
209
+ end
187
210
  end
188
-
189
- return self.new obj
211
+
212
+ Blobject.new hash_or_array
190
213
  end
191
-
192
- if obj.instance_of?(Array)
193
- return obj.map do |e|
194
- __blobjectify__ e
214
+
215
+ def __define_attribute__ name
216
+
217
+ __tag_and_raise__ NameError.new("invalid attribute name #{name}") unless name =~ /^\w+$/
218
+ name = name.to_sym
219
+
220
+ methods = self.instance_methods
221
+
222
+ setter_name = (name.to_s + '=').to_sym
223
+ unless methods.include? setter_name
224
+ self.send :define_method, setter_name do |value|
225
+ begin
226
+ value = self.class.new(value) if value.class <= Hash
227
+ @hash[name] = value
228
+ rescue ex
229
+ __tag_and_raise__(ex)
230
+ end
231
+ @store_in_parent.call unless @store_in_parent.nil?
232
+ end
195
233
  end
196
- end
197
-
198
- obj
199
- end
200
-
201
- def __r_modify_set__ v
202
- @modifying = v
203
- @hash.values.each do |child|
204
- if child.class <= Blobject
205
- child.__r_modify_set__ v
234
+
235
+ unless methods.include? name
236
+ self.send :define_method, name do
237
+
238
+ value = @hash[name]
239
+
240
+ if value.nil? && !frozen?
241
+ value = self.class.new
242
+ @hash[name] = value
243
+ end
244
+
245
+ value
246
+ end
206
247
  end
248
+
249
+ checker_name = (name.to_s + '?').to_sym
250
+ unless methods.include? checker_name
251
+ self.send :define_method, checker_name do
252
+ @hash.key?(name)
253
+ end
254
+ end
255
+
256
+ name
207
257
  end
208
258
  end
209
259
  end