jordi-xml_struct 0.1.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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 Jordi Bunster <jordi@bunster.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,34 @@
1
+ Documentation pending. For now:
2
+
3
+ # Example usage:
4
+
5
+ <recipe name="bread" prep_time="5 mins" cook_time="3 hours">
6
+ <title>Basic bread</title>
7
+ <ingredient amount="8" unit="dL">Flour</ingredient>
8
+ <ingredient amount="10" unit="grams">Yeast</ingredient>
9
+ <ingredient amount="4" unit="dL" state="warm">Water</ingredient>
10
+ <ingredient amount="1" unit="teaspoon">Salt</ingredient>
11
+ <instructions easy="yes" hard="false">
12
+ <step>Mix all ingredients together.</step>
13
+ <step>Knead thoroughly.</step>
14
+ <step>Cover with a cloth, and leave for one hour in warm room.</step>
15
+ <step>Knead again.</step>
16
+ <step>Place in a bread baking tin.</step>
17
+ <step>Cover with a cloth, and leave for one hour in warm room.</step>
18
+ <step>Bake in the oven at 180(degrees)C for 30 minutes.</step>
19
+ </instructions>
20
+ </recipe>
21
+
22
+ recipe = XMLStruct.new recipe_xml_shown_above
23
+
24
+ recipe.name => "bread"
25
+ recipe.title == "Basic bread" => true
26
+
27
+ recipe.ingredients.is_a?(Array) => true
28
+ recipe.ingredients.first.amount => 8
29
+
30
+ recipe.instructions.easy? => true
31
+
32
+ recipe.instructions.first.upcase => "MIX ALL INGREDIENTS TOGETHER."
33
+ recipe.instructions.steps.size => 7
34
+
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Run the unit tests.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'XMLStruct'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include 'README.markdown'
21
+ rdoc.rdoc_files.include 'lib/**/*.rb'
22
+ end
23
+
24
+ desc 'Measures test coverage using rcov'
25
+ task :rcov do
26
+ rm_f 'coverage'
27
+ rm_f 'coverage.data'
28
+
29
+ rcov = %{ rcov --aggregate coverage.data --text-summary
30
+ --include lib
31
+ --exclude /Library/Ruby
32
+ }.strip!.gsub! /\s+/, ' '
33
+
34
+ system("#{rcov} --html #{Dir.glob('test/**/*_test.rb').join(' ')}")
35
+ system('open coverage/index.html') if PLATFORM['darwin']
36
+ end
data/WHATSNEW ADDED
@@ -0,0 +1,3 @@
1
+ * 0.1.1 (2008-10-10):
2
+ - First "release" ;)
3
+
@@ -0,0 +1 @@
1
+ require 'xml_struct'
data/lib/xml_struct.rb ADDED
@@ -0,0 +1,147 @@
1
+ require 'rubygems'
2
+ require 'activesupport'
3
+ require 'rexml/document'
4
+
5
+ class XMLStruct
6
+
7
+ include Comparable
8
+
9
+ # Returns an XMLStruct object
10
+ def self.new(duck)
11
+ duck.is_a?(REXML::Element) ? super : new(REXML::Document.new(duck).root)
12
+ end
13
+
14
+ # Takes any REXML::Element object, and converts it recursively into
15
+ # the corresponding tree of XMLStruct objects
16
+ def initialize(raw)
17
+ @attributes, @children, @raw = {}, {}, raw
18
+ @value = __get_value_from_raw_element @raw
19
+
20
+ @raw.each_element { |el| __set_child el.name, self.class.new(el) }
21
+ @raw.attributes.each { |n, v| __set_attribute n, v }
22
+ end
23
+
24
+ # An XMLStruct is blank when it has a blank value, no child elements,
25
+ # and no attributes. For example:
26
+ #
27
+ # <blank_element></blank_element>
28
+ def blank?
29
+ @value.blank? && @children.blank? && @attributes.blank?
30
+ end
31
+
32
+ # XMLStruct objects are compared according to their values
33
+ def <=>(other)
34
+ @value <=> other
35
+ end
36
+
37
+ # Array-notation access to elements and attributes. It comes handy when
38
+ # the element or attribute you need to reach is not reachable via dot
39
+ # notation (because it's not a valid method name, or because the method
40
+ # exists, such as 'id' or 'class').
41
+ #
42
+ # It also supports hash keys, which are useful to reach attributes named
43
+ # the same as elements in the same level (which are otherwise prioritized)
44
+ #
45
+ # All of this is a lot easier to exampling by example:
46
+ #
47
+ # <article id="main_article" author="j-random">
48
+ # <author>J. Random Hacker</author>
49
+ # </article>
50
+ #
51
+ # article.id => 9314390 # Object#id gets called
52
+ # article[:id] => "main_article" # id attribute
53
+ # article[:author] => <XMLStruct ...> # <author> element
54
+ # article[:attr => 'author'] => "j-random" # author attribute
55
+ #
56
+ # Valid keys for the hash notation in the example above are :attr,
57
+ # :attribute, :child, and :element.
58
+ def [](name)
59
+ unless name.is_a? Hash
60
+ return @children[name.to_sym] if @children[name.to_sym]
61
+ return @attributes[name.to_sym] if @attributes[name.to_sym]
62
+ end
63
+
64
+ raise 'one and only one key allowed' if name.size != 1
65
+
66
+ case (param = name.keys.first.to_sym)
67
+ when :element : @children[name.values.first.to_sym]
68
+ when :child : @children[name.values.first.to_sym]
69
+ when :attr : @attributes[name.values.first.to_sym]
70
+ when :attribute : @attributes[name.values.first.to_sym]
71
+ else raise %{ Invalid key :#{param.to_s}.
72
+ Use one of :element, :child, :attr, or :attribute }.squish!
73
+ end
74
+ end
75
+
76
+ def method_missing(method, *args, &block) # :nodoc:
77
+
78
+ if method.to_s.match(/\?$/) && args.empty? && block.nil?
79
+ boolish = send(method.to_s.chomp('?').to_sym).to_s
80
+
81
+ %w[ true yes t y ].include? boolish.downcase
82
+
83
+ elsif @value.blank? && (@children.size == 1) &&
84
+ (single_child = @children.values.first).respond_to?(method)
85
+
86
+ single_child.send method, *args, &block
87
+ else
88
+ @value.send method, *args, &block
89
+ end
90
+ end
91
+
92
+ def inspect # :nodoc:
93
+ %{ #<#{self.class.to_s} value=#{@value.inspect} (#{@value.class.to_s})
94
+ attributes=[#{@attributes.keys.map(&:to_s).join(', ') }]
95
+ children=[#{@children.keys.map(&:to_s).join(', ') }]> }.squish
96
+ end
97
+
98
+ private
99
+
100
+ def __set_child(name, element) # :nodoc:
101
+ key = name.to_sym
102
+
103
+ @children[key] = if @children[key]
104
+
105
+ unless respond_to?((plural_key = key.to_s.pluralize).to_sym)
106
+ instance_eval %{ def #{plural_key}; @children[:#{key.to_s}]; end }
107
+ end
108
+
109
+ @children[key] = [ @children[key] ] unless @children[key].is_a? Array
110
+ @children[key] << element
111
+ else
112
+ unless respond_to? key
113
+ instance_eval %{ def #{key.to_s}; @children[:#{key.to_s}]; end }
114
+ end
115
+
116
+ element
117
+ end
118
+ end
119
+
120
+ def __set_attribute(name, attribute) # :nodoc:
121
+ obj = __get_object_from_string(attribute)
122
+ @attributes[(key = name.to_sym)] = obj.is_a?(String) ? obj.squish : obj
123
+
124
+ unless respond_to? key
125
+ instance_eval %{ def #{key.to_s}; @attributes[:#{key.to_s}]; end }
126
+ end
127
+ end
128
+
129
+ def __get_value_from_raw_element(raw) # :nodoc:
130
+ str = case
131
+ when raw.has_text? && !raw.text.blank? : raw.text
132
+ else (raw.cdatas.first.to_s rescue '')
133
+ end
134
+
135
+ __get_object_from_string str
136
+ end
137
+
138
+ def __get_object_from_string(str) # :nodoc:
139
+ case
140
+ when str.blank? : nil
141
+ when str.match(/[a-zA-Z]/) : str
142
+ when str.match(/^[+-]?\d+$/) : str.to_i
143
+ when str.match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/) : str.to_f
144
+ else str
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,63 @@
1
+ <!-- Invalid method name test -->
2
+ <lorem id="root" name="ipsum dolor sit amet" _tempor="incididunt">
3
+
4
+ <_minim>veniam</_minim> <!-- Invalid method name test -->
5
+
6
+ <!-- unambiguous element/attribute access test -->
7
+ <sed do="eiusmod attributus">
8
+ <do>eiusmod elementus</do>
9
+ </sed>
10
+
11
+ <!-- Blank test -->
12
+ <ipsum></ipsum>
13
+ <dolor foo="bar"></dolor>
14
+ <sit><amet></amet></sit>
15
+ <ut>quis</ut>
16
+
17
+ <!-- To test the array folding feature, as well as the pluralization.
18
+ The attributes help test float, integer, and boolean recognition,
19
+ as well as method name clashing behaviour (id) -->
20
+ <consectetur id="1" capacity="2.13" enabled="yes" />
21
+ <consectetur id="2" capacity="-1.9" enabled="No" />
22
+ <consectetur id="3" capacity="13.0" enabled="True" />
23
+ <consectetur id="4" capacity="0.00" enabled="false" />
24
+
25
+ <!-- Test boolean recognition -->
26
+ <brutus>true</brutus>
27
+
28
+ <consecteturs> <!-- To test that we can still reach this, even though
29
+ the array of singular ones above will trap the call
30
+ to this when using dot notation -->
31
+ <culpa>officia</culpa>
32
+ </consecteturs>
33
+
34
+ <cupidatats> <!-- Test for the empty element == single array child
35
+ scenario, as well as date recognition -->
36
+ <cupidatat activated_at="2903-02-12">Aliqua</cupidatat>
37
+ <cupidatat activated_at="2903-02-13">Occaecat</cupidatat>
38
+ <cupidatat activated_at="2904-10-18">Cupidatat</cupidatat>
39
+ <cupidatat activated_at="2905-03-21">Mollit</cupidatat>
40
+ </cupidatats>
41
+
42
+ <!-- Non-empty element, should not be array proxy -->
43
+ <voluptate>
44
+ velit
45
+ <esse>cillum</esse>
46
+ <esse>cillum dolore</esse>
47
+ </voluptate>
48
+
49
+ <!-- Test for both text value and CDATA value -->
50
+ <ullamco>Laboris<![CDATA[nisi]]></ullamco>
51
+
52
+ <!-- Test for multiple CDATA values -->
53
+ <deserunt><![CDATA[mollit]]><![CDATA[anim]]></deserunt>
54
+
55
+ <!-- Test for whitespace preservation / squishing -->
56
+ <irure metadata="
57
+ dolor ">
58
+ reprehenderit </irure>
59
+
60
+ <!-- CDATA whitespace preservation test -->
61
+ <![CDATA[ foo
62
+ ]]>
63
+ </lorem>
@@ -0,0 +1,37 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+
4
+ begin
5
+ require 'redgreen'
6
+ rescue LoadError
7
+ puts "Install the 'redgreen' gem to get color output"
8
+ end
9
+
10
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'xml_struct')
11
+
12
+ def xml_file(name_symbol)
13
+ File.open File.join(File.dirname(__FILE__), 'samples',
14
+ "#{name_symbol.to_s}.xml")
15
+ end
16
+
17
+ require 'digest/md5'
18
+ { :lorem => '6dc88e269000db3be599fca3367e2aa5' }.each do |file_key, md5|
19
+
20
+ unless Digest::MD5.hexdigest(xml_file(file_key).read) == md5
21
+ raise "Sample test file #{file_key.to_s}.xml doesn't match expected MD5"
22
+ end
23
+ end
24
+
25
+ class Test::Unit::TestCase
26
+ def self.should(do_stuff_that_is_true, &block)
27
+
28
+ object = to_s.split('Test').first
29
+ method = "test that #{object} should #{do_stuff_that_is_true}"
30
+
31
+ define_method(method.intern) { assert block.bind(self).call }
32
+ end
33
+
34
+ def debug
35
+ require 'ruby-debug'; debugger
36
+ end
37
+ end
@@ -0,0 +1,115 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class XMLStructTest < Test::Unit::TestCase
4
+
5
+ include RubyProf::Test if defined? RubyProf::Test
6
+
7
+ def setup
8
+ @lorem = XMLStruct.new xml_file(:lorem)
9
+ end
10
+
11
+ should 'be an XMLStruct' do
12
+ @lorem.is_a? XMLStruct
13
+ end
14
+
15
+ should 'be blank if devoid of children, attributes and value' do
16
+ @lorem.ipsum.blank?
17
+ end
18
+
19
+ should 'not be blank when value, children, or attributes are present' do
20
+ [ @lorem.dolor, @lorem.sit, @lorem.ut ].all? { |xr| not xr.blank? }
21
+ end
22
+
23
+ should 'allow access to attributes named like invalid methods' do
24
+ @lorem['_tempor'] == 'incididunt'
25
+ end
26
+
27
+ should 'allow access to elements named like invalid methods' do
28
+ @lorem['_minim'] == 'veniam'
29
+ end
30
+
31
+ should 'provide unambiguous access to elements named like attributes' do
32
+ @lorem.sed[:element => 'do'] == 'eiusmod elementus'
33
+ end
34
+
35
+ should 'provide unambiguous access to attributes named like elements' do
36
+ @lorem.sed[:attribute => 'do'] == 'eiusmod attributus'
37
+ end
38
+
39
+ should 'return elements first when using dot notation' do
40
+ @lorem.sed.do == @lorem.sed[:element => 'do']
41
+ end
42
+
43
+ should 'return elements first when using array notation and string key' do
44
+ @lorem.sed['do'] == @lorem.sed[:element => 'do']
45
+ end
46
+
47
+ should 'return elements first when using array notation and symbol key' do
48
+ @lorem.sed[:do] == @lorem.sed[:element => 'do']
49
+ end
50
+
51
+ should 'raise exception when unkown keys are used in hash-in-array mode' do
52
+ (@lorem[:foo => 'bar']; false) rescue true
53
+ end
54
+
55
+ should 'group multiple parallel namesake elements in arrays' do
56
+ @lorem.consectetur.is_a? Array
57
+ end
58
+
59
+ should 'make auto-grouped arrays accessible by their plural form' do
60
+ @lorem.consecteturs.equal? @lorem.consectetur
61
+ end
62
+
63
+ should 'allow explicit access to elements named like plural arrays' do
64
+ not @lorem.consecteturs.equal? @lorem[:element => 'consecteturs']
65
+ end
66
+
67
+ should 'convert integer-looking attribute strings to integers' do
68
+ @lorem.consecteturs.all? { |c| c[:attr => 'id'].is_a? Numeric }
69
+ end
70
+
71
+ should 'convert float-looking attribute strings to floats' do
72
+ @lorem.consecteturs.all? { |c| c.capacity.is_a? Float }
73
+ end
74
+
75
+ should 'convert bool-looking attribute strings to bools when asked' do
76
+ @lorem.consecteturs.all? { |c| c.enabled?.equal? !!(c.enabled?) }
77
+ end
78
+
79
+ should 'convert to bool correctly when asked' do
80
+ @lorem.consecteturs.first.enabled? == true &&
81
+ @lorem.consecteturs.last.enabled? == false
82
+ end
83
+
84
+ should 'pass forth methods to single array child when empty valued' do
85
+ @lorem.cupidatats.slice(0).equal? @lorem.cupidatats.cupidatat.slice(0)
86
+ end
87
+
88
+ should 'not pass methods to single array child if not empty valued' do
89
+ not @lorem.voluptate.slice(0).equal? @lorem.voluptate.esse.slice(0)
90
+ end
91
+
92
+ should 'be valued as its text when text first and CDATA exist' do
93
+ @lorem.ullamco == 'Laboris'
94
+ end
95
+
96
+ should 'have the value of its first CDATA when multiple exist' do
97
+ @lorem.deserunt == 'mollit'
98
+ end
99
+
100
+ should 'squish whitespace in string attribute values' do
101
+ @lorem.irure.metadata == 'dolor'
102
+ end
103
+
104
+ should 'not squish whitespace in string element values' do
105
+ @lorem.irure == " \n\t\t\treprehenderit "
106
+ end
107
+
108
+ should 'not squish whitespace in CDATA values' do
109
+ @lorem == "\t foo\n"
110
+ end
111
+
112
+ should 'have a working inspect function' do
113
+ (@lorem.inspect.to_s.is_a? String; true) rescue false
114
+ end
115
+ end
@@ -0,0 +1,38 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'xml_struct'
3
+ s.version = '0.1.1'
4
+ s.date = '2008-10-09'
5
+
6
+ s.author = 'Jordi Bunster'
7
+ s.email = 'jordi@bunster.org'
8
+ s.homepage = 'http://github.com/jordi/xml_struct'
9
+
10
+ s.add_dependency 'activesupport', '>= 2.1.1'
11
+
12
+ s.summary = "The Rubyista's way to do quick XML sit-ups"
13
+ s.description = %{ This is a library for reading (not writing) XML. It is
14
+ particularly suited for cases where one is dealing with
15
+ small documents of a known structure.
16
+
17
+ It is slow and not devoid of caveats, but has a very
18
+ pleasant, Ruby-like syntax. }.strip!.gsub! /\s+/, ' '
19
+
20
+ s.test_files = %w[ test
21
+ test/samples
22
+ test/samples/lorem.xml
23
+ test/test_helper.rb
24
+ test/xml_struct_test.rb ]
25
+
26
+ s.files = %w[ MIT-LICENSE
27
+ README.markdown
28
+ Rakefile
29
+ WHATSNEW
30
+ lib
31
+ lib/jordi-xml_struct.rb
32
+ lib/xml_struct.rb
33
+ xml_struct.gemspec ]
34
+
35
+ s.has_rdoc = true
36
+ s.extra_rdoc_files = %w[ README.markdown ]
37
+ s.rdoc_options = %w[ --main README.markdown ]
38
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jordi-xml_struct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Jordi Bunster
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-09 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.1.1
23
+ version:
24
+ description: This is a library for reading (not writing) XML. It is particularly suited for cases where one is dealing with small documents of a known structure. It is slow and not devoid of caveats, but has a very pleasant, Ruby-like syntax.
25
+ email: jordi@bunster.org
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README.markdown
32
+ files:
33
+ - MIT-LICENSE
34
+ - README.markdown
35
+ - Rakefile
36
+ - WHATSNEW
37
+ - lib
38
+ - lib/jordi-xml_struct.rb
39
+ - lib/xml_struct.rb
40
+ - xml_struct.gemspec
41
+ has_rdoc: true
42
+ homepage: http://github.com/jordi/xml_struct
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --main
46
+ - README.markdown
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: The Rubyista's way to do quick XML sit-ups
68
+ test_files:
69
+ - test
70
+ - test/samples
71
+ - test/samples/lorem.xml
72
+ - test/test_helper.rb
73
+ - test/xml_struct_test.rb