ostructer 0.1

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,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