yob-roxml 3.1.6

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 (102) hide show
  1. data/.gitignore +7 -0
  2. data/.gitmodules +3 -0
  3. data/History.txt +354 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +195 -0
  6. data/Rakefile +117 -0
  7. data/TODO +37 -0
  8. data/VERSION +1 -0
  9. data/examples/amazon.rb +35 -0
  10. data/examples/current_weather.rb +27 -0
  11. data/examples/dashed_elements.rb +20 -0
  12. data/examples/library.rb +40 -0
  13. data/examples/posts.rb +27 -0
  14. data/examples/rails.rb +70 -0
  15. data/examples/twitter.rb +37 -0
  16. data/examples/xml/active_record.xml +70 -0
  17. data/examples/xml/amazon.xml +133 -0
  18. data/examples/xml/current_weather.xml +89 -0
  19. data/examples/xml/dashed_elements.xml +52 -0
  20. data/examples/xml/posts.xml +23 -0
  21. data/examples/xml/twitter.xml +422 -0
  22. data/lib/roxml.rb +556 -0
  23. data/lib/roxml/definition.rb +238 -0
  24. data/lib/roxml/hash_definition.rb +25 -0
  25. data/lib/roxml/xml.rb +40 -0
  26. data/lib/roxml/xml/parsers/libxml.rb +85 -0
  27. data/lib/roxml/xml/parsers/nokogiri.rb +82 -0
  28. data/lib/roxml/xml/references.rb +322 -0
  29. data/roxml.gemspec +206 -0
  30. data/spec/definition_spec.rb +494 -0
  31. data/spec/examples/active_record_spec.rb +40 -0
  32. data/spec/examples/amazon_spec.rb +54 -0
  33. data/spec/examples/current_weather_spec.rb +37 -0
  34. data/spec/examples/dashed_elements_spec.rb +20 -0
  35. data/spec/examples/library_spec.rb +46 -0
  36. data/spec/examples/post_spec.rb +24 -0
  37. data/spec/examples/twitter_spec.rb +32 -0
  38. data/spec/roxml_spec.rb +372 -0
  39. data/spec/shared_specs.rb +15 -0
  40. data/spec/spec.opts +1 -0
  41. data/spec/spec_helper.rb +14 -0
  42. data/spec/support/libxml.rb +3 -0
  43. data/spec/support/nokogiri.rb +3 -0
  44. data/spec/xml/array_spec.rb +36 -0
  45. data/spec/xml/attributes_spec.rb +71 -0
  46. data/spec/xml/encoding_spec.rb +52 -0
  47. data/spec/xml/namespace_spec.rb +270 -0
  48. data/spec/xml/namespaces_spec.rb +67 -0
  49. data/spec/xml/object_spec.rb +82 -0
  50. data/spec/xml/parser_spec.rb +21 -0
  51. data/spec/xml/text_spec.rb +71 -0
  52. data/test/fixtures/book_malformed.xml +5 -0
  53. data/test/fixtures/book_pair.xml +8 -0
  54. data/test/fixtures/book_text_with_attribute.xml +5 -0
  55. data/test/fixtures/book_valid.xml +5 -0
  56. data/test/fixtures/book_with_authors.xml +7 -0
  57. data/test/fixtures/book_with_contributions.xml +9 -0
  58. data/test/fixtures/book_with_contributors.xml +7 -0
  59. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  60. data/test/fixtures/book_with_default_namespace.xml +9 -0
  61. data/test/fixtures/book_with_depth.xml +6 -0
  62. data/test/fixtures/book_with_octal_pages.xml +4 -0
  63. data/test/fixtures/book_with_publisher.xml +7 -0
  64. data/test/fixtures/book_with_wrapped_attr.xml +3 -0
  65. data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
  66. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  67. data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
  68. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  69. data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
  70. data/test/fixtures/dictionary_of_names.xml +4 -0
  71. data/test/fixtures/dictionary_of_texts.xml +10 -0
  72. data/test/fixtures/library.xml +30 -0
  73. data/test/fixtures/library_uppercase.xml +30 -0
  74. data/test/fixtures/muffins.xml +3 -0
  75. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  76. data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
  77. data/test/fixtures/node_with_name_conflicts.xml +4 -0
  78. data/test/fixtures/numerology.xml +4 -0
  79. data/test/fixtures/person.xml +1 -0
  80. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  81. data/test/fixtures/person_with_mothers.xml +10 -0
  82. data/test/load_test.rb +6 -0
  83. data/test/mocks/dictionaries.rb +57 -0
  84. data/test/mocks/mocks.rb +279 -0
  85. data/test/support/fixtures.rb +11 -0
  86. data/test/test_helper.rb +34 -0
  87. data/test/unit/definition_test.rb +235 -0
  88. data/test/unit/deprecations_test.rb +24 -0
  89. data/test/unit/to_xml_test.rb +81 -0
  90. data/test/unit/xml_attribute_test.rb +39 -0
  91. data/test/unit/xml_block_test.rb +81 -0
  92. data/test/unit/xml_bool_test.rb +122 -0
  93. data/test/unit/xml_convention_test.rb +150 -0
  94. data/test/unit/xml_hash_test.rb +115 -0
  95. data/test/unit/xml_initialize_test.rb +49 -0
  96. data/test/unit/xml_name_test.rb +141 -0
  97. data/test/unit/xml_namespace_test.rb +31 -0
  98. data/test/unit/xml_object_test.rb +205 -0
  99. data/test/unit/xml_required_test.rb +94 -0
  100. data/test/unit/xml_text_test.rb +71 -0
  101. data/website/index.html +98 -0
  102. metadata +300 -0
