xommelier 0.1.16 → 0.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +3 -1
- data/lib/xommelier.rb +11 -0
- data/lib/xommelier/atom.rb +0 -1
- data/lib/xommelier/atom/entry.rb +0 -2
- data/lib/xommelier/atom/feed.rb +0 -2
- data/lib/xommelier/common.rb +24 -0
- data/lib/xommelier/opml.rb +122 -0
- data/lib/xommelier/rss.rb +237 -0
- data/lib/xommelier/schemas/rss.xsd +500 -0
- data/lib/xommelier/version.rb +1 -1
- data/lib/xommelier/xml.rb +1 -1
- data/lib/xommelier/xml/element.rb +5 -16
- data/lib/xommelier/xml/element/serialization.rb +69 -62
- data/lib/xommelier/xml/element/structure.rb +69 -68
- data/lib/xommelier/xml/element/structure/property.rb +136 -0
- data/lib/xommelier/xml/namespace.rb +5 -1
- data/spec/fixtures/feed.rss2.0.xml +41 -0
- data/spec/fixtures/rss-2.0-namespace.xml +78 -0
- data/spec/fixtures/simple_feed.rss.xml +17 -0
- data/spec/functional/atom_feed_building_spec.rb +5 -13
- data/spec/functional/atom_feed_thread_building_spec.rb +1 -1
- data/spec/functional/build_nested_document_from_hash_spec.rb +1 -1
- data/spec/functional/rss_feed_building_spec.rb +25 -0
- data/spec/functional/rss_feed_parsing_spec.rb +41 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/fixtures.rb +1 -1
- metadata +17 -4
- data/spec/fixtures/atom.xsd.xml +0 -240
data/lib/xommelier/version.rb
CHANGED
data/lib/xommelier/xml.rb
CHANGED
@@ -20,7 +20,7 @@ module Xommelier
|
|
20
20
|
options[:module] ||= self
|
21
21
|
instance_variable_set(:@_xmlns, Xommelier::Xml::Namespace.new(uri, options, &block))
|
22
22
|
end
|
23
|
-
instance_variable_get(:@_xmlns)
|
23
|
+
instance_variable_get(:@_xmlns) || Xml.xmlns
|
24
24
|
end
|
25
25
|
|
26
26
|
def schema(schema = nil)
|
@@ -7,13 +7,6 @@ require 'active_support/core_ext/class/attribute'
|
|
7
7
|
|
8
8
|
module Xommelier
|
9
9
|
module Xml
|
10
|
-
DEFAULT_OPTIONS = {
|
11
|
-
type: String
|
12
|
-
}
|
13
|
-
DEFAULT_ELEMENT_OPTIONS = DEFAULT_OPTIONS.merge(
|
14
|
-
count: :one
|
15
|
-
)
|
16
|
-
|
17
10
|
class Element
|
18
11
|
include Xommelier::Xml::Element::Structure
|
19
12
|
include Xommelier::Xml::Element::Serialization
|
@@ -28,13 +21,7 @@ module Xommelier
|
|
28
21
|
@text = nil
|
29
22
|
@errors = []
|
30
23
|
|
31
|
-
|
32
|
-
send("#{name}=", options[:default]) if options.key?(:default)
|
33
|
-
end
|
34
|
-
|
35
|
-
self.class.elements.each do |name, options|
|
36
|
-
send("#{name}=", options[:default]) if options.key?(:default)
|
37
|
-
end
|
24
|
+
set_default_values
|
38
25
|
|
39
26
|
case contents
|
40
27
|
when Hash
|
@@ -68,10 +55,12 @@ module Xommelier
|
|
68
55
|
def validate
|
69
56
|
@errors = []
|
70
57
|
to_xml unless xml_document
|
71
|
-
if
|
72
|
-
|
58
|
+
if schema
|
59
|
+
schema.validate(xml_document).each do |error|
|
73
60
|
@errors << error
|
74
61
|
end
|
62
|
+
else
|
63
|
+
raise NoSchemaError.new(self)
|
75
64
|
end
|
76
65
|
end
|
77
66
|
|
@@ -26,13 +26,13 @@ module Xommelier
|
|
26
26
|
[ns, element].compact.join(':')
|
27
27
|
end
|
28
28
|
|
29
|
-
def element_xpath(
|
30
|
-
ns_element(xmlns_xpath(
|
29
|
+
def element_xpath(xml_doc = nil, name = nil)
|
30
|
+
ns_element(xmlns_xpath(xml_doc), name || element_name)
|
31
31
|
end
|
32
32
|
|
33
|
-
def xmlns_xpath(
|
34
|
-
if
|
35
|
-
prefix =
|
33
|
+
def xmlns_xpath(xml_doc = nil)
|
34
|
+
if xml_doc
|
35
|
+
prefix = xml_doc.namespaces.key(xmlns.try(:uri))
|
36
36
|
(prefix =~ /:/) ? prefix[6..-1] : prefix
|
37
37
|
else
|
38
38
|
xmlns.as
|
@@ -41,8 +41,7 @@ module Xommelier
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def from_xml(xml, options = {})
|
44
|
-
|
45
|
-
when IO, String
|
44
|
+
if IO === xml || String === xml
|
46
45
|
xml = Nokogiri::XML(xml)
|
47
46
|
end
|
48
47
|
@_xml_node = options.delete(:node) { xml.at_xpath(element_xpath(xml.document, element_name)) }
|
@@ -52,12 +51,12 @@ module Xommelier
|
|
52
51
|
self.text = @_xml_node.inner_html
|
53
52
|
end
|
54
53
|
|
55
|
-
self.class.attributes.each do |
|
56
|
-
deserialize_attribute(
|
54
|
+
self.class.attributes.values.each do |attribute|
|
55
|
+
deserialize_attribute(attribute)
|
57
56
|
end
|
58
57
|
|
59
|
-
self.class.elements.each do |
|
60
|
-
deserialize_element(
|
58
|
+
self.class.elements.values.each do |element|
|
59
|
+
deserialize_element(element)
|
61
60
|
end
|
62
61
|
end
|
63
62
|
alias_method :from_xommelier, :from_xml
|
@@ -72,7 +71,7 @@ module Xommelier
|
|
72
71
|
builder = options.delete(:builder)
|
73
72
|
attribute_values = {}
|
74
73
|
namespaces = builder.doc.namespaces
|
75
|
-
prefix = options[:prefix] ||
|
74
|
+
prefix = options[:prefix] || namespaces.key(xmlns.uri).try(:[], 6..-1).presence
|
76
75
|
else # Root element
|
77
76
|
builder = Nokogiri::XML::Builder.new(options)
|
78
77
|
attribute_values = children_namespaces.inject({xmlns: xmlns.uri}) do |hash, ns|
|
@@ -86,25 +85,26 @@ module Xommelier
|
|
86
85
|
end
|
87
86
|
current_xmlns = builder.doc.namespaces[prefix ? "xmlns:#{prefix}" : 'xmlns']
|
88
87
|
attributes.each do |name, value|
|
89
|
-
|
90
|
-
attribute_name =
|
91
|
-
ns =
|
88
|
+
attribute = attribute_options(name)
|
89
|
+
attribute_name = attribute.attribute_name
|
90
|
+
ns = attribute.ns
|
92
91
|
if ns.uri != current_xmlns
|
93
92
|
if ns.as == :xml
|
94
|
-
attribute_name = "xml:#{
|
95
|
-
elsif attr_prefix = namespaces.key(ns.uri).try(:[], 6..-1).presence
|
96
|
-
attribute_name = "#{attr_prefix}:#{
|
93
|
+
attribute_name = "xml:#{attribute_name}"
|
94
|
+
elsif (attr_prefix = namespaces.key(ns.uri).try(:[], 6..-1).presence)
|
95
|
+
attribute_name = "#{attr_prefix}:#{attribute_name}"
|
97
96
|
end
|
98
97
|
end
|
99
98
|
serialize_attribute(attribute_name, value, attribute_values)
|
100
99
|
end
|
101
100
|
@_xml_node = (prefix ? builder[prefix] : builder).
|
102
101
|
send(element_name, attribute_values) do |xml|
|
103
|
-
self.class.elements.each do |name,
|
102
|
+
self.class.elements.each do |name, element|
|
104
103
|
value = elements.fetch(name, options[:default])
|
105
104
|
if value
|
106
|
-
|
107
|
-
|
105
|
+
element.override(xmlns: xmlns) do
|
106
|
+
serialize_element(name, value, xml, element)
|
107
|
+
end
|
108
108
|
end
|
109
109
|
end
|
110
110
|
xml.text(@text) if respond_to?(:text)
|
@@ -117,7 +117,7 @@ module Xommelier
|
|
117
117
|
attributes.dup.tap do |hash|
|
118
118
|
@elements.each do |name, value|
|
119
119
|
options = element_options(name)
|
120
|
-
type = options
|
120
|
+
type = options.type
|
121
121
|
value = Array.wrap(value)
|
122
122
|
if type < Xml::Element
|
123
123
|
value = value.map(&:to_hash)
|
@@ -168,16 +168,16 @@ module Xommelier
|
|
168
168
|
|
169
169
|
delegate :ns_element, to: 'self.class'
|
170
170
|
|
171
|
-
def element_xpath(
|
172
|
-
self.class.element_xpath(
|
171
|
+
def element_xpath(xml_doc = self.xml_document, name = nil)
|
172
|
+
self.class.element_xpath(xml_doc, name)
|
173
173
|
end
|
174
174
|
|
175
175
|
def children_namespaces(namespaces = Set[xmlns])
|
176
176
|
elements.inject(namespaces) do |result, (name, children)|
|
177
|
-
|
178
|
-
result <<
|
179
|
-
result += attributes.keys.map { |
|
180
|
-
if
|
177
|
+
element = self.class.elements[name]
|
178
|
+
result << element.ns
|
179
|
+
result += attributes.keys.map { |attr_name| attribute_options(attr_name).ns }
|
180
|
+
if element.type < Xml::Element
|
181
181
|
Array(children).each do |child|
|
182
182
|
result += child.children_namespaces
|
183
183
|
end
|
@@ -198,60 +198,67 @@ module Xommelier
|
|
198
198
|
attributes[name] = value.to_xommelier
|
199
199
|
end
|
200
200
|
|
201
|
-
|
202
|
-
|
203
|
-
ns =
|
201
|
+
# @param [Xommelier::Xml::Element::Structure::Attribute] attribute
|
202
|
+
def deserialize_attribute(attribute)
|
203
|
+
ns = attribute.ns
|
204
204
|
if ns.default? || ns == xmlns
|
205
|
-
send(
|
205
|
+
send(attribute.writer, @_xml_node[attribute.attribute_name])
|
206
206
|
else
|
207
|
-
send(
|
207
|
+
send(attribute.writer, @_xml_node.attribute_with_ns(attribute.attribute_name, ns.uri.to_s).try(:value))
|
208
208
|
end
|
209
209
|
end
|
210
210
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
nodes = @_xml_node.xpath("./#{ns_element(options[:ns].as, options[:element_name])}", options[:ns].to_hash)
|
211
|
+
# @param [Xommelier::Xml::Element::Structure::Element] element
|
212
|
+
def deserialize_element(element)
|
213
|
+
nodes = @_xml_node.xpath("./#{ns_element(element.ns.as, element.element_name)}", element.ns.to_hash)
|
215
214
|
if nodes.any?
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
send options[:plural], children
|
215
|
+
if element.multiple?
|
216
|
+
children = nodes.map { |node| typecast_element(node, element) }
|
217
|
+
send(element.plural_writer, children)
|
220
218
|
else
|
221
|
-
send(
|
219
|
+
send(element.writer, typecast_element(nodes[0], element))
|
222
220
|
end
|
223
221
|
end
|
224
222
|
end
|
225
223
|
|
226
|
-
|
227
|
-
|
228
|
-
|
224
|
+
# @param [Nokogiri::XML::Node] node
|
225
|
+
# @param [Xommelier::Xml::Element::Structure::Element] options
|
226
|
+
def typecast_element(node, options)
|
227
|
+
if options.type < Xml::Element
|
228
|
+
options.type.from_xommelier(xml_document, node: node)
|
229
229
|
else
|
230
|
-
type.from_xommelier(node.text)
|
230
|
+
options.type.from_xommelier(node.text)
|
231
231
|
end
|
232
232
|
end
|
233
233
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
234
|
+
# @param [Object] name
|
235
|
+
# @param [Object] value
|
236
|
+
# @param [Object] xml
|
237
|
+
# @param [Xommelier::Xml::Element::Structure::Element] element
|
238
|
+
def serialize_element(name, value, xml, element)
|
239
|
+
if element.multiple?
|
240
|
+
element.override(multiple: false) do
|
241
|
+
value.each do |item|
|
242
|
+
serialize_element(name, item, xml, element)
|
243
|
+
end
|
244
|
+
end
|
239
245
|
else
|
240
|
-
xmlns
|
241
|
-
prefix = if
|
242
|
-
|
243
|
-
elsif options[:ns].try(:!=, xmlns)
|
244
|
-
xml.doc.namespaces.key(options[:ns].uri)[6..-1].presence
|
245
|
-
else
|
246
|
-
nil
|
246
|
+
xmlns = element.overridden_xmlns || self.xmlns
|
247
|
+
prefix = if xmlns != element.ns
|
248
|
+
xml.doc.namespaces.key(element.ns.uri)[6..-1].presence
|
247
249
|
end
|
248
250
|
case value
|
249
251
|
when Xommelier::Xml::Element
|
250
|
-
value.to_xommelier(
|
252
|
+
value.to_xommelier(
|
253
|
+
builder: xml,
|
254
|
+
element_name: element.element_name,
|
255
|
+
prefix: prefix,
|
256
|
+
ns: element.ns
|
257
|
+
)
|
251
258
|
else
|
252
|
-
|
253
|
-
|
254
|
-
|
259
|
+
(prefix ? xml[prefix] : xml).send(element.serializable_element_name) do
|
260
|
+
xml.text(value.to_xommelier)
|
261
|
+
end
|
255
262
|
end
|
256
263
|
end
|
257
264
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'xommelier/xml/element'
|
2
|
+
require 'xommelier/xml/element/structure/property'
|
2
3
|
require 'active_support/concern'
|
3
4
|
require 'active_support/core_ext/module/delegation'
|
4
5
|
require 'active_support/core_ext/object/with_options'
|
@@ -14,16 +15,15 @@ module Xommelier
|
|
14
15
|
|
15
16
|
included do
|
16
17
|
class_attribute :elements, :attributes
|
17
|
-
attr_writer :element_name
|
18
18
|
|
19
|
-
self.elements
|
19
|
+
self.elements = {}
|
20
20
|
self.attributes = {}
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
attr_writer :element_name
|
23
|
+
|
24
|
+
extend SingletonClassMethods
|
25
25
|
|
26
|
-
delegate :xmlns, to: 'self.class'
|
26
|
+
delegate :xmlns, :schema, to: 'self.class'
|
27
27
|
end
|
28
28
|
|
29
29
|
module SingletonClassMethods
|
@@ -42,8 +42,13 @@ module Xommelier
|
|
42
42
|
end
|
43
43
|
@xmlns ||= find_namespace
|
44
44
|
end
|
45
|
+
|
45
46
|
alias_method :xmlns=, :xmlns
|
46
47
|
|
48
|
+
def schema
|
49
|
+
containing_module.schema
|
50
|
+
end
|
51
|
+
|
47
52
|
def element_name(element_name = nil)
|
48
53
|
@element_name = element_name if element_name
|
49
54
|
@element_name ||= find_element_name
|
@@ -66,29 +71,27 @@ module Xommelier
|
|
66
71
|
|
67
72
|
module ClassMethods
|
68
73
|
def inherited(child)
|
69
|
-
child.elements
|
70
|
-
child.attributes
|
74
|
+
child.elements = elements.dup
|
75
|
+
child.attributes = attributes.dup
|
71
76
|
end
|
72
77
|
|
73
78
|
# Defines containing element
|
74
79
|
# @example
|
75
80
|
# element :author, type: Xommelier::Atom::Person
|
76
81
|
def element(name, options = {})
|
77
|
-
options[:
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
elements[name] = DEFAULT_ELEMENT_OPTIONS.merge(options)
|
82
|
+
options[:ns] ||= if options[:type].try(:<, Xml::Element)
|
83
|
+
options[:type].xmlns
|
84
|
+
else
|
85
|
+
xmlns
|
86
|
+
end
|
87
|
+
elements[name] = Element.new(name, options)
|
84
88
|
define_element_accessors(name)
|
85
89
|
end
|
86
90
|
|
87
91
|
# Defines containing attribute
|
88
92
|
def attribute(name, options = {})
|
89
|
-
options[:
|
90
|
-
|
91
|
-
attributes[name] = DEFAULT_OPTIONS.merge(options)
|
93
|
+
options[:ns] ||= xmlns
|
94
|
+
attributes[name] = Attribute.new(name, options)
|
92
95
|
define_attribute_accessors(name)
|
93
96
|
end
|
94
97
|
|
@@ -98,7 +101,7 @@ module Xommelier
|
|
98
101
|
end
|
99
102
|
|
100
103
|
def any(&block)
|
101
|
-
with_options(count: :any)
|
104
|
+
with_options(count: :any) { |any| any.instance_eval(&block) }
|
102
105
|
end
|
103
106
|
|
104
107
|
def many(&block)
|
@@ -109,73 +112,71 @@ module Xommelier
|
|
109
112
|
with_options(count: :may) { |may| may.instance_eval(&block) }
|
110
113
|
end
|
111
114
|
|
112
|
-
def root; end
|
113
|
-
|
114
115
|
private
|
115
116
|
|
116
117
|
def define_element_accessors(name)
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
alias_method "#{name}=", name
|
128
|
-
when :many, :any
|
129
|
-
plural = name.to_s.pluralize.to_sym
|
130
|
-
element_options[:plural] = plural
|
131
|
-
|
132
|
-
define_method(plural) do |*args|
|
133
|
-
if args.any?
|
134
|
-
args.flatten.each_with_index do |object, index|
|
135
|
-
write_element(name, object, index)
|
136
|
-
end
|
137
|
-
end
|
118
|
+
element = elements[name]
|
119
|
+
if element.multiple?
|
120
|
+
# Define plural accessors
|
121
|
+
plural = element.plural
|
122
|
+
|
123
|
+
rw_accessor(plural) do |*args|
|
124
|
+
args.flatten.each_with_index do |object, index|
|
125
|
+
write_element(name, object, index)
|
126
|
+
end if args.any?
|
127
|
+
|
138
128
|
@elements[name] ||= []
|
139
129
|
end
|
140
|
-
|
141
|
-
|
142
|
-
unless
|
143
|
-
|
144
|
-
if args[0]
|
145
|
-
|
146
|
-
else
|
147
|
-
send(plural)[0]
|
148
|
-
end
|
130
|
+
|
131
|
+
# Define singular accessors for first element
|
132
|
+
unless element.numbers_equal?
|
133
|
+
rw_accessor(name) do |*args|
|
134
|
+
send(plural, [args[0]]) if args[0]
|
135
|
+
send(plural)[0]
|
149
136
|
end
|
150
|
-
|
137
|
+
end
|
138
|
+
else
|
139
|
+
# Define singular accessors
|
140
|
+
name = name.to_sym
|
141
|
+
rw_accessor(name) do |*args|
|
142
|
+
write_element(name, args[0]) if args[0]
|
143
|
+
read_element(name)
|
151
144
|
end
|
152
145
|
end
|
153
146
|
end
|
154
147
|
|
155
148
|
def define_attribute_accessors(name)
|
156
|
-
|
157
|
-
if args[0]
|
158
|
-
write_attribute(name.to_s, args[0])
|
159
|
-
end
|
149
|
+
rw_accessor(name) do |*args|
|
150
|
+
write_attribute(name.to_s, args[0]) if args[0]
|
160
151
|
read_attribute(name)
|
161
152
|
end
|
162
|
-
alias_method "#{name}=", name
|
163
153
|
end
|
164
154
|
|
165
155
|
def define_text_accessors
|
166
|
-
|
167
|
-
if args[0]
|
168
|
-
write_text(args[0])
|
169
|
-
end
|
156
|
+
rw_accessor(:text) do |*args|
|
157
|
+
write_text(args[0]) if args[0]
|
170
158
|
read_text
|
171
159
|
end
|
172
|
-
alias_method :text=, :text
|
173
160
|
alias_attribute :content, :text
|
174
161
|
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
# Defines read-write accessor for +name+ and provides alias for write-only version
|
166
|
+
def rw_accessor(name, &block)
|
167
|
+
define_method(name, &block)
|
168
|
+
alias_method "#{name}=", name
|
169
|
+
end
|
175
170
|
end
|
176
171
|
|
177
172
|
protected
|
178
173
|
|
174
|
+
def set_default_values
|
175
|
+
self.class.attributes.merge(self.class.elements).each do |name, property|
|
176
|
+
send("#{name}=", property.default) if property.default?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
179
180
|
def options=(options = {})
|
180
181
|
if @options.key?(:element_name)
|
181
182
|
element_name(@options.delete(:element_name))
|
@@ -184,12 +185,11 @@ module Xommelier
|
|
184
185
|
end
|
185
186
|
|
186
187
|
def element_name(value = nil)
|
187
|
-
if value
|
188
|
-
@element_name = value
|
189
|
-
end
|
188
|
+
@element_name = value if value
|
190
189
|
@element_name ||= self.class.element_name
|
191
190
|
end
|
192
191
|
|
192
|
+
# @return [Xommelier::Xml::Element::Structure::Element]
|
193
193
|
def element_options(name)
|
194
194
|
self.class.elements[name.to_sym]
|
195
195
|
end
|
@@ -199,7 +199,7 @@ module Xommelier
|
|
199
199
|
end
|
200
200
|
|
201
201
|
def write_element(name, value, index = nil)
|
202
|
-
type = element_options(name)
|
202
|
+
type = element_options(name).type
|
203
203
|
unless value.is_a?(type)
|
204
204
|
value = if (type < Xommelier::Xml::Element) && !value.is_a?(Nokogiri::XML::Node)
|
205
205
|
type.new(value)
|
@@ -208,7 +208,7 @@ module Xommelier
|
|
208
208
|
end
|
209
209
|
end
|
210
210
|
if index
|
211
|
-
@elements[name.to_sym]
|
211
|
+
@elements[name.to_sym] ||= []
|
212
212
|
@elements[name.to_sym][index] = value
|
213
213
|
else
|
214
214
|
@elements[name.to_sym] = value
|
@@ -219,6 +219,7 @@ module Xommelier
|
|
219
219
|
@elements.delete(name.to_sym)
|
220
220
|
end
|
221
221
|
|
222
|
+
# @return [Xommelier::Xml::Element::Structure::Attribute]
|
222
223
|
def attribute_options(name)
|
223
224
|
self.class.attributes[name.to_sym]
|
224
225
|
end
|
@@ -228,7 +229,7 @@ module Xommelier
|
|
228
229
|
end
|
229
230
|
|
230
231
|
def write_attribute(name, value)
|
231
|
-
type = attribute_options(name)
|
232
|
+
type = attribute_options(name).type
|
232
233
|
value = type.from_xommelier(value) unless value.is_a?(type)
|
233
234
|
@attributes[name.to_sym] = value
|
234
235
|
end
|