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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +32 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.sabre.md +167 -0
  7. data/CONTRIBUTING.md +25 -0
  8. data/Gemfile +15 -0
  9. data/Gemfile.lock +56 -0
  10. data/LICENSE +27 -0
  11. data/LICENSE.sabre +27 -0
  12. data/README.md +30 -0
  13. data/Rakefile +17 -0
  14. data/lib/tilia/xml/context_stack_trait.rb +99 -0
  15. data/lib/tilia/xml/element/base.rb +73 -0
  16. data/lib/tilia/xml/element/cdata.rb +53 -0
  17. data/lib/tilia/xml/element/elements.rb +109 -0
  18. data/lib/tilia/xml/element/key_value.rb +110 -0
  19. data/lib/tilia/xml/element/uri.rb +98 -0
  20. data/lib/tilia/xml/element/xml_fragment.rb +128 -0
  21. data/lib/tilia/xml/element.rb +22 -0
  22. data/lib/tilia/xml/lib_xml_exception.rb +9 -0
  23. data/lib/tilia/xml/parse_exception.rb +7 -0
  24. data/lib/tilia/xml/reader.rb +240 -0
  25. data/lib/tilia/xml/service.rb +151 -0
  26. data/lib/tilia/xml/version.rb +9 -0
  27. data/lib/tilia/xml/writer.rb +261 -0
  28. data/lib/tilia/xml/xml_deserializable.rb +29 -0
  29. data/lib/tilia/xml/xml_serializable.rb +27 -0
  30. data/lib/tilia/xml.rb +23 -0
  31. data/test/test_helper.rb +4 -0
  32. data/test/xml/context_stack_test.rb +40 -0
  33. data/test/xml/element/cdata_test.rb +37 -0
  34. data/test/xml/element/eater.rb +60 -0
  35. data/test/xml/element/elements_test.rb +113 -0
  36. data/test/xml/element/key_value_test.rb +187 -0
  37. data/test/xml/element/mock.rb +52 -0
  38. data/test/xml/element/uri_test.rb +55 -0
  39. data/test/xml/element/xml_fragment_test.rb +121 -0
  40. data/test/xml/infite_loop_test.rb +47 -0
  41. data/test/xml/reader_test.rb +407 -0
  42. data/test/xml/service_test.rb +156 -0
  43. data/test/xml/writer_test.rb +260 -0
  44. data/tilia-xml.gemspec +15 -0
  45. metadata +132 -0