@@ -0,0 +1,238 @@
1
+ require 'roxml/hash_definition'
2
+
3
+ class Module
4
+ def bool_attr_reader(*attrs)
5
+ attrs.each do |attr|
6
+ define_method :"#{attr}?" do
7
+ instance_variable_get(:"@#{attr}") || false
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ module ROXML
14
+ class ContradictoryNamespaces < StandardError
15
+ end
16
+
17
+ class Definition # :nodoc:
18
+ attr_reader :name, :sought_type, :wrapper, :hash, :blocks, :accessor, :to_xml, :attr_name, :namespace
19
+ bool_attr_reader :name_explicit, :array, :cdata, :required, :frozen
20
+
21
+ def initialize(sym, opts = {}, &block)
22
+ opts.assert_valid_keys(:from, :in, :as, :namespace,
23
+ :else, :required, :frozen, :cdata, :to_xml)
24
+ @default = opts.delete(:else)
25
+ @to_xml = opts.delete(:to_xml)
26
+ @name_explicit = opts.has_key?(:from) && opts[:from].is_a?(String)
27
+ @cdata = opts.delete(:cdata)
28
+ @required = opts.delete(:required)
29
+ @frozen = opts.delete(:frozen)
30
+ @wrapper = opts.delete(:in)
31
+ @namespace = opts.delete(:namespace)
32
+
33
+ @accessor = sym.to_s
34
+ opts[:as] ||=
35
+ if @accessor.ends_with?('?')
36
+ :bool
37
+ elsif @accessor.ends_with?('_on')
38
+ Date
39
+ elsif @accessor.ends_with?('_at')
40
+ DateTime
41
+ end
42
+
43
+ @array = opts[:as].is_a?(Array)
44
+ @blocks = collect_blocks(block, opts[:as])
45
+
46
+ @sought_type = extract_type(opts[:as])
47
+ if @sought_type.respond_to?(:roxml_tag_name)
48
+ opts[:from] ||= @sought_type.roxml_tag_name
49
+ end
50
+
51
+ if opts[:from] == :content
52
+ opts[:from] = '.'
53
+ elsif opts[:from] == :name
54
+ opts[:from] = '*'
55
+ elsif opts[:from] == :attr
56
+ @sought_type = :attr
57
+ opts[:from] = nil
58
+ elsif opts[:from] == :namespace
59
+ opts[:from] = '*'
60
+ @sought_type = :namespace
61
+ elsif opts[:from].to_s.starts_with?('@')
62
+ @sought_type = :attr
63
+ opts[:from].sub!('@', '')
64
+ end
65
+
66
+ @name = @attr_name = accessor.to_s.chomp('?')
67
+ @name = @name.singularize if hash? || array?
68
+ @name = (opts[:from] || @name).to_s
69
+ if hash? && (hash.key.name? || hash.value.name?)
70
+ @name = '*'
71
+ end
72
+ raise ContradictoryNamespaces if @name.include?(':') && (@namespace.present? || @namespace == false)
73
+
74
+ raise ArgumentError, "Can't specify both :else default and :required" if required? && @default
75
+ end
76
+
77
+ def instance_variable_name
78
+ :"@#{attr_name}"
79
+ end
80
+
81
+ def setter
82
+ :"#{attr_name}="
83
+ end
84
+
85
+ def hash
86
+ if hash?
87
+ @sought_type.wrapper ||= name
88
+ @sought_type
89
+ end
90
+ end
91
+
92
+ def hash?
93
+ @sought_type.is_a?(HashDefinition)
94
+ end
95
+
96
+ def name?
97
+ @name == '*'
98
+ end
99
+
100
+ def content?
101
+ @name == '.'
102
+ end
103
+
104
+ def default
105
+ if @default.nil?
106
+ @default = [] if array?
107
+ @default = {} if hash?
108
+ end
109
+ @default.duplicable? ? @default.dup : @default
110
+ end
111
+
112
+ def to_ref(inst)
113
+ case sought_type
114
+ when :attr then XMLAttributeRef
115
+ when :text then XMLTextRef
116
+ when :namespace then XMLNameSpaceRef
117
+ when HashDefinition then XMLHashRef
118
+ when Symbol then raise ArgumentError, "Invalid type argument #{sought_type}"
119
+ else XMLObjectRef
120
+ end.new(self, inst)
121
+ end
122
+
123
+ private
124
+ def self.all(items, &block)
125
+ array = items.is_a?(Array)
126
+ results = (array ? items : [items]).map do |item|
127
+ yield item
128
+ end
129
+
130
+ array ? results : results.first
131
+ end
132
+
133
+ def self.fetch_bool(value, default)
134
+ value = value.to_s.downcase
135
+ if %w{true yes 1 t}.include? value
136
+ true
137
+ elsif %w{false no 0 f}.include? value
138
+ false
139
+ else
140
+ default
141
+ end
142
+ end
143
+
144
+ CORE_BLOCK_SHORTHANDS = {
145
+ # Core Shorthands
146
+ Integer => lambda do |val|
147
+ all(val) do |v|
148
+ Integer(v) unless v.blank?
149
+ end
150
+ end,
151
+ Float => lambda do |val|
152
+ all(val) do |v|
153
+ Float(v) unless v.blank?
154
+ end
155
+ end,
156
+ Fixnum => lambda do |val|
157
+ all(val) do |v|
158
+ v.to_i unless v.blank?
159
+ end
160
+ end,
161
+ Time => lambda do |val|
162
+ all(val) {|v| Time.parse(v) unless v.blank? }
163
+ end,
164
+
165
+ :bool => nil,
166
+ :bool_standalone => lambda do |val|
167
+ all(val) do |v|
168
+ fetch_bool(v, nil)
169
+ end
170
+ end,
171
+ :bool_combined => lambda do |val|
172
+ all(val) do |v|
173
+ fetch_bool(v, v)
174
+ end
175
+ end
176
+ }
177
+
178
+ def self.block_shorthands
179
+ # dynamically load these shorthands at class definition time, but
180
+ # only if they're already availbable
181
+ CORE_BLOCK_SHORTHANDS.tap do |blocks|
182
+ blocks.reverse_merge!(BigDecimal => lambda do |val|
183
+ all(val) do |v|
184
+ BigDecimal.new(v) unless v.blank?
185
+ end
186
+ end) if defined?(BigDecimal)
187
+
188
+ blocks.reverse_merge!(DateTime => lambda do |val|
189
+ if defined?(DateTime)
190
+ all(val) {|v| DateTime.parse(v) unless v.blank? }
191
+ end
192
+ end) if defined?(DateTime)
193
+
194
+ blocks.reverse_merge!(Date => lambda do |val|
195
+ if defined?(Date)
196
+ all(val) {|v| Date.parse(v) unless v.blank? }
197
+ end
198
+ end) if defined?(Date)
199
+ end
200
+ end
201
+
202
+ def collect_blocks(block, as)
203
+ if as.is_a?(Array)
204
+ if as.size > 1
205
+ raise ArgumentError, "multiple :as types (#{as.map(&:inspect).join(', ')}) is not supported. Use a block if you want more complicated behavior."
206
+ end
207
+
208
+ as = as.first
209
+ end
210
+
211
+ if as == :bool
212
+ # if a second block is present, and we can't coerce the xml value
213
+ # to bool, we need to be able to pass it to the user-provided block
214
+ as = (block ? :bool_combined : :bool_standalone)
215
+ end
216
+ as = self.class.block_shorthands.fetch(as) do
217
+ unless (as == :text) || as.respond_to?(:from_xml) || (as.respond_to?(:first) && as.first.respond_to?(:from_xml)) || (as.is_a?(Hash) && !(as.keys & [:key, :value]).empty?)
218
+ raise ArgumentError, "Invalid :as argument #{as}" unless as.nil?
219
+ end
220
+ nil
221
+ end
222
+ [as, block].compact
223
+ end
224
+
225
+ def extract_type(as)
226
+ if as.is_a?(Hash)
227
+ return HashDefinition.new(as)
228
+ elsif as.respond_to?(:from_xml)
229
+ return as
230
+ elsif as.is_a?(Array) && as.first.respond_to?(:from_xml)
231
+ @array = true
232
+ return as.first
233
+ else
234
+ :text
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,25 @@
1
+ module ROXML
2
+ class HashDefinition # :nodoc:
3
+ attr_reader :key, :value
4
+ attr_accessor :wrapper
5
+
6
+ def initialize(opts)
7
+ opts.assert_valid_keys(:key, :value)
8
+
9
+ @key = Definition.new(nil, to_definition_options(opts, :key))
10
+ @value = Definition.new(nil, to_definition_options(opts, :value))
11
+ end
12
+
13
+ private
14
+ def to_definition_options(opts, what)
15
+ case opts[what]
16
+ when Hash
17
+ opts[what]
18
+ when String, Symbol
19
+ {:from => opts[what]}
20
+ else
21
+ raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ module ROXML
2
+ PARSERS = %w[nokogiri libxml].freeze
3
+ unless const_defined? 'XML_PARSER'
4
+ parsers = PARSERS.dup
5
+ begin
6
+ require parsers.first
7
+ XML_PARSER = parsers.first # :nodoc:
8
+ rescue LoadError
9
+ parsers.shift
10
+ retry unless parsers.empty?
11
+ raise "Could not load a parser. Tried #{PARSERS.to_sentence}"
12
+ end
13
+ end
14
+
15
+ require File.join('roxml/xml/parsers', XML_PARSER)
16
+
17
+ module XML
18
+ class Node
19
+ def self.from(data)
20
+ case data
21
+ when XML::Node
22
+ data
23
+ when XML::Document
24
+ data.root
25
+ when File, IO
26
+ XML.parse_io(data).root
27
+ else
28
+ if (defined?(URI) && data.is_a?(URI::Generic)) ||
29
+ (defined?(Pathname) && data.is_a?(Pathname))
30
+ XML.parse_file(data.to_s).root
31
+ else
32
+ XML.parse_string(data).root
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ require 'roxml/xml/references'
@@ -0,0 +1,85 @@
1
+ require 'libxml'
2
+
3
+ module ROXML
4
+ module XML # :nodoc:all
5
+
6
+ class << self
7
+ def set_attribute(node, name, value)
8
+ node.attributes[name] = value
9
+ end
10
+
11
+ def set_content(node, content)
12
+ node.content = content.gsub('&', '&amp;')
13
+ end
14
+
15
+ def new_node(name)
16
+ LibXML::XML::Node.new(name)
17
+ end
18
+
19
+ def add_node(parent, name)
20
+ add_child(parent, new_node(name))
21
+ end
22
+
23
+ def add_cdata(parent, content)
24
+ add_child(parent, LibXML::XML::Node.new_cdata(content))
25
+ end
26
+
27
+ def add_child(parent, child)
28
+ parent << child
29
+ child
30
+ end
31
+
32
+ def parse_string(str_data)
33
+ LibXML::XML::Parser.string(str_data).parse
34
+ end
35
+
36
+ def parse_file(path)
37
+ LibXML::XML::Parser.file(path).parse
38
+ end
39
+
40
+ def parse_io(stream)
41
+ LibXML::XML::Parser.io(stream).parse
42
+ end
43
+
44
+ def save_doc(doc, path)
45
+ doc.save(path)
46
+ end
47
+ end
48
+
49
+ Document = LibXML::XML::Document
50
+ Node = LibXML::XML::Node
51
+
52
+ module NamespacedSearch
53
+ def roxml_search(xpath, roxml_namespaces = {})
54
+ if namespaces.default
55
+ roxml_namespaces = {:xmlns => namespaces.default.href}.merge(roxml_namespaces)
56
+ end
57
+ if roxml_namespaces.present?
58
+ find(xpath, roxml_namespaces.map {|prefix, href| [prefix, href].join(':') })
59
+ else
60
+ find(xpath)
61
+ end
62
+ end
63
+ end
64
+
65
+ class Document
66
+ include NamespacedSearch
67
+
68
+ def default_namespace
69
+ default = namespaces.default
70
+ default.prefix || 'xmlns' if default
71
+ end
72
+
73
+ private
74
+ delegate :namespaces, :to => :root
75
+ end
76
+
77
+ class Node
78
+ include NamespacedSearch
79
+
80
+ def default_namespace
81
+ doc.default_namespace
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,82 @@
1
+ require 'nokogiri'
2
+
3
+ module ROXML
4
+ module XML # :nodoc:all
5
+
6
+ class << self
7
+ def set_attribute(node, name, value)
8
+ node[name] = value
9
+ end
10
+
11
+ def set_content(node, content)
12
+ node.content = content
13
+ end
14
+
15
+ def new_node(name)
16
+ Nokogiri::XML::Node.new(name, Document.new)
17
+ end
18
+
19
+ def add_node(parent, name)
20
+ add_child(parent, Nokogiri::XML::Node.new(name, parent.document))
21
+ end
22
+
23
+ def add_cdata(parent, content)
24
+ parent.add_child(Nokogiri::XML::CDATA.new(parent.document, content))
25
+ end
26
+
27
+ def add_child(parent, child)
28
+ parent.add_child(child)
29
+ end
30
+
31
+ def parse_string(string)
32
+ Nokogiri::XML(string)
33
+ end
34
+
35
+ def parse_file(path)
36
+ path = path.sub('file:', '') if path.starts_with?('file:')
37
+ parse_io(open(path))
38
+ end
39
+
40
+ def parse_io(stream)
41
+ Nokogiri::XML(stream)
42
+ end
43
+
44
+ def save_doc(doc, path)
45
+ open(path, 'w') do |file|
46
+ file << doc.serialize
47
+ end
48
+ end
49
+ end
50
+
51
+ Document = Nokogiri::XML::Document
52
+ Element = Nokogiri::XML::Element
53
+ Node = Nokogiri::XML::Node
54
+
55
+ class Document
56
+ alias :roxml_search :search
57
+
58
+ def default_namespace
59
+ 'xmlns' if root.namespaces['xmlns']
60
+ end
61
+ end
62
+
63
+ module NodeExtensions
64
+ def roxml_search(xpath, roxml_namespaces = {})
65
+ xpath = "./#{xpath}"
66
+ (roxml_namespaces.present? ? search(xpath, roxml_namespaces) : search(xpath))
67
+ end
68
+
69
+ def default_namespace
70
+ document.default_namespace
71
+ end
72
+ end
73
+
74
+ class Element
75
+ include NodeExtensions
76
+ end
77
+
78
+ class Node
79
+ include NodeExtensions
80
+ end
81
+ end
82
+ end