tilia-xml 1.2.0.2 → 1.3.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.
@@ -0,0 +1,194 @@
1
+ module Tilia
2
+ module Xml
3
+ # This file provides a number of 'serializer' helper functions.
4
+ #
5
+ # These helper functions can be used to easily xml-encode common PHP
6
+ # data structures, or can be placed in the class_map.
7
+ module Serializer
8
+ # The 'enum' serializer writes simple list of elements.
9
+ #
10
+ # For example, calling:
11
+ #
12
+ # enum(writer, [
13
+ # "{http://sabredav.org/ns}elem1",
14
+ # "{http://sabredav.org/ns}elem2",
15
+ # "{http://sabredav.org/ns}elem3",
16
+ # "{http://sabredav.org/ns}elem4",
17
+ # "{http://sabredav.org/ns}elem5",
18
+ # ])
19
+ #
20
+ # Will generate something like this (if the correct namespace is declared):
21
+ #
22
+ # <s:elem1 />
23
+ # <s:elem2 />
24
+ # <s:elem3 />
25
+ # <s:elem4>content</s:elem4>
26
+ # <s:elem5 attr="val" />
27
+ #
28
+ # @param [Writer] writer
29
+ # @param [Array<String>] values
30
+ # @return [void]
31
+ def self.enum(writer, values)
32
+ values.each do |value|
33
+ writer.write_element(value)
34
+ end
35
+ end
36
+
37
+ # The valueObject serializer turns a simple PHP object into a classname.
38
+ #
39
+ # Every public property will be encoded as an xml element with the same
40
+ # name, in the XML namespace as specified.
41
+ #
42
+ # @param [Writer] writer
43
+ # @param [Object] value_object
44
+ # @param [String] namespace
45
+ def self.value_object(writer, value_object, namespace)
46
+ value_object.instance_variables.each do |key|
47
+ value = value_object.instance_variable_get(key)
48
+
49
+ # key is a symbol and starts with @
50
+ key = key.to_s[1..-1]
51
+
52
+ writer.write_element("{#{namespace}}#{key}", value)
53
+ end
54
+ end
55
+
56
+ # This serializer helps you serialize xml structures that look like
57
+ # this:
58
+ #
59
+ # <collection>
60
+ # <item>...</item>
61
+ # <item>...</item>
62
+ # <item>...</item>
63
+ # </collection>
64
+ #
65
+ # In that previous example, this serializer just serializes the item element,
66
+ # and this could be called like this:
67
+ #
68
+ # repeating_elements(writer, items, '{}item')
69
+ #
70
+ # @param [Writer] writer
71
+ # @param [Array] items A list of items sabre/xml can serialize.
72
+ # @param [String] child_element_name Element name in clark-notation
73
+ # @return [void]
74
+ def self.repeating_elements(writer, items, child_element_name)
75
+ items.each do |item|
76
+ writer.write_element(child_element_name, item)
77
+ end
78
+ end
79
+
80
+ # This function is the 'default' serializer that is able to serialize most
81
+ # things, and delegates to other serializers if needed.
82
+ #
83
+ # The standardSerializer supports a wide-array of values.
84
+ #
85
+ # value may be a string or integer, it will just write out the string as text.
86
+ # value may be an instance of XmlSerializable or Element, in which case it
87
+ # calls it's xml_serialize method.
88
+ # value may be a PHP callback/function/closure, in case we call the callback
89
+ # and give it the Writer as an argument.
90
+ # value may be a an object, and if it's in the classMap we automatically call
91
+ # the correct serializer for it.
92
+ # value may be null, in which case we do nothing.
93
+ #
94
+ # If value is an array, the array must look like this:
95
+ #
96
+ # [
97
+ # [
98
+ # 'name' => '{namespaceUri}element-name',
99
+ # 'value' => '...',
100
+ # 'attributes' => [ 'attName' => 'attValue' ]
101
+ # ]
102
+ # [,
103
+ # 'name' => '{namespaceUri}element-name2',
104
+ # 'value' => '...',
105
+ # ]
106
+ # ]
107
+ #
108
+ # This would result in xml like:
109
+ #
110
+ # <element-name xmlns="namespaceUri" attName="attValue">
111
+ # ...
112
+ # </element-name>
113
+ # <element-name2>
114
+ # ...
115
+ # </element-name2>
116
+ #
117
+ # The value property may be any value standardSerializer supports, so you can
118
+ # nest data-structures this way. Both value and attributes are optional.
119
+ #
120
+ # Alternatively, you can also specify the array using this syntax:
121
+ #
122
+ # [
123
+ # [
124
+ # '{namespaceUri}element-name' => '...',
125
+ # '{namespaceUri}element-name2' => '...',
126
+ # ]
127
+ # ]
128
+ #
129
+ # This is excellent for simple key.value structures, and here you can also
130
+ # specify anything for the value.
131
+ #
132
+ # You can even mix the two array syntaxes.
133
+ #
134
+ # @param [Writer] writer
135
+ # @param value
136
+ # @return [void]
137
+ def self.standard_serializer(writer, value)
138
+ if value.is_a?(Numeric) || value.is_a?(String)
139
+ writer.write_string(value.to_s)
140
+ elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
141
+ writer.write_string(value.to_s)
142
+ elsif value.is_a?(XmlSerializable)
143
+ value.xml_serialize(writer)
144
+ elsif writer.class_map.key?(value.class)
145
+ # It's an object which class appears in the classmap.
146
+ writer.class_map[value.class].call(writer, value)
147
+ elsif value.respond_to?(:call)
148
+ # A callback
149
+ value.call(writer)
150
+ elsif value.nil?
151
+ # noop
152
+ elsif value.is_a?(Hash) || value.is_a?(Array)
153
+ # Code for ruby implementation
154
+ if value.is_a?(Array)
155
+ hash = {}
156
+ value.each_with_index do |v, i|
157
+ hash[i] = v
158
+ end
159
+ value = hash
160
+ end
161
+
162
+ value.each do |name, item|
163
+ if name.is_a? Fixnum
164
+ # This item has a numeric index. We expect to be an array with a name and a value.
165
+ unless item.is_a?(Hash) && item.key?('name')
166
+ raise ArgumentError, 'When passing an array to ->write with numeric indices, every item must be an array containing at least the "name" key'
167
+ end
168
+
169
+ attributes = item.key?('attributes') ? item['attributes'] : []
170
+ name = item['name']
171
+ item = item['value'] || []
172
+ elsif item.is_a?(Hash) && item.key?('value')
173
+ # This item has a text index. We expect to be an array with a value and optional attributes.
174
+ attributes = item.key?('attributes') ? item['attributes'] : []
175
+ item = item['value']
176
+ else
177
+ # If it's an array with text-indices, we expect every item's
178
+ # key to be an xml element name in clark notation.
179
+ # No attributes can be passed.
180
+ attributes = []
181
+ end
182
+
183
+ writer.start_element(name)
184
+ writer.write_attributes(attributes)
185
+ writer.write(item)
186
+ writer.end_element
187
+ end
188
+ else
189
+ raise ArgumentError, "The writer cannot serialize objects of type: #{value.class}"
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -25,6 +25,32 @@ module Tilia
25
25
  # @return [Hash]
