tilia-xml 1.2.0.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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