blobject 0.2.3 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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