tilia-xml 1.2.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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +167 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +56 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +30 -0
- data/Rakefile +17 -0
- data/lib/tilia/xml/context_stack_trait.rb +99 -0
- data/lib/tilia/xml/element/base.rb +73 -0
- data/lib/tilia/xml/element/cdata.rb +53 -0
- data/lib/tilia/xml/element/elements.rb +109 -0
- data/lib/tilia/xml/element/key_value.rb +110 -0
- data/lib/tilia/xml/element/uri.rb +98 -0
- data/lib/tilia/xml/element/xml_fragment.rb +128 -0
- data/lib/tilia/xml/element.rb +22 -0
- data/lib/tilia/xml/lib_xml_exception.rb +9 -0
- data/lib/tilia/xml/parse_exception.rb +7 -0
- data/lib/tilia/xml/reader.rb +240 -0
- data/lib/tilia/xml/service.rb +151 -0
- data/lib/tilia/xml/version.rb +9 -0
- data/lib/tilia/xml/writer.rb +261 -0
- data/lib/tilia/xml/xml_deserializable.rb +29 -0
- data/lib/tilia/xml/xml_serializable.rb +27 -0
- data/lib/tilia/xml.rb +23 -0
- data/test/test_helper.rb +4 -0
- data/test/xml/context_stack_test.rb +40 -0
- data/test/xml/element/cdata_test.rb +37 -0
- data/test/xml/element/eater.rb +60 -0
- data/test/xml/element/elements_test.rb +113 -0
- data/test/xml/element/key_value_test.rb +187 -0
- data/test/xml/element/mock.rb +52 -0
- data/test/xml/element/uri_test.rb +55 -0
- data/test/xml/element/xml_fragment_test.rb +121 -0
- data/test/xml/infite_loop_test.rb +47 -0
- data/test/xml/reader_test.rb +407 -0
- data/test/xml/service_test.rb +156 -0
- data/test/xml/writer_test.rb +260 -0
- data/tilia-xml.gemspec +15 -0
- metadata +132 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
require 'stringio'
|
3
|
+
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
|
4
|
+
|
5
|
+
module Tilia
|
6
|
+
module Xml
|
7
|
+
# The Reader class expands upon PHP's built-in XMLReader.
|
8
|
+
#
|
9
|
+
# The intended usage, is to assign certain XML elements to PHP classes. These
|
10
|
+
# need to be registered using the element_map public property.
|
11
|
+
#
|
12
|
+
# After this is done, a single call to parse() will parse the entire document,
|
13
|
+
# and delegate sub-sections of the document to element classes.
|
14
|
+
class Reader
|
15
|
+
include Tilia::Xml::ContextStackTrait
|
16
|
+
|
17
|
+
# Returns the current nodename in clark-notation.
|
18
|
+
#
|
19
|
+
# For example: "{http://www.w3.org/2005/Atom}feed".
|
20
|
+
# Or if no namespace is defined: "{}feed".
|
21
|
+
#
|
22
|
+
# This method returns null if we're not currently on an element.
|
23
|
+
#
|
24
|
+
# @return [String, nil]
|
25
|
+
def clark
|
26
|
+
return nil unless local_name
|
27
|
+
|
28
|
+
"{#{namespace_uri}}#{local_name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Reads the entire document.
|
32
|
+
#
|
33
|
+
# This function returns an array with the following three elements:
|
34
|
+
# * name - The root element name.
|
35
|
+
# * value - The value for the root element.
|
36
|
+
# * attributes - An array of attributes.
|
37
|
+
#
|
38
|
+
# This function will also disable the standard libxml error handler (which
|
39
|
+
# usually just results in PHP errors), and throw exceptions instead.
|
40
|
+
#
|
41
|
+
# @return [Hash]
|
42
|
+
def parse
|
43
|
+
begin
|
44
|
+
nil while node_type != ::LibXML::XML::Reader::TYPE_ELEMENT && read # noop
|
45
|
+
|
46
|
+
result = parse_current_element
|
47
|
+
rescue ::LibXML::XML::Error => e
|
48
|
+
raise Tilia::Xml::LibXmlException, e.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# parse_get_elements parses everything in the current sub-tree,
|
55
|
+
# and returns a an array of elements.
|
56
|
+
#
|
57
|
+
# Each element has a 'name', 'value' and 'attributes' key.
|
58
|
+
#
|
59
|
+
# If the the element didn't contain sub-elements, an empty array is always
|
60
|
+
# returned. If there was any text inside the element, it will be
|
61
|
+
# discarded.
|
62
|
+
#
|
63
|
+
# If the element_map argument is specified, the existing element_map will
|
64
|
+
# be overridden while parsing the tree, and restored after this process.
|
65
|
+
#
|
66
|
+
# @param [Hash] element_map
|
67
|
+
# @return [Array]
|
68
|
+
def parse_get_elements(element_map = nil)
|
69
|
+
result = parse_inner_tree(element_map)
|
70
|
+
|
71
|
+
return [] unless result.is_a?(Array)
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parses all elements below the current element.
|
76
|
+
#
|
77
|
+
# This method will return a string if this was a text-node, or an array if
|
78
|
+
# there were sub-elements.
|
79
|
+
#
|
80
|
+
# If there's both text and sub-elements, the text will be discarded.
|
81
|
+
#
|
82
|
+
# If the element_map argument is specified, the existing element_map will
|
83
|
+
# be overridden while parsing the tree, and restored after this process.
|
84
|
+
#
|
85
|
+
# @param [Hash] element_map
|
86
|
+
# @return [Array, String]
|
87
|
+
def parse_inner_tree(element_map = nil)
|
88
|
+
text = nil
|
89
|
+
elements = []
|
90
|
+
|
91
|
+
if node_type == ::LibXML::XML::Reader::TYPE_ELEMENT && self.empty_element?
|
92
|
+
# Easy!
|
93
|
+
self.next
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
|
97
|
+
unless element_map.nil?
|
98
|
+
push_context
|
99
|
+
@element_map = element_map
|
100
|
+
end
|
101
|
+
|
102
|
+
return false unless read
|
103
|
+
|
104
|
+
loop do
|
105
|
+
# RUBY: Skip is_valid block
|
106
|
+
|
107
|
+
case node_type
|
108
|
+
when ::LibXML::XML::Reader::TYPE_ELEMENT
|
109
|
+
elements << parse_current_element
|
110
|
+
when ::LibXML::XML::Reader::TYPE_TEXT,
|
111
|
+
::LibXML::XML::Reader::TYPE_CDATA
|
112
|
+
text ||= ''
|
113
|
+
text += value
|
114
|
+
read
|
115
|
+
when ::LibXML::XML::Reader::TYPE_END_ELEMENT
|
116
|
+
# Ensuring we are moving the cursor after the end element.
|
117
|
+
read
|
118
|
+
break
|
119
|
+
when ::LibXML::XML::Reader::TYPE_NONE
|
120
|
+
fail Tilia::Xml::ParseException, 'We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'
|
121
|
+
else
|
122
|
+
# Advance to the next element
|
123
|
+
read
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
pop_context unless element_map.nil?
|
128
|
+
|
129
|
+
elements.any? ? elements : text
|
130
|
+
end
|
131
|
+
|
132
|
+
# Reads all text below the current element, and returns this as a string.
|
133
|
+
#
|
134
|
+
# @return [String]
|
135
|
+
def read_text
|
136
|
+
result = ''
|
137
|
+
previous_depth = depth
|
138
|
+
|
139
|
+
while read && depth != previous_depth
|
140
|
+
result += value if [
|
141
|
+
::LibXML::XML::Reader::TYPE_TEXT,
|
142
|
+
::LibXML::XML::Reader::TYPE_CDATA,
|
143
|
+
::LibXML::XML::Reader::TYPE_WHITESPACE
|
144
|
+
].include? node_type
|
145
|
+
end
|
146
|
+
|
147
|
+
result
|
148
|
+
end
|
149
|
+
|
150
|
+
# Parses the current XML element.
|
151
|
+
#
|
152
|
+
# This method returns arn array with 3 properties:
|
153
|
+
# * name - A clark-notation XML element name.
|
154
|
+
# * value - The parsed value.
|
155
|
+
# * attributes - A key-value list of attributes.
|
156
|
+
#
|
157
|
+
# @return [Hash]
|
158
|
+
def parse_current_element
|
159
|
+
name = clark
|
160
|
+
|
161
|
+
attributes = {}
|
162
|
+
|
163
|
+
attributes = parse_attributes if self.has_attributes?
|
164
|
+
|
165
|
+
if @element_map.key? name
|
166
|
+
deserializer = @element_map[name]
|
167
|
+
|
168
|
+
if deserializer.is_a?(Class) && deserializer.include?(XmlDeserializable)
|
169
|
+
value = deserializer.xml_deserialize(self)
|
170
|
+
elsif deserializer.is_a? Proc
|
171
|
+
value = deserializer.call(self)
|
172
|
+
else
|
173
|
+
# Omit php stuff for error creation
|
174
|
+
fail "Could not use this type as a deserializer: #{deserializer.inspect}"
|
175
|
+
end
|
176
|
+
else
|
177
|
+
value = Element::Base.xml_deserialize(self)
|
178
|
+
end
|
179
|
+
{
|
180
|
+
'name' => name,
|
181
|
+
'value' => value,
|
182
|
+
'attributes' => attributes
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
# Grabs all the attributes from the current element, and returns them as a
|
187
|
+
# key-value array.
|
188
|
+
#
|
189
|
+
# If the attributes are part of the same namespace, they will simply be
|
190
|
+
# short keys. If they are defined on a different namespace, the attribute
|
191
|
+
# name will be retured in clark-notation.
|
192
|
+
#
|
193
|
+
# @return [Hash]
|
194
|
+
def parse_attributes
|
195
|
+
attributes = {}
|
196
|
+
|
197
|
+
while move_to_next_attribute != 0
|
198
|
+
if namespace_uri
|
199
|
+
# Ignoring 'xmlns', it doesn't make any sense.
|
200
|
+
next if namespace_uri == 'http://www.w3.org/2000/xmlns/'
|
201
|
+
|
202
|
+
name = clark
|
203
|
+
attributes[name] = value
|
204
|
+
else
|
205
|
+
attributes[local_name] = value
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
move_to_element
|
210
|
+
|
211
|
+
attributes
|
212
|
+
end
|
213
|
+
|
214
|
+
# TODO: document this
|
215
|
+
def initialize
|
216
|
+
initialize_context_stack_attributes
|
217
|
+
end
|
218
|
+
|
219
|
+
# TODO: documentation
|
220
|
+
def xml(input)
|
221
|
+
fail 'XML document already loaded' if @reader
|
222
|
+
|
223
|
+
if input.is_a? String
|
224
|
+
@reader = ::LibXML::XML::Reader.string(input)
|
225
|
+
elsif input.is_a? File
|
226
|
+
@reader = ::LibXML::XML::Reader.file(input)
|
227
|
+
elsif input.is_a? StringIO
|
228
|
+
@reader = ::LibXML::XML::Reader.io(input)
|
229
|
+
else
|
230
|
+
fail 'Unable to load XML document'
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# TODO: documentation
|
235
|
+
def method_missing(name, *args)
|
236
|
+
@reader.send(name, *args)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Xml
|
3
|
+
# XML parsing and writing service.
|
4
|
+
#
|
5
|
+
# You are encouraged to make a instance of this for your application and
|
6
|
+
# potentially extend it, as a central API point for dealing with xml and
|
7
|
+
# configuring the reader and writer.
|
8
|
+
class Service
|
9
|
+
# This is the element map. It contains a list of XML elements (in clark
|
10
|
+
# notation) as keys and PHP class names as values.
|
11
|
+
#
|
12
|
+
# The PHP class names must implement Sabre\Xml\Element.
|
13
|
+
#
|
14
|
+
# Values may also be a callable. In that case the function will be called
|
15
|
+
# directly.
|
16
|
+
#
|
17
|
+
# @return [Hash]
|
18
|
+
attr_accessor :element_map
|
19
|
+
|
20
|
+
# This is a list of namespaces that you want to give default prefixes.
|
21
|
+
#
|
22
|
+
# You must make sure you create this entire list before starting to write.
|
23
|
+
# They should be registered on the root element.
|
24
|
+
#
|
25
|
+
# @return [Hash]
|
26
|
+
attr_accessor :namespace_map
|
27
|
+
|
28
|
+
# Returns a fresh XML Reader
|
29
|
+
#
|
30
|
+
# @return [Reader]
|
31
|
+
def reader
|
32
|
+
reader = Reader.new
|
33
|
+
reader.element_map = @element_map
|
34
|
+
reader
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a fresh xml writer
|
38
|
+
#
|
39
|
+
# @return [Writer]
|
40
|
+
def writer
|
41
|
+
writer = Writer.new
|
42
|
+
writer.namespace_map = @namespace_map
|
43
|
+
writer
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses a document in full.
|
47
|
+
#
|
48
|
+
# Input may be specified as a string or readable stream resource.
|
49
|
+
# The returned value is the value of the root document.
|
50
|
+
#
|
51
|
+
# Specifying the context_uri allows the parser to figure out what the URI
|
52
|
+
# of the document was. This allows relative URIs within the document to be
|
53
|
+
# expanded easily.
|
54
|
+
#
|
55
|
+
# The root_element_name is specified by reference and will be populated
|
56
|
+
# with the root element name of the document.
|
57
|
+
#
|
58
|
+
# @param [String, File, StringIO] input
|
59
|
+
# @param [String, nil] context_uri
|
60
|
+
# @param [String, nil] root_element_name
|
61
|
+
# @raise [ParseException]
|
62
|
+
# @return [Array, Object, String]
|
63
|
+
def parse(input, context_uri = nil, root_element_name = nil)
|
64
|
+
# Skip php short commings
|
65
|
+
reader = self.reader
|
66
|
+
reader.context_uri = context_uri
|
67
|
+
reader.xml(input)
|
68
|
+
|
69
|
+
result = reader.parse
|
70
|
+
root_element_name.replace(result['name'])
|
71
|
+
result['value']
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parses a document in full, and specify what the expected root element
|
75
|
+
# name is.
|
76
|
+
#
|
77
|
+
# This function works similar to parse, but the difference is that the
|
78
|
+
# user can specify what the expected name of the root element should be,
|
79
|
+
# in clark notation.
|
80
|
+
#
|
81
|
+
# This is useful in cases where you expected a specific document to be
|
82
|
+
# passed, and reduces the amount of if statements.
|
83
|
+
#
|
84
|
+
# @param [String] root_element_name
|
85
|
+
# @param [String, File, StringIO] input
|
86
|
+
# @param [String, nil] context_uri
|
87
|
+
# @return [void]
|
88
|
+
def expect(root_element_name, input, context_uri = nil)
|
89
|
+
# Skip php short commings
|
90
|
+
reader = self.reader
|
91
|
+
reader.context_uri = context_uri
|
92
|
+
reader.xml(input)
|
93
|
+
|
94
|
+
result = reader.parse
|
95
|
+
if root_element_name != result['name']
|
96
|
+
fail Tilia::Xml::ParseException, "Expected #{root_element_name} but received #{result['name']} as the root element"
|
97
|
+
end
|
98
|
+
result['value']
|
99
|
+
end
|
100
|
+
|
101
|
+
# Generates an XML document in one go.
|
102
|
+
#
|
103
|
+
# The $rootElement must be specified in clark notation.
|
104
|
+
# The value must be a string, an array or an object implementing
|
105
|
+
# XmlSerializable. Basically, anything that's supported by the Writer
|
106
|
+
# object.
|
107
|
+
#
|
108
|
+
# context_uri can be used to specify a sort of 'root' of the PHP application,
|
109
|
+
# in case the xml document is used as a http response.
|
110
|
+
#
|
111
|
+
# This allows an implementor to easily create URI's relative to the root
|
112
|
+
# of the domain.
|
113
|
+
#
|
114
|
+
# @param [String] root_element_name
|
115
|
+
# @param [String, Array, XmlSerializable] value
|
116
|
+
# @param [String, nil] context_uri
|
117
|
+
# @return [void]
|
118
|
+
def write(root_element_name, value, context_uri = nil)
|
119
|
+
writer = self.writer
|
120
|
+
writer.open_memory
|
121
|
+
writer.context_uri = context_uri
|
122
|
+
writer.set_indent(true)
|
123
|
+
writer.start_document
|
124
|
+
writer.write_element(root_element_name, value)
|
125
|
+
writer.output_memory
|
126
|
+
end
|
127
|
+
|
128
|
+
# Parses a clark-notation string, and returns the namespace and element
|
129
|
+
# name components.
|
130
|
+
#
|
131
|
+
# If the string was invalid, it will throw an InvalidArgumentException.
|
132
|
+
#
|
133
|
+
# @param [String] str
|
134
|
+
# @raise [InvalidArgumentException]
|
135
|
+
# @return [Array]
|
136
|
+
def self.parse_clark_notation(str)
|
137
|
+
if str =~ /^{([^}]*)}(.*)/
|
138
|
+
[Regexp.last_match[1], Regexp.last_match[2]]
|
139
|
+
else
|
140
|
+
fail ArgumentError, "'#{str}' is not a valid clark-notation formatted string"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO: document
|
145
|
+
def initialize
|
146
|
+
@element_map = {}
|
147
|
+
@namespace_map = {}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
module Tilia
|
3
|
+
module Xml
|
4
|
+
# The XML Writer class.
|
5
|
+
#
|
6
|
+
# This class works exactly as PHP's built-in XMLWriter, with a few additions.
|
7
|
+
#
|
8
|
+
# Namespaces can be registered beforehand, globally. When the first element is
|
9
|
+
# written, namespaces will automatically be declared.
|
10
|
+
#
|
11
|
+
# The write_attribute, startElement and write_element can now take a
|
12
|
+
# clark-notation element name (example: {http://www.w3.org/2005/Atom}link).
|
13
|
+
#
|
14
|
+
# If, when writing the namespace is a known one a prefix will automatically be
|
15
|
+
# selected, otherwise a random prefix will be generated.
|
16
|
+
#
|
17
|
+
# Instead of standard string values, the writer can take Element classes (as
|
18
|
+
# defined by this library) to delegate the serialization.
|
19
|
+
#
|
20
|
+
# The write() method can take array structures to quickly write out simple xml
|
21
|
+
# trees.
|
22
|
+
class Writer
|
23
|
+
include ContextStackTrait
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# Any namespace that the writer is asked to write, will be added here.
|
28
|
+
#
|
29
|
+
# Any of these elements will get a new namespace definition *every single
|
30
|
+
# time* they are used, but this array allows the writer to make sure that
|
31
|
+
# the prefixes are consistent anyway.
|
32
|
+
#
|
33
|
+
# @return [Hash]
|
34
|
+
attr_accessor :adhoc_namespaces
|
35
|
+
|
36
|
+
# When the first element is written, this flag is set to true.
|
37
|
+
#
|
38
|
+
# This ensures that the namespaces in the namespaces map are only written
|
39
|
+
# once.
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
attr_accessor :namespaces_written
|
43
|
+
|
44
|
+
public
|
45
|
+
|
46
|
+
# Writes a value to the output stream.
|
47
|
+
#
|
48
|
+
# The following values are supported:
|
49
|
+
# 1. Scalar values will be written as-is, as text.
|
50
|
+
# 2. Null values will be skipped (resulting in a short xml tag).
|
51
|
+
# 3. If a value is an instance of an Element class, writing will be
|
52
|
+
# delegated to the object.
|
53
|
+
# 4. If a value is an array, two formats are supported.
|
54
|
+
#
|
55
|
+
# Array format 1:
|
56
|
+
# [
|
57
|
+
# "{namespace}name1" => "..",
|
58
|
+
# "{namespace}name2" => "..",
|
59
|
+
# ]
|
60
|
+
#
|
61
|
+
# One element will be created for each key in this array. The values of
|
62
|
+
# this array support any format this method supports (this method is
|
63
|
+
# called recursively).
|
64
|
+
#
|
65
|
+
# Array format 2:
|
66
|
+
#
|
67
|
+
# [
|
68
|
+
# [
|
69
|
+
# "name" => "{namespace}name1"
|
70
|
+
# "value" => "..",
|
71
|
+
# "attributes" => [
|
72
|
+
# "attr" => "attribute value",
|
73
|
+
# ]
|
74
|
+
# ],
|
75
|
+
# [
|
76
|
+
# "name" => "{namespace}name1"
|
77
|
+
# "value" => "..",
|
78
|
+
# "attributes" => [
|
79
|
+
# "attr" => "attribute value",
|
80
|
+
# ]
|
81
|
+
# ]
|
82
|
+
# ]
|
83
|
+
#
|
84
|
+
# @param value
|
85
|
+
# @return [void]
|
86
|
+
def write(value)
|
87
|
+
if value.is_a?(Numeric) || value.is_a?(String)
|
88
|
+
write_string(value.to_s)
|
89
|
+
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
90
|
+
write_string(value.to_s)
|
91
|
+
elsif value.is_a? XmlSerializable
|
92
|
+
value.xml_serialize(self)
|
93
|
+
elsif value.nil?
|
94
|
+
# noop
|
95
|
+
elsif value.is_a?(Hash) || value.is_a?(Array)
|
96
|
+
# Code for ruby implementation
|
97
|
+
if value.is_a?(Array)
|
98
|
+
hash = {}
|
99
|
+
value.each_with_index do |v, i|
|
100
|
+
hash[i] = v
|
101
|
+
end
|
102
|
+
value = hash
|
103
|
+
end
|
104
|
+
|
105
|
+
value.each do |name, item|
|
106
|
+
if name.is_a? Fixnum
|
107
|
+
# This item has a numeric index. We expect to be an array with a name and a value.
|
108
|
+
unless item.is_a?(Hash) && item.key?('name') && item.key?('value')
|
109
|
+
fail ArgumentError, 'When passing an array to ->write with numeric indices, every item must be an array containing the "name" and "value" key'
|
110
|
+
end
|
111
|
+
|
112
|
+
attributes = item.key?('attributes') ? item['attributes'] : []
|
113
|
+
name = item['name']
|
114
|
+
item = item['value']
|
115
|
+
elsif item.is_a?(Hash) && item.key?('value')
|
116
|
+
# This item has a text index. We expect to be an array with a value and optional attributes.
|
117
|
+
attributes = item.key?('attributes') ? item['attributes'] : []
|
118
|
+
item = item['value']
|
119
|
+
else
|
120
|
+
# If it's an array with text-indices, we expect every item's
|
121
|
+
# key to be an xml element name in clark notation.
|
122
|
+
# No attributes can be passed.
|
123
|
+
attributes = []
|
124
|
+
end
|
125
|
+
|
126
|
+
start_element(name)
|
127
|
+
write_attributes(attributes)
|
128
|
+
write(item)
|
129
|
+
end_element
|
130
|
+
end
|
131
|
+
else
|
132
|
+
fail ArgumentError, "The writer cannot serialize objects of type: #{value.class}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Starts an element.
|
137
|
+
#
|
138
|
+
# @param [String] name
|
139
|
+
# @return [Boolean]
|
140
|
+
def start_element(name)
|
141
|
+
if name[0] == '{'
|
142
|
+
(namespace, local_name) = Service.parse_clark_notation(name)
|
143
|
+
|
144
|
+
if @namespace_map.key? namespace
|
145
|
+
result = start_element_ns(@namespace_map[namespace], local_name, nil)
|
146
|
+
else
|
147
|
+
# An empty namespace means it's the global namespace. This is
|
148
|
+
# allowed, but it mustn't get a prefix.
|
149
|
+
if namespace == ''
|
150
|
+
result = start_element(local_name)
|
151
|
+
write_attribute('xmlns', '')
|
152
|
+
else
|
153
|
+
unless @adhoc_namespaces.key? namespace
|
154
|
+
@adhoc_namespaces[namespace] = 'x' + (@adhoc_namespaces.size + 1).to_s
|
155
|
+
end
|
156
|
+
result = start_element_ns(@adhoc_namespaces[namespace], local_name, namespace)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
else
|
160
|
+
result = @writer.start_element(name)
|
161
|
+
end
|
162
|
+
|
163
|
+
unless @namespaces_written
|
164
|
+
@namespace_map.each do |ns, prefix|
|
165
|
+
write_attribute((prefix ? 'xmlns:' + prefix : 'xmlns'), ns)
|
166
|
+
end
|
167
|
+
@namespaces_written = true
|
168
|
+
end
|
169
|
+
|
170
|
+
result
|
171
|
+
end
|
172
|
+
|
173
|
+
# Write a full element tag.
|
174
|
+
#
|
175
|
+
# This method automatically closes the element as well.
|
176
|
+
#
|
177
|
+
# @param [String] name
|
178
|
+
# @param [String] content
|
179
|
+
# @return [Boolean]
|
180
|
+
def write_element(name, content = nil)
|
181
|
+
start_element(name)
|
182
|
+
write(content) unless content.nil?
|
183
|
+
end_element
|
184
|
+
end
|
185
|
+
|
186
|
+
# Writes a list of attributes.
|
187
|
+
#
|
188
|
+
# Attributes are specified as a key->value array.
|
189
|
+
#
|
190
|
+
# The key is an attribute name. If the key is a 'localName', the current
|
191
|
+
# xml namespace is assumed. If it's a 'clark notation key', this namespace
|
192
|
+
# will be used instead.
|
193
|
+
#
|
194
|
+
# @param [Hash] attributes
|
195
|
+
# @return [void]
|
196
|
+
def write_attributes(attributes)
|
197
|
+
attributes.each do |name, value|
|
198
|
+
write_attribute(name, value)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Writes a new attribute.
|
203
|
+
#
|
204
|
+
# The name may be specified in clark-notation.
|
205
|
+
#
|
206
|
+
# Returns true when successful.
|
207
|
+
#
|
208
|
+
# @param [String] name
|
209
|
+
# @param [String] value
|
210
|
+
# @return [Boolean]
|
211
|
+
def write_attribute(name, value)
|
212
|
+
if name[0] == '{'
|
213
|
+
(namespace, local_name) = Service.parse_clark_notation(name)
|
214
|
+
if @namespace_map.key? namespace
|
215
|
+
# It's an attribute with a namespace we know
|
216
|
+
write_attribute(
|
217
|
+
@namespace_map[namespace] + ':' + local_name,
|
218
|
+
value
|
219
|
+
)
|
220
|
+
else
|
221
|
+
# We don't know the namespace, we must add it in-line
|
222
|
+
@adhoc_namespaces[namespace] = 'x' + (@adhoc_namespaces.size + 1).to_s unless @adhoc_namespaces.key?(namespace)
|
223
|
+
|
224
|
+
write_attribute_ns(
|
225
|
+
@adhoc_namespaces[namespace],
|
226
|
+
local_name,
|
227
|
+
namespace,
|
228
|
+
value
|
229
|
+
)
|
230
|
+
end
|
231
|
+
else
|
232
|
+
@writer.write_attribute(name, value)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# TODO: document this
|
237
|
+
def initialize
|
238
|
+
@adhoc_namespaces = {}
|
239
|
+
@namespaces_written = false
|
240
|
+
initialize_context_stack_attributes
|
241
|
+
end
|
242
|
+
|
243
|
+
# TODO: documentation
|
244
|
+
def open_memory
|
245
|
+
fail 'XML document already created' if @writer
|
246
|
+
|
247
|
+
@writer = ::LibXML::XML::Writer.string
|
248
|
+
end
|
249
|
+
|
250
|
+
# TODO: documentation
|
251
|
+
def output_memory
|
252
|
+
@writer.result
|
253
|
+
end
|
254
|
+
|
255
|
+
# TODO: documentation
|
256
|
+
def method_missing(name, *args)
|
257
|
+
@writer.send(name, *args)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Xml
|
3
|
+
# Implementing the XmlDeserializable interface allows you to use a class as a
|
4
|
+
# deserializer for a specific element.
|
5
|
+
module XmlDeserializable
|
6
|
+
# The deserialize method is called during xml parsing.
|
7
|
+
#
|
8
|
+
# This method is called statictly, this is because in theory this method
|
9
|
+
# may be used as a type of constructor, or factory method.
|
10
|
+
#
|
11
|
+
# Often you want to return an instance of the current class, but you are
|
12
|
+
# free to return other data as well.
|
13
|
+
#
|
14
|
+
# You are responsible for advancing the reader to the next element. Not
|
15
|
+
# doing anything will result in a never-ending loop.
|
16
|
+
#
|
17
|
+
# If you just want to skip parsing for this element altogether, you can
|
18
|
+
# just call $reader->next();
|
19
|
+
#
|
20
|
+
# $reader->parseInnerTree() will parse the entire sub-tree, and advance to
|
21
|
+
# the next element.
|
22
|
+
#
|
23
|
+
# @param [Reader] _reader
|
24
|
+
# @return mixed
|
25
|
+
def xml_deserialize(_reader)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|