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