jordi-xml_struct 0.2.1 → 0.9.0

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.
Files changed (43) hide show
  1. data/README.rdoc +152 -0
  2. data/WHATSNEW +10 -0
  3. data/lib/xml_struct/adapters/hpricot.rb +47 -0
  4. data/lib/xml_struct/adapters/rexml.rb +34 -0
  5. data/lib/xml_struct/array_notation.rb +44 -0
  6. data/lib/xml_struct/blankish_slate.rb +1 -1
  7. data/lib/xml_struct/collection_proxy.rb +7 -3
  8. data/lib/xml_struct/default_adapter.rb +15 -0
  9. data/lib/xml_struct/method_missing_dispatchers.rb +43 -0
  10. data/lib/xml_struct/string.rb +6 -2
  11. data/lib/xml_struct.rb +31 -52
  12. data/xml_struct.gemspec +38 -0
  13. metadata +17 -49
  14. data/README.markdown +0 -151
  15. data/Rakefile +0 -36
  16. data/lib/xml_struct/common_behaviours.rb +0 -53
  17. data/test/samples/lorem.xml +0 -63
  18. data/test/samples/recipe.xml +0 -16
  19. data/test/samples/weird_characters.xml +0 -2
  20. data/test/test_helper.rb +0 -39
  21. data/test/vendor/test-spec/README +0 -378
  22. data/test/vendor/test-spec/ROADMAP +0 -1
  23. data/test/vendor/test-spec/Rakefile +0 -146
  24. data/test/vendor/test-spec/SPECS +0 -161
  25. data/test/vendor/test-spec/TODO +0 -2
  26. data/test/vendor/test-spec/bin/specrb +0 -107
  27. data/test/vendor/test-spec/examples/stack.rb +0 -38
  28. data/test/vendor/test-spec/examples/stack_spec.rb +0 -119
  29. data/test/vendor/test-spec/lib/test/spec/dox.rb +0 -148
  30. data/test/vendor/test-spec/lib/test/spec/rdox.rb +0 -25
  31. data/test/vendor/test-spec/lib/test/spec/should-output.rb +0 -49
  32. data/test/vendor/test-spec/lib/test/spec/version.rb +0 -8
  33. data/test/vendor/test-spec/lib/test/spec.rb +0 -660
  34. data/test/vendor/test-spec/test/spec_dox.rb +0 -39
  35. data/test/vendor/test-spec/test/spec_flexmock.rb +0 -209
  36. data/test/vendor/test-spec/test/spec_mocha.rb +0 -104
  37. data/test/vendor/test-spec/test/spec_nestedcontexts.rb +0 -26
  38. data/test/vendor/test-spec/test/spec_new_style.rb +0 -80
  39. data/test/vendor/test-spec/test/spec_should-output.rb +0 -26
  40. data/test/vendor/test-spec/test/spec_testspec.rb +0 -699
  41. data/test/vendor/test-spec/test/spec_testspec_order.rb +0 -26
  42. data/test/vendor/test-spec/test/test_testunit.rb +0 -22
  43. data/test/xml_struct_test.rb +0 -185
