xommelier 0.1.16 → 0.1.18
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.
- 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
|