26
26
  attr_accessor :namespace_map
27
27
 
28
+ # This is a list of custom serializers for specific classes.
29
+ #
30
+ # The writer may use this if you attempt to serialize an object with a
31
+ # class that does not implement XmlSerializable.
32
+ #
33
+ # Instead it will look at this classmap to see if there is a custom
34
+ # serializer here. This is useful if you don't want your value objects
35
+ # to be responsible for serializing themselves.
36
+ #
37
+ # The keys in this classmap need to be fully qualified PHP class names,
38
+ # the values must be callbacks. The callbacks take two arguments. The
39
+ # writer class, and the value that must be written.
40
+ #
41
+ # function (Writer writer, object value)
42
+ #
43
+ # @return [Hash]
44
+ attr_accessor :class_map
45
+
46
+ # Initializes the xml service
47
+ def initialize
48
+ @element_map = {}
49
+ @namespace_map = {}
50
+ @class_map = {}
51
+ @value_object_map = {}
52
+ end
53
+
28
54
  # Returns a fresh XML Reader
29
55
  #
30
56
  # @return [Reader]
@@ -40,6 +66,7 @@ module Tilia
40
66
  def writer
41
67
  writer = Writer.new
42
68
  writer.namespace_map = @namespace_map
69
+ writer.class_map = @class_map
43
70
  writer
44
71
  end
45
72
 
@@ -57,7 +84,7 @@ module Tilia
57
84
  #
58
85
  # @param [String, File, StringIO] input
59
86
  # @param [String, nil] context_uri
60
- # @param [String, nil] root_element_name
87
+ # @param [Tilia::Box, nil] root_element_name
61
88
  # @raise [ParseException]
62
89
  # @return [Array, Object, String]
63
90
  def parse(input, context_uri = nil, root_element_name = Box.new)
@@ -81,19 +108,20 @@ module Tilia
81
108
  # This is useful in cases where you expected a specific document to be