data/README.rdoc ADDED
@@ -0,0 +1,152 @@
1
+ = XMLStruct
2
+
3
+ (This is inspired by Python's +xml_objectify+)
4
+
5
+ XMLStruct attempts to make the accessing of small, well-formed XML structures
6
+ convenient, by using dot notation to represent both attributes and child
7
+ elements whenever possible.
8
+
9
+ XML parsing libraries (in general) have interfaces that are useful when
10
+ one is using XML for its intended purpose, but cumbersome when one always
11
+ sends the same XML structure, and always process all of it in the same
12
+ way. This one aims to be a bit different.
13
+
14
+ == Example usage
15
+
16
+ <recipe name="bread" prep_time="5 mins" cook_time="3 hours">
17
+ <title>Basic bread</title>
18
+ <ingredient amount="8" unit="dL">Flour</ingredient>
19
+ <ingredient amount="10" unit="grams">Yeast</ingredient>
20
+ <ingredient amount="4" unit="dL" state="warm">Water</ingredient>
21
+ <ingredient amount="1" unit="teaspoon">Salt</ingredient>
22
+ <instructions easy="yes" hard="false">
23
+ <step>Mix all ingredients together.</step>
24
+ <step>Knead thoroughly.</step>
25
+ <step>Cover with a cloth, and leave for one hour in warm room.</step>
26
+ <step>Knead again.</step>
27
+ <step>Place in a bread baking tin.</step>
28
+ <step>Cover with a cloth, and leave for one hour in warm room.</step>
29
+ <step>Bake in the oven at 180(degrees)C for 30 minutes.</step>
30
+ </instructions>
31
+ </recipe>
32
+
33
+ require 'xml_struct'
34
+ recipe = XMLStruct.new io_with_recipe_xml_shown_above
35
+
36
+ recipe.name => "bread"
37
+ recipe.title => "Basic bread"
38
+
39
+ recipe.ingredients.is_a?(Array) => true
40
+ recipe.ingredients.first.amount => "8" # Not a Fixnum. Too hard. :(
41
+
42
+ recipe.instructions.easy? => true
43
+
44
+ recipe.instructions.first.upcase => "MIX ALL INGREDIENTS TOGETHER."
45
+ recipe.instructions.steps.size => 7
46
+
47
+ == Installation instructions
48
+
49
+ sudo gem install jordi-xml_struct --source http://gems.github.com
50
+
51
+ == Motivation
52
+
53
+ XML is an *extensible* markup language. It is extensible because it is
54
+ meant to define markup languages for *any* type of document, so new tags
55
+ are needed depending on the problem domain.
56
+
57
+ Sometimes, however, XML ends up being used to solve a much simpler problem:
58
+ the issue of passing a data-structure over the network, and/or between two
59
+ different languages. Tools like +JSON+ or +YAML+ are a much better fit for
60
+ this kind of job, but one doesn't always have that luxury.
61
+
62
+ == Caveats
63
+
64
+ The dot notation is used as follows. For the given file:
65
+
66
+ <outer id="root" name="foo">
67
+ <name>Outer Element</name>
68
+ </outer>
69
+
70
+ +outer.name+ is the +name+ *element*. Child elements are always looked up
71
+ first, then attributes. To access the attribute in the case of ambiguity,
72
+ use outer[:attr => 'name'].
73
+
74
+ +outer.id+ is really Object#id, because all of the object methods are
75
+ preserved (this is on purpose). To access the attribute +id+, use
76
+ outer[:attr => 'id'], or outer['id'] since there's no element/attribute
77
+ ambiguity.
78
+
79
+ == Features & Problems
80
+
81
+ === Collection auto-folding
82
+
83
+ Similar to XmlSimple, XMLStruct folds same named elements at the same
84
+ level. For example:
85
+
86
+ <student>
87
+ <name>Bob</name>
88
+ <course>Math</course>
89
+ <course>Biology</course>
90
+ </student>
91
+
92
+ student = XMLStruct.new(xml_file)
93
+
94
+ student.course.is_a? Array => true
95
+ student.course.first == 'Math' => true
96
+ student.course.last == 'Biology => true
97
+
98
+ === Collection pluralization
99
+
100
+ With the same file from the +Collection auto-folding+ section above, you
101
+ also get this (courtesy of +ActiveSupport+'s +Inflector+):
102
+
103
+ student.courses.first == student.course.first => true
104
+
105
+ === Collection proxy
106
+
107
+ Sometimes, collections are expressed with a container element in XML:
108
+
109
+ <student>
110
+ <name>Bob</name>
111
+ <courses>
112
+ <course>Math</course>
113
+ <course>Biology</course>
114
+ </courses>
115
+ </student>
116
+
117
+ In this case, since the container element +courses+ has no text element
118
+ of its own, and it only has elements of one name under it, it delegates
119
+ all methods it doesn't contain to the collection below, so you get:
120
+
121
+ student.courses.collect { |c| c.downcase.to_sym } => [:math, :biology]
122
+
123
+ === Question mark notation
124
+
125
+ Strings that look like booleans are "booleanized" if called by their
126
+ question mark names (such as +enabled?+)
127
+
128
+ === Adapters
129
+
130
+ XMLStruct supports different adapters to do the actual XML parsing. It ships
131
+ with +REXML+ and +Hpricot+ adapters. If +Hpricot+ is detected it gets used,
132
+ otherwise +REXML+ is used as a fallback.
133
+
134
+ === Recursive
135
+
136
+ The design of the adapters assumes parsing of the objects recursively. Deep
137
+ files are bound to throw +SystemStackError+, but for the kinds of files I
138
+ need to read, things are working fine so far. In any case, stream parsing
139
+ is on the TODO list.
140
+
141
+ === Incomplete
142
+
143
+ It most likely doesn't work with a ton of features of complex XML files. I
144
+ will always try to accomodate those, as long as they don't make the basic
145
+ usage more complex. As usual, patches welcome.
146
+
147
+ == Legal
148
+
149
+ Copyright (c) 2008 Jordi Bunster, released under the MIT license
150
+
151
+
152
+
data/WHATSNEW CHANGED
@@ -1,3 +1,13 @@
1
+ * 0.9.0 (2008-10-15):
2
+ - Added support for plug-able adapters
3
+ - Backported REXML code as an adapter, added Hpricot adapter
4
+ - Performance: XMLStruct now decorates objects lazily
5
+ - Performance: XMLStruct uses the Hpricot adapter if possible, otherwise
6
+ REXML as a fallback
7
+ - API Change: XMLStruct.new is mostly delegated to the adapter, and both
8
+ included adapters behave the same: a String is considered to be
9
+ XML data, anything else is probed for #read and then #to_s
10
+
1
11
  * 0.2.1 (2008-10-13):
2
12
  - Fixed a bug where attributes with dashes would crash the party
3
13
 
@@ -0,0 +1,47 @@
1
+ module XMLStruct::Adapters::Hpricot
2
+
3
+ # Can take a String of XML data, or anything that responds to
4
+ # either +read+ or +to_s+.
5
+ def self.new(duck)
6
+ case
7
+ when duck.is_a?(::Hpricot::Elem) : Element.new(duck)
8
+ when duck.is_a?(::String) : new(::Hpricot::XML(duck).root)
9
+ when duck.respond_to?(:read) : new(duck.read)
10
+ when duck.respond_to?(:to_s) : new(duck.to_s)
11
+ else raise "Don't know how to deal with '#{duck.class}' object"
12
+ end
13
+ end
14
+
15
+ private ##################################################################
16
+
17
+ class Element # :nodoc:
18
+ attr_reader :raw, :name, :value, :attributes, :children
19
+
20
+ def text_value(raw)
21
+ raw.children.select do |e|
22
+ (e.class == ::Hpricot::Text) && !e.to_s.blank?
23
+ end.join.to_s
24
+ end
25
+
26
+ def cdata_value(raw)
27
+ raw.children.select do |e|
28
+ (e.class == ::Hpricot::CData) && !e.to_s.blank?
29
+ end.first.to_s
30
+ end
31
+
32
+ def initialize(xml)
33
+ @raw, @name, @attributes, @children = xml, xml.name, {}, []
34
+
35
+ @attributes = xml.attributes
36
+ xml.children.select { |e| e.elem? }.each do |e|
37
+ @children << self.class.new(e)
38
+ end
39
+
40
+ @value = case
41
+ when (not text_value(@raw).blank?) : text_value(@raw)
42
+ when (not cdata_value(@raw).blank?) : cdata_value(@raw)
43
+ else ''
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ module XMLStruct::Adapters::REXML
2
+ require 'rexml/document'
3
+
4
+ # Can take a String of XML data, or anything that responds to
5
+ # either +read+ or +to_s+.
6
+ def self.new(duck)
7
+ case
8
+ when duck.is_a?(::REXML::Element) : Element.new(duck)
9
+ when duck.is_a?(::String) : new(::REXML::Document.new(duck).root)
10
+ when duck.respond_to?(:read) : new(duck.read)
11
+ when duck.respond_to?(:to_s) : new(duck.to_s)
12
+ else raise "Don't know how to deal with '#{duck.class}' object"
13
+ end
14
+ end
15
+
16
+ private ##################################################################
17
+
18
+ class Element # :nodoc:
19
+ attr_reader :raw, :name, :value, :attributes, :children
20
+
21
+ def initialize(xml)
22
+ @raw, @name, @attributes, @children = xml, xml.name, {}, []
23
+
24
+ @attributes = xml.attributes
25
+ xml.each_element { |e| @children << self.class.new(e) }
26
+
27
+ @value = case
28
+ when (not xml.text.blank?) : xml.text.to_s
29
+ when (xml.cdatas.size >= 1) : xml.cdatas.first.to_s
30
+ else ''
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ module XMLStruct::ArrayNotation
2
+ # Array-bracket (+[]+) notation access to elements and attributes. Use
3
+ # when the element or attribute you need to reach is not reachable via dot
4
+ # notation (because it's not a valid method name, or because the method
5
+ # exists, such as +id+ or +class+).
6
+ #
7
+ # It also supports a hash key, which is used to reach attributes named
8
+ # the same as elements in the same depth level (which otherwise go first)
9
+ #
10
+ # All of this is a lot easier to explain by example:
11
+ #
12
+ # <article id="main_article" author="j-random">
13
+ # <author>J. Random Hacker</author>
14
+ # </article>
15
+ #
16
+ # article.id => 9314390 # Object#id
17
+ # article[:id] => "main_article" # id attribute
18
+ # article[:author] => "J. Random Hacker" # <author> element
19
+ # article[:attr => 'author'] => "j-random" # author attribute
20
+ #
21
+ # Valid keys for the hash notation in the example above are +:attr+,
22
+ # +:attribute+, +:child+, and +:element+.
23
+ def [](name)
24
+ return @__target[name] if @__target && name.is_a?(Numeric)
25
+
26
+ unless name.is_a? Hash
27
+ key = name.to_sym
28
+
29
+ return @__children[key] if @__children.has_key?(key)
30
+ return @__attributes[key] if @__attributes.has_key?(key)
31
+ end
32
+
33
+ raise 'one and only one key allowed' if name.size != 1
34
+
35
+ case (param = name.keys.first.to_sym)
36
+ when :element : @__children[name.values.first.to_sym]
37
+ when :child : @__children[name.values.first.to_sym]
38
+ when :attr : @__attributes[name.values.first.to_sym]
39
+ when :attribute : @__attributes[name.values.first.to_sym]
40
+ else raise %{ Invalid key :#{param.to_s}.
41
+ Use one of :element, :child, :attr, or :attribute }.squish!
42
+ end
43
+ end
44
+ end
@@ -1,4 +1,4 @@
1
- class XMLStruct::BlankishSlate
1
+ class XMLStruct::BlankishSlate # :nodoc:
2
2
 
3
3
  instance_methods.each do |m|
4
4
  undef_method m unless m =~ /^__/ ||
@@ -1,10 +1,14 @@
1
- class XMLStruct::CollectionProxy < XMLStruct::BlankishSlate
1
+ class XMLStruct::CollectionProxy < XMLStruct::BlankishSlate # :nodoc:
2
2
  def initialize(target)
3
3
  @__children, @__attributes, @__target = {}, {}, target
4
4
  end
5
5
 
6
+ private ##################################################################
7
+
6
8
  def method_missing(m, *a, &b) # :nodoc:
7
- answer = __question_answer(m, *a, &b)
8
- answer.nil? ? (@__target.__send__(m, *a, &b) if @__target) : answer
9
+ dp = __question_dispatch(m, *a, &b)
10
+ dp = __dot_notation_dispatch(m, *a, &b) if dp.nil?
11
+ dp = @__target.__send__(m, *a, &b) if @__target.respond_to?(m) && dp.nil?
12
+ dp
9
13
  end
10
14
  end
@@ -0,0 +1,15 @@
1
+ module XMLStruct # :nodoc:
2
+ module Adapters # :nodoc:
3
+ ADAPTERS_PATH = File.join(File.dirname(__FILE__), 'adapters')
4
+
5
+ Default = begin
6
+ require 'hpricot'
7
+ require File.join(ADAPTERS_PATH, 'hpricot')
8
+ Hpricot
9
+ rescue LoadError
10
+ require File.join(ADAPTERS_PATH, 'rexml')
11
+ REXML
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,43 @@
1
+ module XMLStruct::MethodMissingDispatchers # :nodoc:
2
+
3
+ private ##################################################################
4
+
5
+ def __question_dispatch(meth, *args, &block)
6
+ return unless meth.to_s.match(/\?$/) && args.empty? && block.nil?
7
+
8
+ method_sans_question = meth.to_s.chomp('?').to_sym
9
+
10
+ if boolish = __send__(method_sans_question).downcase
11
+ bool = case
12
+ when %w[ true yes t y ].include?(boolish) : true
13
+ when %w[ false no f n ].include?(boolish) : false
14
+ else nil
15
+ end
16
+
17
+ unless bool.nil? # Fun, eh?
18
+ instance_eval %{ def #{meth}; #{bool ? 'true' : 'false'}; end }
19
+ end
20
+
21
+ bool
22
+ end
23
+ end
24
+
25
+ def __dot_notation_dispatch(meth, *args, &block)
26
+ return unless args.empty? && block.nil?
27
+
28
+ if @__children.has_key?(singular = meth.to_s.singularize.to_sym) &&
29
+ @__children[singular].is_a?(Array)
30
+
31
+ instance_eval %{ def #{meth}; @__children[%s|#{singular}|]; end }
32
+ @__children[singular]
33
+
34
+ elsif @__children.has_key?(meth)
35
+ instance_eval %{ def #{meth}; @__children[%s|#{meth}|]; end }
36
+ @__children[meth]
37
+
38
+ elsif @__attributes.has_key?(meth)
39
+ instance_eval %{ def #{meth}; @__attributes[%s|#{meth}|]; end }
40
+ @__attributes[meth]
41
+ end
42
+ end
43
+ end
@@ -9,7 +9,7 @@ module XMLStruct::String
9
9
  # and returns accordingly. If not, just returns the string.
10
10
  def rb
11
11
  @__rb ||= case
12
- when (self !~ /\S/) : nil
12
+ when (self !~ /\S/) : ''
13
13
  when match(/[a-zA-Z]/) : ::String.new(self)
14
14
  when match(/^[+-]?\d+$/) : self.to_i
15
15
  when match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/) : self.to_f
@@ -25,7 +25,11 @@ module XMLStruct::String
25
25
  (self !~ /\S/) && @__children.blank? && @__attributes.blank?
26
26
  end
27
27
 
28
+ private ##################################################################
29
+
28
30
  def method_missing(m, *a, &b) # :nodoc:
29
- __question_answer(m, *a, &b)
31
+ dp = __question_dispatch(m, *a, &b)
32
+ dp = __dot_notation_dispatch(m, *a, &b) if dp.nil?
33
+ dp
30
34
  end
31
35
  end
data/lib/xml_struct.rb CHANGED
@@ -1,53 +1,59 @@
1
1
  require 'rubygems'
2
2
  require 'activesupport'
3
- require 'rexml/document'
4
3
 
5
- module XMLStruct; end
4
+ module XMLStruct
5
+
6
+ unless defined?(BASE_DIR) # Slow call
7
+ BASE_DIR = File.join(File.dirname(__FILE__), 'xml_struct')
8
+ end
6
9
 
7
- require File.join(File.dirname(__FILE__), 'xml_struct', 'blankish_slate')
8
- require File.join(File.dirname(__FILE__), 'xml_struct', 'collection_proxy')
9
- require File.join(File.dirname(__FILE__), 'xml_struct', 'string')
10
- require File.join(File.dirname(__FILE__), 'xml_struct', 'common_behaviours')
10
+ require File.join(BASE_DIR, 'default_adapter')
11
+ require File.join(BASE_DIR, 'method_missing_dispatchers')
12
+ require File.join(BASE_DIR, 'array_notation')
13
+ require File.join(BASE_DIR, 'blankish_slate')
14
+ require File.join(BASE_DIR, 'collection_proxy')
15
+ require File.join(BASE_DIR, 'string')
11
16
 
12
- module XMLStruct
17
+ def self.adapter=(adapter_module)
18
+ @adapter = adapter_module
19
+ end
20
+
21
+ def self.adapter
22
+ @adapter ||= Adapters::Default
23
+ end
13
24
 
14
25
  # Returns a String or Array object representing the given XML, decorated
15
26
  # with methods to access attributes and/or child elements.
16
27
  def self.new(duck)
17
28
  case duck
18
- when ::String : return new(File.open(duck))
19
- when ::IO : return new(REXML::Document.new(duck).root)
20
- when REXML::Element : return new_decorated_obj(duck)
21
- when REXML::Elements : return duck.map { |dee| new_decorated_obj(dee) }
22
- else raise "Don't know how to start from '#{duck.class}' object."
29
+ when adapter::Element : new_decorated_obj(duck)
30
+ when Array : duck.map { |d| new_decorated_obj(d) }
31
+ else new adapter.new(duck)
23
32
  end
24
33
  end
25
34
 
26
- # Takes any REXML::Element object, and converts it recursively into
35
+ private ##################################################################
36
+
37
+ # Takes any Element object, and converts it recursively into
27
38
  # the corresponding tree of decorated objects.
28
39
  def self.new_decorated_obj(xml)
29
- obj = if xml.text.blank? &&
30
- xml.elements.map { |e| e.name }.uniq.size == 1
40
+ obj = if xml.value.blank? &&
41
+ xml.children.collect { |e| e.name }.uniq.size == 1
31
42
 
32
- CollectionProxy.new new(xml.elements)
43
+ CollectionProxy.new new(xml.children)
33
44
  else
34
- case
35
- when (not xml.text.blank?) : xml.text.to_s
36
- when (xml.cdatas.size >= 1) : xml.cdatas.first.to_s
37
- else ''
38
- end.extend String
45
+ xml.value.extend String # Teach our string to behave like XML
39
46
  end
40
47
 
41
48
  obj.instance_variable_set :@__raw_xml, xml
42
49
 
43
- xml.each_element { |child| add_child(obj, child.name, new(child)) }
50
+ xml.children.each { |child| add_child(obj, child.name, new(child)) }
44
51
  xml.attributes.each { |name, value| add_attribute(obj, name, value) }
45
52
 
46
- obj.extend CommonBehaviours
53
+ # Let's teach our object some new tricks:
54
+ obj.extend(ArrayNotation).extend(MethodMissingDispatchers)
47
55
  end
48
56
 
49
- private ##################################################################
50
-
51
57
  # Decorates the given object 'obj' with a method 'name' that returns the
52
58
  # given 'element'. If 'name' is already taken, takes care of the array
53
59
  # folding behaviour.
@@ -57,27 +63,9 @@ module XMLStruct
57
63
 
58
64
  children[key] = if children[key]
59
65
 
60
- unless obj.respond_to?((plural_key = key.to_s.pluralize).to_sym)
61
- begin
62
- obj.instance_eval %{
63
- def #{plural_key}; @__children[%s|#{key.to_s}|]; end }
64
- rescue SyntaxError
65
- nil
66
- end
67
- end
68
-
69
66
  children[key] = [ children[key] ] unless children[key].is_a? Array
70
67
  children[key] << element
71
68
  else
72
- unless obj.respond_to? key
73
- begin
74
- obj.instance_eval %{
75
- def #{key.to_s}; @__children[%s|#{key.to_s}|]; end }
76
- rescue SyntaxError
77
- nil
78
- end
79
- end
80
-
81
69
  element
82
70
  end
83
71
 
@@ -92,15 +80,6 @@ module XMLStruct
92
80
  attributes = obj.instance_variable_get :@__attributes
93
81
  attributes[(key = name.to_sym)] = attr_value.squish.extend String
94
82
 
95
- unless obj.respond_to? key
96
- begin
97
- obj.instance_eval %{
98
- def #{key.to_s}; @__attributes[%s|#{key.to_s}|]; end }
99
- rescue SyntaxError
100
- nil
101
- end
102
- end
103
-
104
83
  obj.instance_variable_set :@__attributes, attributes
105
84
  attr_value
106
85
  end