@@ -0,0 +1,109 @@
1
+ module Tilia
2
+ module Xml
3
+ module Element
4
+ # 'Elements' is a simple list of elements, without values or attributes.
5
+ # For example, Elements will parse:
6
+ #
7
+ # <?xml version="1.0"?>
8
+ # <s:root xmlns:s="http://sabredav.org/ns">
9
+ # <s:elem1 />
10
+ # <s:elem2 />
11
+ # <s:elem3 />
12
+ # <s:elem4>content</s:elem4>
13
+ # <s:elem5 attr="val" />
14
+ # </s:root>
15
+ #
16
+ # Into:
17
+ #
18
+ # [
19
+ # "{http://sabredav.org/ns}elem1",
20
+ # "{http://sabredav.org/ns}elem2",
21
+ # "{http://sabredav.org/ns}elem3",
22
+ # "{http://sabredav.org/ns}elem4",
23
+ # "{http://sabredav.org/ns}elem5",
24
+ # ];
25
+ class Elements
26
+ include Element
27
+
28
+ protected
29
+
30
+ # Value to serialize
31
+ #
32
+ # @return [Array]
33
+ attr_accessor :value
34
+
35
+ public
36
+
37
+ # Constructor
38
+ #
39
+ # @param [Array] value
40
+ def initialize(value = [])
41
+ @value = value
42
+ end
43
+
44
+ # The xml_serialize metod is called during xml writing.
45
+ #
46
+ # Use the writer argument to write its own xml serialization.
47
+ #
48
+ # An important note: do _not_ create a parent element. Any element
49
+ # implementing XmlSerializble should only ever write what's considered
50
+ # its 'inner xml'.
51
+ #
52
+ # The parent of the current element is responsible for writing a
53
+ # containing element.
54
+ #
55
+ # This allows serializers to be re-used for different element names.
56
+ #
57
+ # If you are opening new elements, you must also close them again.
58
+ #
59
+ # @param [Writer] writer
60
+ # @return [void]
61
+ def xml_serialize(writer)
62
+ @value.each do |val|
63
+ writer.write_element(val)
64
+ end
65
+ end
66
+
67
+ # The deserialize method is called during xml parsing.
68
+ #
69
+ # This method is called statictly, this is because in theory this method
70
+ # may be used as a type of constructor, or factory method.
71
+ #
72
+ # Often you want to return an instance of the current class, but you are
73
+ # free to return other data as well.
74
+ #
75
+ # Important note 2: You are responsible for advancing the reader to the
76
+ # next element. Not doing anything will result in a never-ending loop.
77
+ #
78
+ # If you just want to skip parsing for this element altogether, you can
79
+ # just call reader->next();
80
+ #
81
+ # reader->parseSubTree() will parse the entire sub-tree, and advance to
82
+ # the next element.
83
+ #
84
+ # @param [Reader] reader
85
+ # @return mixed
86
+ def self.xml_deserialize(reader)
87
+ # If there's no children, we don't do anything.
88
+ if reader.empty_element?
89
+ reader.next
90
+ return []
91
+ end
92
+ reader.read
93
+ current_depth = reader.depth
94
+
95
+ values = []
96
+ loop do
97
+ if reader.node_type == ::LibXML::XML::Reader::TYPE_ELEMENT
98
+ values << reader.clark
99
+ end
100
+ break unless reader.depth >= current_depth && reader.next
101
+ end
102
+
103
+ reader.next
104
+ values
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ module Tilia
2
+ module Xml
3
+ module Element
4
+ # 'KeyValue' parses out all child elements from a single node, and outputs a
5
+ # key=>value struct.
6
+ #
7
+ # Attributes will be removed, and duplicate child elements are discarded.
8
+ # Complex values within the elements will be parsed by the 'standard' parser.
9
+ #
10
+ # For example, KeyValue will parse:
11
+ #
12
+ # <?xml version="1.0"?>
13
+ # <s:root xmlns:s="http://sabredav.org/ns">
14
+ # <s:elem1>value1</s:elem1>
15
+ # <s:elem2>value2</s:elem2>
16
+ # <s:elem3 />
17
+ # </s:root>
18
+ #
19
+ # Into:
20
+ #
21
+ # [
22
+ # "{http://sabredav.org/ns}elem1" => "value1",
23
+ # "{http://sabredav.org/ns}elem2" => "value2",
24
+ # "{http://sabredav.org/ns}elem3" => null,
25
+ # ];
26
+ class KeyValue
27
+ include Element
28
+
29
+ protected
30
+
31
+ # Value to serialize
32
+ #
33
+ # @return [Array]
34
+ attr_accessor :value
35
+
36
+ public
37
+
38
+ # Constructor
39
+ #
40
+ # @param [Array] value
41
+ def initialize(value = [])
42
+ @value = value
43
+ end
44
+
45
+ # The xml_serialize metod is called during xml writing.
46
+ #
47
+ # Use the writer argument to write its own xml serialization.
48
+ #
49
+ # An important note: do _not_ create a parent element. Any element
50
+ # implementing XmlSerializble should only ever write what's considered
51
+ # its 'inner xml'.
52
+ #
53
+ # The parent of the current element is responsible for writing a
54
+ # containing element.
55
+ #
56
+ # This allows serializers to be re-used for different element names.
57
+ #
58
+ # If you are opening new elements, you must also close them again.
59
+ #
60
+ # @param [Writer] writer
61
+ # @return [void]
62
+ def xml_serialize(writer)
63
+ writer.write(@value)
64
+ end
65
+
66
+ # The deserialize method is called during xml parsing.
67
+ #
68
+ # This method is called staticly, this is because in theory this method
69
+ # may be used as a type of constructor, or factory method.
70
+ #
71
+ # Often you want to return an instance of the current class, but you are
72
+ # free to return other data as well.
73
+ #
74
+ # Important note 2: You are responsible for advancing the reader to the
75
+ # next element. Not doing anything will result in a never-ending loop.
76
+ #
77
+ # If you just want to skip parsing for this element altogether, you can
78
+ # just call reader->next();
79
+ #
80
+ # reader->parseInnerTree() will parse the entire sub-tree, and advance to
81
+ # the next element.
82
+ #
83
+ # @param [Reader] reader
84
+ # @return mixed
85
+ def self.xml_deserialize(reader)
86
+ # If there's no children, we don't do anything.
87
+ if reader.empty_element?
88
+ reader.next
89
+ return {}
90
+ end
91
+
92
+ values = {}
93
+
94
+ reader.read
95
+ loop do
96
+ if reader.node_type == ::LibXML::XML::Reader::TYPE_ELEMENT
97
+ clark = reader.clark
98
+ values[clark] = reader.parse_current_element['value']
99
+ else
100
+ reader.read
101
+ end
102
+ break unless reader.node_type != ::LibXML::XML::Reader::TYPE_END_ELEMENT
103
+ end
104
+ reader.read
105
+ values
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,98 @@
1
+ module Tilia
2
+ module Xml
3
+ module Element
4
+ # Uri element.
5
+ #
6
+ # This represents a single uri. An example of how this may be encoded:
7
+ #
8
+ # <link>/foo/bar</link>
9
+ # <d:href xmlns:d="DAV:">http://example.org/hi</d:href>
10
+ #
11
+ # If the uri is relative, it will be automatically expanded to an absolute
12
+ # url during writing and reading, if the contextUri property is set on the
13
+ # reader and/or writer.
14
+ class Uri
15
+ include Element
16
+
17
+ protected
18
+
19
+ # Uri element value.
20
+ #
21
+ # @return [String]
22
+ attr_accessor :value
23
+
24
+ public
25
+
26
+ # Constructor
27
+ #
28
+ # @param [String] value
29
+ def initialize(value)
30
+ @value = value
31
+ end
32
+
33
+ # The xml_serialize metod is called during xml writing.
34
+ #
35
+ # Use the writer argument to write its own xml serialization.
36
+ #
37
+ # An important note: do _not_ create a parent element. Any element
38
+ # implementing XmlSerializble should only ever write what's considered
39
+ # its 'inner xml'.
40
+ #
41
+ # The parent of the current element is responsible for writing a
42
+ # containing element.
43
+ #
44
+ # This allows serializers to be re-used for different element names.
45
+ #
46
+ # If you are opening new elements, you must also close them again.
47
+ #
48
+ # @param [Writer] writer
49
+ # @return [void]
50
+ def xml_serialize(writer)
51
+ writer.write_string(
52
+ ::Tilia::Uri.resolve(
53
+ writer.context_uri,
54
+ @value
55
+ )
56
+ )
57
+ end
58
+
59
+ # This method is called during xml parsing.
60
+ #
61
+ # This method is called statically, this is because in theory this method
62
+ # may be used as a type of constructor, or factory method.
63
+ #
64
+ # Often you want to return an instance of the current class, but you are
65
+ # free to return other data as well.
66
+ #
67
+ # Important note 2: You are responsible for advancing the reader to the
68
+ # next element. Not doing anything will result in a never-ending loop.
69
+ #
70
+ # If you just want to skip parsing for this element altogether, you can
71
+ # just call reader->next();
72
+ #
73
+ # reader->parseSubTree() will parse the entire sub-tree, and advance to
74
+ # the next element.
75
+ #
76
+ # @param [Reader] reader
77
+ # @return mixed
78
+ def self.xml_deserialize(reader)
79
+ new(
80
+ ::Tilia::Uri.resolve(
81
+ reader.context_uri,
82
+ reader.read_text
83
+ )
84
+ )
85
+ end
86
+
87
+ # TODO: document
88
+ def ==(other)
89
+ if other.is_a? self.class
90
+ other.instance_eval { @value } == @value
91
+ else
92
+ false
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,128 @@
1
+ module Tilia
2
+ module Xml
3
+ module Element
4
+ # The XmlFragment element allows you to extract a portion of your xml tree,
5
+ # and get a well-formed xml string.
6
+ #
7
+ # This goes a bit beyond `innerXml` and friends, as we'll also match all the
8
+ # correct namespaces.
9
+ #
10
+ # Please note that the XML fragment:
11
+ #
12
+ # 1. Will not have an <?xml declaration.
13
+ # 2. Or a DTD
14
+ # 3. It will have all the relevant xmlns attributes.
15
+ # 4. It may not have a root element.
16
+ class XmlFragment
17
+ include Element
18
+
19
+ protected
20
+
21
+ attr_accessor :xml
22
+
23
+ public
24
+
25
+ def initialize(xml)
26
+ @xml = xml
27
+ end
28
+
29
+ # get_xml replaced by xml
30
+
31
+ # The xml_serialize metod is called during xml writing.
32
+ #
33
+ # Use the writer argument to write its own xml serialization.
34
+ #
35
+ # An important note: do _not_ create a parent element. Any element
36
+ # implementing XmlSerializble should only ever write what's considered
37
+ # its 'inner xml'.
38
+ #
39
+ # The parent of the current element is responsible for writing a
40
+ # containing element.
41
+ #
42
+ # This allows serializers to be re-used for different element names.
43
+ #
44
+ # If you are opening new elements, you must also close them again.
45
+ #
46
+ # @param [Writer] writer
47
+ # @return [void]
48
+ def xml_serialize(writer)
49
+ reader = Reader.new
50
+
51
+ # Wrapping the xml in a container, so root-less values can still be
52
+ # parsed.
53
+ xml = <<XML
54
+ <?xml version="1.0"?>
55
+ <xml-fragment xmlns="http://sabre.io/ns">#{@xml}</xml-fragment>
56
+ XML
57
+
58
+ reader.xml(xml)
59
+
60
+ while reader.read
61
+ if reader.depth < 1
62
+ # Skipping the root node.
63
+ next
64
+ end
65
+
66
+ case reader.node_type
67
+ when ::LibXML::XML::Reader::TYPE_ELEMENT
68
+ writer.start_element(reader.clark)
69
+ empty = reader.empty_element?
70
+
71
+ while reader.move_to_next_attribute != 0
72
+ case reader.namespace_uri
73
+ when '', nil # RUBY namespace_uri = nil ...
74
+ writer.write_attribute(reader.local_name, reader.value)
75
+ when 'http://www.w3.org/2000/xmlns/'
76
+ # Skip namespace declarations
77
+ else
78
+ writer.write_attribute(reader.clark, reader.value)
79
+ end
80
+ end
81
+
82
+ writer.end_element if empty
83
+ when ::LibXML::XML::Reader::TYPE_CDATA,
84
+ ::LibXML::XML::Reader::TYPE_TEXT
85
+ writer.write_string(reader.value)
86
+ when ::LibXML::XML::Reader::TYPE_END_ELEMENT
87
+ writer.end_element
88
+ end
89
+ end
90
+ end
91
+
92
+ # The deserialize method is called during xml parsing.
93
+ #
94
+ # This method is called statictly, this is because in theory this method
95
+ # may be used as a type of constructor, or factory method.
96
+ #
97
+ # Often you want to return an instance of the current class, but you are
98
+ # free to return other data as well.
99
+ #
100
+ # You are responsible for advancing the reader to the next element. Not
101
+ # doing anything will result in a never-ending loop.
102
+ #
103
+ # If you just want to skip parsing for this element altogether, you can
104
+ # just call reader->next();
105
+ #
106
+ # reader->parseInnerTree() will parse the entire sub-tree, and advance to
107
+ # the next element.
108
+ #
109
+ # @param [Reader] reader
110
+ # @return mixed
111
+ def self.xml_deserialize(reader)
112
+ result = new(reader.read_inner_xml)
113
+ reader.next
114
+ result
115
+ end
116
+
117
+ # TODO: document
118
+ def ==(other)
119
+ if other.is_a? self.class
120
+ other.xml == @xml
121
+ else
122
+ false
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,22 @@
1
+ module Tilia
2
+ module Xml
3
+ # This is the XML element interface.
4
+ #
5
+ # Elements are responsible for serializing and deserializing part of an XML
6
+ # document into PHP values.
7
+ #
8
+ # It combines XmlSerializable and XmlDeserializable into one logical class
9
+ # that does both.
10
+ module Element
11
+ include XmlSerializable
12
+ include XmlDeserializable
13
+
14
+ require 'tilia/xml/element/base'
15
+ require 'tilia/xml/element/cdata'
16
+ require 'tilia/xml/element/elements'
17
+ require 'tilia/xml/element/key_value'
18
+ require 'tilia/xml/element/uri'
19
+ require 'tilia/xml/element/xml_fragment'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module Tilia
2
+ module Xml
3
+ # This exception is thrown when the Readers runs into a parsing error.
4
+ #
5
+ # This exception effectively wraps 1 or more LibXMLError objects.
6
+ class LibXmlException < ParseException
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Tilia
2
+ module Xml
3
+ # This is a base exception for any exception related to parsing xml files.
4
+ class ParseException < ::StandardError
5
+ end
6
+ end
7
+ end