82
109
  # passed, and reduces the amount of if statements.
83
110
  #
84
- # @param [String] root_element_name
111
+ # @param [String, Array<String>] root_element_name
85
112
  # @param [String, File, StringIO] input
86
113
  # @param [String, nil] context_uri
87
114
  # @return [void]
88
115
  def expect(root_element_name, input, context_uri = nil)
89
- # Skip php short commings
116
+ root_element_name = [root_element_name] unless root_element_name.is_a?(Array)
117
+
90
118
  reader = self.reader
91
119
  reader.context_uri = context_uri
92
120
  reader.xml(input)
93
121
 
94
122
  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"
123
+ unless root_element_name.include?(result['name'])
124
+ raise Tilia::Xml::ParseException, "Expected #{root_element_name.join(' or ')} but received #{result['name']} as the root element"
97
125
  end
98
126
  result['value']
99
127
  end
@@ -125,6 +153,68 @@ module Tilia
125
153
  writer.output_memory
126
154
  end
127
155
 
156
+ # Map an xml element to a PHP class.
157
+ #
158
+ # Calling this function will automatically setup the Reader and Writer
159
+ # classes to turn a specific XML element to a PHP class.
160
+ #
161
+ # For example, given a class such as :
162
+ #
163
+ # class Author {
164
+ # public first_name
165
+ # public last_name
166
+ # }
167
+ #
168
+ # and an XML element such as:
169
+ #
170
+ # <author xmlns="http://example.org/ns">
171
+ # <firstName>...</firstName>
172
+ # <lastName>...</lastName>
173
+ # </author>
174
+ #
175
+ # These can easily be mapped by calling:
176
+ #
177
+ # service.map_value_object('{http://example.org}author', 'Author')
178
+ #
179
+ # @param [String] element_name
180
+ # @param [Class] klass
181
+ # @return [void]
182
+ def map_value_object(element_name, klass)
183
+ namespace = Service.parse_clark_notation(element_name).first
184
+
185
+ @element_map[element_name] = lambda do |reader|
186
+ return Deserializer.value_object(reader, klass, namespace)
187
+ end
188
+ @class_map[klass] = lambda do |writer, value_object|
189
+ return Serializer.value_object(writer, value_object, namespace)
190
+ end
191
+ @value_object_map[klass] = element_name
192
+ end
193
+
194
+ # Writes a value object.
195
+ #
196
+ # This function largely behaves similar to write, except that it's
197
+ # intended specifically to serialize a Value Object into an XML document.
198
+ #
199
+ # The ValueObject must have been previously registered using
200
+ # map_value_object.
201
+ #
202
+ # @param object
203
+ # @param [String] context_uri
204
+ # @return [void]
205
+ def write_value_object(object, context_uri = nil)
206
+ unless @value_object_map.key?(object.class)
207
+ raise ArgumentError,
208
+ "'#{object.class}' is not a registered value object class. Register your class with mapValueObject"
209
+ end
210
+
211
+ write(
212
+ @value_object_map[object.class],
213
+ object,
214
+ context_uri
215
+ )
216
+ end
217
+
128
218
  # Parses a clark-notation string, and returns the namespace and element
129
219
  # name components.
130
220
  #
@@ -132,20 +222,14 @@ module Tilia
132
222
  #
133
223
  # @param [String] str
134
224
  # @raise [InvalidArgumentException]
135
- # @return [Array]
225
+ # @return [Array(String, String)]
136
226
  def self.parse_clark_notation(str)
137
227
  if str =~ /^{([^}]*)}(.*)/
138
- [Regexp.last_match[1], Regexp.last_match[2]]
228
+ [Regexp.last_match[1] || '', Regexp.last_match[2]]
139
229
  else
140
- fail ArgumentError, "'#{str}' is not a valid clark-notation formatted string"
230
+ raise ArgumentError, "'#{str}' is not a valid clark-notation formatted string"
141
231
  end
142
232
  end
143
-
144
- # TODO: document
145
- def initialize
146
- @element_map = {}
147
- @namespace_map = {}
148
- end
149
233
  end
150
234
  end
151
235
  end
@@ -3,7 +3,7 @@ module Tilia
3
3
  # This class contains the version number for this package.
4
4
  class Version
5
5
  # Full version number
6
- VERSION = '1.2.0.2'
6
+ VERSION = '1.3.0'.freeze
7
7
  end
8
8
  end
9
9
  end
@@ -22,27 +22,6 @@ module Tilia
22
22
  class Writer
23
23
  include ContextStackTrait
