jordi-xml_struct 0.2.1 → 0.9.0

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