ostructer 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ === 0.1 / 2011-09-11
2
+
3
+ * Everything is new. First release
@@ -0,0 +1,5 @@
1
+ History.rdoc
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/ostructer.rb
@@ -0,0 +1,17 @@
1
+ = Ostructer - Open Struct Builder in Ruby
2
+
3
+ * http://github.com/geraldb/ostructer
4
+
5
+ == DESCRIPTION:
6
+
7
+ Lets you build open structs from nested hashes, arrays and strings.
8
+
9
+ == INSTALL:
10
+
11
+ Just install the gem:
12
+
13
+ $ sudo gem install ostructer
14
+
15
+ == LICENSE:
16
+
17
+ The ostructers sources are dedicated to the public domain. Use it as you please with no restrictions whatsoever.
@@ -0,0 +1,23 @@
1
+ require 'hoe'
2
+ require './lib/ostructer.rb'
3
+
4
+ Hoe.spec 'ostructer' do
5
+
6
+ self.version = Ostructer::VERSION
7
+
8
+ self.summary = 'Ostructer - Open Struct Builder'
9
+ self.url = 'http://github.com/geraldb/ostructer'
10
+
11
+ self.author = 'Gerald Bauer'
12
+ self.email = 'gerald.bauer@gmail.com'
13
+
14
+ self.extra_deps = [
15
+ ['xml-simple','>= 1.1.0']
16
+ ]
17
+
18
+ self.remote_rdoc_dir = 'doc'
19
+
20
+ # switch extension to .rdoc for gihub formatting
21
+ self.readme_file = 'README.rdoc'
22
+ self.history_file = 'History.rdoc'
23
+ end
@@ -0,0 +1,353 @@
1
+
2
+ require 'forwardable'
3
+ require 'cgi'
4
+
5
+ module Ostructer
6
+
7
+ VERSION = '0.1'
8
+
9
+ ## todo: use blank slate/basic object (no id? no type?)
10
+ #
11
+ # undef id and type (possible?)
12
+ # class OpenStruct
13
+ # undef id # deprecated in Ruby 1.8; removed in 1.9
14
+ # undef type # same thing
15
+ # end
16
+
17
+ #
18
+ #
19
+ # folded elements (e.g. element with no attributes just content) get returned as string, thus,
20
+ #
21
+ # to_bool
22
+ # to_date
23
+ # to_xml will not be available!!!!! (unless added to String class)
24
+ #
25
+ # check if to_date exists? in ruby standard
26
+
27
+ #
28
+ # NB: key.first -> different with Rails or without (without results in more than one letter!!)
29
+
30
+ class OsBase # base for OsArray/OsOpenStruct/OsOpenStructNil
31
+
32
+ attr :logger # read-only
33
+ attr_accessor :parent # read,write
34
+ attr_accessor :field # read,write
35
+
36
+ def full_dot_path
37
+ path = "#{@field}"
38
+ node = @parent
39
+
40
+ ## NOTE: assume if parent == nil openstruct is root node
41
+ while node && node.parent
42
+ # logger.debug "node.class: #{node.class}, node.field: #{node.field}"
43
+ if node.is_a?( OsArray )
44
+ path = "#{node.field}" + path
45
+ else
46
+ path = "#{node.field}." + path
47
+ end
48
+ node = node.parent
49
+ end
50
+ path
51
+ end
52
+
53
+ end # class OsBase
54
+
55
+
56
+ class OsValue < OsBase
57
+
58
+ # holds/wraps a string
59
+
60
+ def initialize( value, logger )
61
+ @value = value
62
+ @logger = logger
63
+
64
+ @parent = nil
65
+ @field ="[!unknown_value]"
66
+ end
67
+
68
+ def to_openstruct_pass2
69
+ # do nothing (has no children)
70
+ end
71
+
72
+ include Comparable
73
+
74
+ def <=>(other)
75
+ if other.is_a? Numeric
76
+ logger.debug "OsValue <=> Numeric: #{self.to_i} <=> #{other}"
77
+ self.to_i <=> other
78
+ elsif other.is_a? String
79
+ logger.debug "OsValue <=> String: #{self.to_s} <=> #{other}"
80
+ self.to_s <=> other
81
+ else
82
+ logger.debug "OsValue - no <=> defined for type #{other.class}"
83
+ nil
84
+ end
85
+
86
+ ## todo: add bool
87
+ end
88
+
89
+ def to_s
90
+ @value
91
+ end
92
+
93
+ def to_i
94
+ @value.to_i
95
+ end
96
+
97
+ def to_bool
98
+ @value == 'true'
99
+ end
100
+
101
+
102
+ end # class OsValue
103
+
104
+
105
+ class OsArray < OsBase
106
+ extend Forwardable
107
+
108
+ include Enumerable
109
+
110
+ def_delegators :@ary, :each, :[], :size
111
+
112
+ def initialize( ary, logger )
113
+ @ary = ary
114
+ @logger = logger
115
+
116
+ @parent = nil
117
+ @field = "[!unknown_array]"
118
+ end
119
+
120
+ def to_openstruct_pass2
121
+ self.each_with_index do | el, index |
122
+ if el.is_a?( OsOpenStruct ) || el.is_a?( OsArray ) || el.is_a?( OsValue )
123
+ el.field = "[#{index}]"
124
+ el.parent = self
125
+ end
126
+ el.to_openstruct_pass2
127
+ end
128
+ end
129
+
130
+ end # class OsArray
131
+
132
+
133
+
134
+ class OsOpenStructNil < OsBase
135
+
136
+ def initialize( parent, field, logger )
137
+ @parent = parent
138
+ @field = "!#{field}" # mark missing field with starting ! (exclamation mark)
139
+ @logger = logger
140
+ end
141
+
142
+ def nil?
143
+ logger.debug "calling OsNil#nil?"
144
+ true
145
+ end
146
+
147
+ def to_s
148
+ logger.debug "calling OsNil#to_s"
149
+ nil
150
+ end
151
+
152
+ def to_i
153
+ logger.debug "calling OsNil#to_i"
154
+ nil
155
+ end
156
+
157
+ def to_date
158
+ logger.debug "calling OsNil#to_date"
159
+ nil
160
+ end
161
+
162
+ def to_bool
163
+ logger.debug "calling OsNil#to_bool"
164
+ nil
165
+ end
166
+
167
+
168
+ def method_missing(mn,*a)
169
+ ## NEW: added; add to org
170
+ mn = mn.to_s
171
+
172
+ logger.warn "OsOpenStructNil - feld '#{mn}' nicht gefunden using path '#{full_dot_path}'; returning OsOpenStructNil"
173
+ OsOpenStructNil.new( self, mn, logger )
174
+ end
175
+
176
+ end # end class OsOpenStructNil
177
+
178
+
179
+
180
+ class OsOpenStruct < OsBase
181
+
182
+ def initialize( hash_in, logger )
183
+ @parent = nil
184
+ @field = "[!unknown_hash]"
185
+
186
+ @logger = logger
187
+ @hash = {}
188
+
189
+ ## fix: can we change the hash-in-place?? don't create a new one
190
+
191
+ # todo: add hook for normalize keys (downcase first letter, strip packages, etc.)
192
+ hash_in.each do |key,value|
193
+ # downcase first letter (e.g. Address to address)
194
+ key = key[0...1].downcase + key[1..-1]
195
+
196
+ @hash[ key ] = value
197
+ end
198
+ end
199
+
200
+ def to_openstruct_pass2 # link up: set parent and field
201
+ @hash.each do |key,value|
202
+ if value.is_a?( OsOpenStruct ) || value.is_a?( OsArray ) || value.is_a?( OsValue )
203
+ value.field = key.to_s
204
+ value.parent = self
205
+ end
206
+ value.to_openstruct_pass2
207
+ end
208
+ # logger.debug "after MyOpenStruct#to_openstruct_pass2 parent=#{parent.nil? ? 'nil' : parent.object_id}, field=#{field}"
209
+ end
210
+
211
+ def has_key?( key )
212
+ # NOTE: use has_key? instead of nil? to avoid warnings in log that are expected if fields are missing
213
+ @hash.has_key?( key.to_s )
214
+ end
215
+
216
+ def fetch( key, default = nil )
217
+ @hash.fetch( key, default)
218
+ end
219
+
220
+ include Comparable
221
+
222
+ def <=>(other)
223
+
224
+ ## fix: check if struct has short-cut value (e.g. text); short-cut value might not exist
225
+ ## fix/todo: add bool
226
+
227
+ if other.is_a? Numeric
228
+ logger.debug "OsOpenStruct <=> Numeric: #{self.to_i} <=> #{other}"
229
+ self.to_i <=> other
230
+ elsif other.is_a? String
231
+ logger.debug "OsOpenStruct <=> String: #{self.to_s} <=> #{other}"
232
+ self.to_s <=> other
233
+ else
234
+ logger.debug "OsOpenStruct - no <=> defined for type #{other.class}"
235
+ nil
236
+ end
237
+ end
238
+
239
+ def method_missing(mn,*a)
240
+
241
+ mn = mn.to_s
242
+
243
+ ## convenience property for boolean check
244
+ if mn[-1].chr == "?"
245
+ mn = mn[0..-2] # cut of trailing ?
246
+ if @hash.has_key?(mn)
247
+ value = @hash.fetch( mn ).fetch( 'text' )
248
+
249
+ logger.debug "OsOpenStruct - boolean feld '#{mn}' with value '#{value}' returns #{value=='1'}"
250
+
251
+ return value == "1" # return
252
+ else
253
+ logger.warn "OsOpenStruct - boolean feld '#{mn}' nicht gefunden in Hash using path '#{full_dot_path}'; returning OsOpenStructNil"
254
+ OsOpenStructNil.new( self, mn+"?", logger ) # return
255
+ end
256
+ else
257
+ if @hash.has_key?(mn)
258
+ @hash[mn]
259
+ else
260
+ logger.warn "OsOpenStruct - feld '#{mn}' nicht gefunden in Hash using path '#{full_dot_path}'; returning OsOpenStructNil"
261
+ ## pp self
262
+ OsOpenStructNil.new( self, mn, logger )
263
+ end
264
+ end
265
+ end
266
+
267
+
268
+ def to_xml
269
+ logger.debug "calling OsOpenStruct#to_xml"
270
+ ## todo: check if CGI module is included
271
+ ## todo: use another unescape method for xml entities??
272
+ CGI.unescapeHTML( internal_value )
273
+ end
274
+
275
+ def to_s
276
+ # puts "[DEBUG] calling OsOpenStruct#to_s"
277
+ value = internal_value
278
+ value.nil? ? nil : value.to_s
279
+ end
280
+
281
+ def to_i
282
+ # puts "[DEBUG] calling OsOpenStruct#to_i"
283
+ value = internal_value
284
+ value.nil? ? nil : value.to_i
285
+ end
286
+
287
+ def to_date
288
+ logger.debug "calling MyOpenStruct#to_date"
289
+ internal_value # format to date required for sqlite
290
+ end
291
+
292
+ def to_bool
293
+ ## todo: how to handle no value exists? return false or nil?
294
+ # puts "[DEBUG] calling MyOpenStruct#to_bool"
295
+ internal_value == 'true'
296
+ end
297
+
298
+ private
299
+ def internal_value
300
+ if @hash.has_key?('text')
301
+ value = @hash.fetch( 'text' )
302
+ return value
303
+ else
304
+ logger.warn "feld 'text' nicht gefunden (called internal_value())"
305
+ return nil
306
+ end
307
+ end
308
+
309
+ end # class OsOpenStruct
310
+
311
+
312
+ end # module Ostructer
313
+
314
+
315
+ ################################
316
+ ##
317
+ ## extensions to core/built-in classes (Object, Array, Hash)
318
+ ##
319
+
320
+ class Object
321
+ def to_openstruct( logger )
322
+
323
+ # pass 1: construct data structure using new classes (OsArray, OsOpenStruct, OsValue)
324
+ # convert Array to OsArray
325
+ # convert Hash to OsOpenStruct
326
+ # convert String to OsValue
327
+
328
+ newself = self.to_openstruct_pass1( logger )
329
+ newself.to_openstruct_pass2 # pass 2: link up parents (reverse link)
330
+ newself
331
+ end
332
+ end
333
+
334
+ class String
335
+ def to_openstruct_pass1( logger )
336
+ Ostructer::OsValue.new( self, logger )
337
+ end
338
+ end
339
+
340
+ class Array
341
+ def to_openstruct_pass1( logger )
342
+ mapped = map{ |el| el.to_openstruct_pass1( logger ) }
343
+ Ostructer::OsArray.new( mapped, logger )
344
+ end
345
+ end
346
+
347
+ class Hash
348
+ def to_openstruct_pass1( logger )
349
+ mapped = {}
350
+ each{ |key,value | mapped[key.to_s] = value.to_openstruct_pass1( logger ) }
351
+ Ostructer::OsOpenStruct.new( mapped, logger )
352
+ end
353
+ end
@@ -0,0 +1,69 @@
1
+ require 'test/unit'
2
+ require 'logger'
3
+ require 'pp'
4
+
5
+ require 'rubygems'
6
+ require 'xmlsimple'
7
+
8
+ # LIB_PATH = File.expand_path( File.dirname( 'lib' ) )
9
+ # $LOAD_PATH.unshift(LIB_PATH)
10
+
11
+ require 'lib/ostructer.rb'
12
+
13
+ ## notes:
14
+ ## add note about faster_xml_simple (drop-in replacement for xml_in only using libxml)
15
+
16
+ ## todo: check if root is simple root if forceArray is true!!
17
+
18
+ # NB: ContentKey text only added if at least one attribute present!!
19
+ # otherwise value is used-as-is in place (not as a hash)
20
+ # use ForceContent => true options to force creation of hash
21
+ #
22
+ #
23
+ #
24
+
25
+ class TestConfig < Test::Unit::TestCase
26
+
27
+ def setup
28
+ fn = 'test/config.xml'
29
+ opts = { 'ContentKey' => 'text',
30
+ 'ForceArray' => false,
31
+ 'ForceContent' => true,
32
+ 'KeepRoot' => true }
33
+
34
+ @doc = XmlSimple.xml_in( fn, opts )
35
+
36
+ # puts "=== xmlsimple:"
37
+ # pp @doc
38
+
39
+ logger = Logger.new(STDOUT)
40
+ logger.level = Logger::DEBUG
41
+
42
+ @doc = @doc.to_openstruct( logger )
43
+
44
+ ## puts "=== ostructer:"
45
+ ## pp @doc
46
+ end
47
+
48
+ def test_readers
49
+ assert_equal( 'sahara', @doc.config.server[0].name.to_s ) # attribute
50
+ assert_equal( '10.0.0.101', @doc.config.server[0].address[0].text.to_s ) # contentKey
51
+ assert_equal( '10.0.0.101', @doc.config.server[0].address[0].to_s ) # contentKey shortcut (no .text)
52
+ assert( '.text shortcut String <=>', @doc.config.server[0].address[0] == '10.0.0.101' )
53
+ end
54
+
55
+ def test_full_dot_path
56
+ assert_equal( 'config.logdir', @doc.config.logdir.full_dot_path )
57
+ assert_equal( 'config.server', @doc.config.server.full_dot_path )
58
+ assert_equal( 'config.server[0]', @doc.config.server[0].full_dot_path )
59
+ assert_equal( 'config.server[0].name', @doc.config.server[0].name.full_dot_path )
60
+ assert_equal( 'config.server[0].address', @doc.config.server[0].address.full_dot_path )
61
+ assert_equal( 'config.server[0].address[0]', @doc.config.server[0].address[0].full_dot_path )
62
+ assert_equal( 'config.server[0].address[0].text', @doc.config.server[0].address[0].text.full_dot_path )
63
+ end
64
+
65
+ def test_missing
66
+ assert_equal( '!one.!two.!three', @doc.one.two.three.full_dot_path )
67
+ end
68
+
69
+ end # class TestConfig
@@ -0,0 +1,63 @@
1
+ require 'test/unit'
2
+ require 'logger'
3
+ require 'pp'
4
+
5
+ require 'rubygems'
6
+ require 'xmlsimple'
7
+
8
+ # LIB_PATH = File.expand_path( File.dirname( 'lib' ) )
9
+ # $LOAD_PATH.unshift(LIB_PATH)
10
+
11
+ require 'lib/ostructer.rb'
12
+
13
+ ## NB: XmlSimple -> ForceArray is true by default (turn it off!!)
14
+ ##
15
+
16
+ class TestCustomer < Test::Unit::TestCase
17
+
18
+ def setup
19
+ fn = 'test/customer.xml'
20
+ opts = { 'ContentKey' => 'text',
21
+ 'ForceArray' => false,
22
+ 'ForceContent' => true }
23
+
24
+ @customer = XmlSimple.xml_in( fn, opts )
25
+
26
+ puts "=== xmlsimple:"
27
+ pp @customer
28
+
29
+ logger = Logger.new(STDOUT)
30
+ logger.level = Logger::DEBUG
31
+
32
+ @customer = @customer.to_openstruct( logger )
33
+
34
+ # puts "=== ostructer:"
35
+ # pp @customer
36
+ end
37
+
38
+ def test_readers
39
+ assert_equal( 'home', @customer.address[0].typ.to_s ) # attribute
40
+ assert_equal( 'Joe', @customer.first_name.text.to_s )
41
+ assert_equal( 'Joe', @customer.first_name.to_s ) # short-cut (.text)
42
+ end
43
+
44
+
45
+ end # class TestCustomer
46
+
47
+
48
+ ## from the XmlSimple docs
49
+ #
50
+ # Elements which only contain text content will simply be represented as a scalar.
51
+ # Where an element has both attributes and text content, the element will be represented
52
+ # as a hash with the text content in the 'content' key:
53
+ #
54
+ # <opt>
55
+ # <one>first</one>
56
+ # <two attr="value">second</two>
57
+ # </opt>
58
+ #
59
+ # {
60
+ # 'one' => 'first',
61
+ # 'two' => { 'attr' => 'value', 'content' => 'second' }
62
+ # }
63
+ #
@@ -0,0 +1,40 @@
1
+ require 'logger'
2
+ require 'pp'
3
+
4
+ require 'rubygems'
5
+ require 'xmlsimple'
6
+
7
+ # LIB_PATH = File.expand_path( File.dirname( 'lib' ) )
8
+ # $LOAD_PATH.unshift(LIB_PATH)
9
+
10
+ require 'lib/ostructer.rb'
11
+
12
+
13
+ def test_keep_root
14
+ fn = 'test/config.xml'
15
+ opts = { 'ContentKey' => 'text',
16
+ 'ForceArray' => false,
17
+ 'ForceContent' => true,
18
+ 'KeepRoot' => true }
19
+
20
+ doc = XmlSimple.xml_in( fn, opts )
21
+
22
+ puts "=== xmlsimple:"
23
+ pp doc
24
+ end
25
+
26
+ def test_parse_status
27
+ fn = 'test/status.xml'
28
+ opts = { 'ContentKey' => 'text',
29
+ 'ForceArray' => false }
30
+
31
+ statuses = XmlSimple.xml_in( fn, opts )
32
+
33
+ puts "=== xmlsimple:"
34
+ pp statuses
35
+ end
36
+
37
+ if __FILE__ == $0
38
+ # test_keep_root()
39
+ test_parse_status()
40
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ostructer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Gerald Bauer
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-09-11 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: xml-simple
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 19
29
+ segments:
30
+ - 1
31
+ - 1
32
+ - 0
33
+ version: 1.1.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rubyforge
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 7
45
+ segments:
46
+ - 2
47
+ - 0
48
+ - 4
49
+ version: 2.0.4
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: hoe
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 21
61
+ segments:
62
+ - 2
63
+ - 6
64
+ - 1
65
+ version: 2.6.1
66
+ type: :development
67
+ version_requirements: *id003
68
+ description: Lets you build open structs from nested hashes, arrays and strings.
69
+ email: gerald.bauer@gmail.com
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - Manifest.txt
76
+ files:
77
+ - History.rdoc
78
+ - Manifest.txt
79
+ - README.rdoc
80
+ - Rakefile
81
+ - lib/ostructer.rb
82
+ - test/test_customer.rb
83
+ - test/test_config.rb
84
+ - test/test_misc.rb
85
+ has_rdoc: true
86
+ homepage: http://github.com/geraldb/ostructer
87
+ licenses: []
88
+
89
+ post_install_message:
90
+ rdoc_options:
91
+ - --main
92
+ - README.rdoc
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project: ostructer
116
+ rubygems_version: 1.3.7
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Ostructer - Open Struct Builder
120
+ test_files:
121
+ - test/test_customer.rb
122
+ - test/test_config.rb
123
+ - test/test_misc.rb