24
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
25
  # Writes a value to the output stream.
47
26
  #
48
27
  # The following values are supported:
@@ -84,53 +63,7 @@ module Tilia
84
63
  # @param value
85
64
  # @return [void]
86
65
  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
66
+ Serializer.standard_serializer(self, value)
134
67
  end
135
68
 
136
69
  # Starts an element.
@@ -141,20 +74,20 @@ module Tilia
141
74
  if name[0] == '{'
142
75
  (namespace, local_name) = Service.parse_clark_notation(name)
143
76
 
144
- if @namespace_map.key? namespace
145
- result = start_element_ns(@namespace_map[namespace], local_name, nil)
146
- else
77
+ if @namespace_map.key?(namespace)
78
+ tmp_ns = @namespace_map[namespace]
79
+ tmp_ns = nil if tmp_ns.blank?
80
+ result = start_element_ns(tmp_ns, local_name, nil)
81
+ elsif namespace.blank?
147
82
  # An empty namespace means it's the global namespace. This is
148
83
  # 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)
84
+ result = start_element(local_name)
85
+ write_attribute('xmlns', '')
86
+ else
87
+ unless @adhoc_namespaces.key?(namespace)
88
+ @adhoc_namespaces[namespace] = 'x' + (@adhoc_namespaces.size + 1).to_s
157
89
  end
90
+ result = start_element_ns(@adhoc_namespaces[namespace], local_name, namespace)
158
91
  end
159
92
  else
160
93
  result = @writer.start_element(name)
@@ -162,7 +95,7 @@ module Tilia
162
95
 
163
96
  unless @namespaces_written
164
97
  @namespace_map.each do |ns, prefix|
165
- write_attribute((prefix ? 'xmlns:' + prefix : 'xmlns'), ns)
98
+ write_attribute((prefix.present? ? 'xmlns:' + prefix : 'xmlns'), ns)
166
99
  end
167
100
  @namespaces_written = true
168
101
  end
@@ -170,10 +103,24 @@ module Tilia
170
103
  result
171
104
  end
172
105
 
173
- # Write a full element tag.
106
+ # Write a full element tag and it's contents.
174
107
  #
175
108
  # This method automatically closes the element as well.
176
109
  #
110
+ # The element name may be specified in clark-notation.
111
+ #
112
+ # Examples:
113
+ #
114
+ # writer.write_element('{http://www.w3.org/2005/Atom}author',null)
115
+ # becomes:
116
+ # <author xmlns="http://www.w3.org/2005" />
117
+ #
118
+ # writer.write_element('{http://www.w3.org/2005/Atom}author', [
119
+ # '{http://www.w3.org/2005/Atom}name' => 'Evert Pot',
120
+ # ])
121
+ # becomes:
122
+ # <author xmlns="http://www.w3.org/2005" /><name>Evert Pot</name></author>
123
+ #
177
124
  # @param [String] name
178
125
  # @param [String] content
179
126
  # @return [Boolean]
@@ -211,7 +158,7 @@ module Tilia
211
158
  def write_attribute(name, value)
212
159
  if name[0] == '{'
213
160
  (namespace, local_name) = Service.parse_clark_notation(name)
214
- if @namespace_map.key? namespace
161
+ if @namespace_map.key?(namespace)
215
162
  # It's an attribute with a namespace we know
216
163
  write_attribute(
217
164
  @namespace_map[namespace] + ':' + local_name,
@@ -233,26 +180,33 @@ module Tilia
233
180
  end
234
181
  end
235
182
 
236
- # TODO: document this
183
+ # Initializes the instance variables
237
184
  def initialize
238
185
  @adhoc_namespaces = {}
239
186
  @namespaces_written = false
240
- initialize_context_stack_attributes
187
+
188
+ super
241
189
  end
242
190
 
243
- # TODO: documentation
191
+ # Fakes the php function open_memory
192
+ #
193
+ # Initilizes the LibXML Writer
194
+ #
195
+ # @return [void]
244
196
  def open_memory
245
- fail 'XML document already created' if @writer
197
+ raise 'XML document already created' if @writer
246
198
 
247
199
  @writer = ::LibXML::XML::Writer.string
248
200
  end
249
201
 
250
- # TODO: documentation
202
+ # Fakes the php function output_memory
203
+ #
204
+ # @return [String]
251
205
  def output_memory
252
206
  @writer.result
253
207
  end
254
208
 
255
- # TODO: documentation
209
+ # Delegates missing methods to XML::Writer instance
256
210
  def method_missing(name, *args)
257
211
  @writer.send(name, *args)
258
212
  end