jordi-xml_struct 0.1.1

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