yob-roxml 3.1.6

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