xmlparsable 1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +80 -0
- data/Rakefile +19 -0
- data/lib/xmlparsable/elements/abstract.rb +21 -0
- data/lib/xmlparsable/elements/date.rb +25 -0
- data/lib/xmlparsable/elements/list.rb +31 -0
- data/lib/xmlparsable/elements/record.rb +48 -0
- data/lib/xmlparsable/elements/string.rb +23 -0
- data/lib/xmlparsable/elements/text.rb +34 -0
- data/lib/xmlparsable/elements/time.rb +25 -0
- data/lib/xmlparsable/elements/xml.rb +79 -0
- data/lib/xmlparsable/elements.rb +12 -0
- data/lib/xmlparsable/parser.rb +42 -0
- data/lib/xmlparsable.rb +74 -0
- data/spec/examples/record.example +229 -0
- data/spec/spec_helper.rb +13 -0
- metadata +60 -0
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# XmlParsable
|
2
|
+
|
3
|
+
XmlParsable is a simple means to declare how an XML grammar maps to Ruby objects. It does not
|
4
|
+
perform validation of the input, and it is only able to describe fairly simple structures. It
|
5
|
+
relies on LibXML::XML::SaxParser.
|
6
|
+
|
7
|
+
1. Declare XML grammar
|
8
|
+
|
9
|
+
require "xmlparsable"
|
10
|
+
|
11
|
+
# Needed only for using the "collection" declaration or
|
12
|
+
# implement your own String#pluralize, String#singularize
|
13
|
+
require "active_support/core_ext"
|
14
|
+
|
15
|
+
class AddressBook
|
16
|
+
include XmlParsable
|
17
|
+
|
18
|
+
class Contact
|
19
|
+
include XmlParsable
|
20
|
+
element :givenname
|
21
|
+
element :familyname
|
22
|
+
element :phone
|
23
|
+
element :email
|
24
|
+
element :spouse, Contact
|
25
|
+
collection :children, Contact
|
26
|
+
element :birthdate, DateElement
|
27
|
+
element :updated, TimeElement
|
28
|
+
end
|
29
|
+
|
30
|
+
collection :contact, Contact
|
31
|
+
end
|
32
|
+
|
33
|
+
2. Parse input XML from string, IO, or File
|
34
|
+
|
35
|
+
book = AddressBook.parse(<<-XML)
|
36
|
+
<contacts>
|
37
|
+
<contact>
|
38
|
+
<familyname>Kim</familyname>
|
39
|
+
<givenname>Jong-il</givenname>
|
40
|
+
<birthdate>1942-02-16</birthdate>
|
41
|
+
<updated>2010-01-01T12:45:30</updated>
|
42
|
+
<children>
|
43
|
+
<child><givenname>Sul-song</givenname></child>
|
44
|
+
<child><givenname>Jong-chul</givenname></child>
|
45
|
+
<child><givenname>Jong-un</givenname></child>
|
46
|
+
<child>
|
47
|
+
<givenname>Jong-nam</givenname>
|
48
|
+
<spouse>
|
49
|
+
<givenname>Jong-hui</givenname>
|
50
|
+
<familyname>Shin</familyname>
|
51
|
+
</spouse>
|
52
|
+
</child>
|
53
|
+
</children>
|
54
|
+
</contact>
|
55
|
+
|
56
|
+
<contact>
|
57
|
+
<givenname>Vladimir</givenname>
|
58
|
+
<familyname>Putin</familyname>
|
59
|
+
<email>vlad@gmail.com</email>
|
60
|
+
<spouse>
|
61
|
+
<givenname>Lyudmila</givenname>
|
62
|
+
<familyname>Putina</familyname>
|
63
|
+
</spouse>
|
64
|
+
</contact>
|
65
|
+
</contacts>
|
66
|
+
XML
|
67
|
+
|
68
|
+
3. Iterate parsed data
|
69
|
+
|
70
|
+
kim, vlad = book.contacts
|
71
|
+
kim.givenname #=> "Kim"
|
72
|
+
kim.birthdate #=> #<Date: 4860813/2,0,2299161>
|
73
|
+
kim.updated #=> Fri Jan 01 12:45:30 2010
|
74
|
+
|
75
|
+
sulsong, jongchul, jongun, jongnam = kim.children
|
76
|
+
jongnam.spouse.givenname #=> "Jong-hui"
|
77
|
+
jongnam.spouse.familyname #=> "Shin"
|
78
|
+
|
79
|
+
vlad.email #=> "vlad@gmail.com"
|
80
|
+
vlad.spouse #=> #<AddressBook::Contact @givenname="Lyudmila" @familyname="Putina">
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
begin
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new do |t|
|
5
|
+
t.verbose = false
|
6
|
+
t.pattern = "spec/examples/**/*.example"
|
7
|
+
t.rspec_opts = ["--color", "--format=p"]
|
8
|
+
end
|
9
|
+
rescue LoadError
|
10
|
+
require "spec/rake/spectask"
|
11
|
+
|
12
|
+
Spec::Rake::SpecTask.new do |t|
|
13
|
+
t.pattern = "spec/examples/**/*.example"
|
14
|
+
t.spec_opts << "--color"
|
15
|
+
t.spec_opts << "--format=p"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => :spec
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module XmlParsable
|
4
|
+
module Elements
|
5
|
+
#
|
6
|
+
# Parses content into a ruby Date value
|
7
|
+
#
|
8
|
+
class DateElement < AbstractElement
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name, attributes, parent, *arguments)
|
12
|
+
@name, @parent, @string = name, parent, ""
|
13
|
+
end
|
14
|
+
|
15
|
+
def read(text)
|
16
|
+
@string << text
|
17
|
+
end
|
18
|
+
|
19
|
+
def finalize
|
20
|
+
stripped = @string.strip
|
21
|
+
@parent.closed(self, Date.parse(stripped)) if stripped != ""
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
|
4
|
+
class ListElement < AbstractElement
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name, attributes, parent, *arguments)
|
8
|
+
@name, @parent, @elements = name, parent, []
|
9
|
+
|
10
|
+
@cname = arguments.shift.to_s
|
11
|
+
@cparser = arguments.shift
|
12
|
+
@cargs = arguments
|
13
|
+
end
|
14
|
+
|
15
|
+
def open(name, attributes)
|
16
|
+
if name == @cname
|
17
|
+
@cparser.parsable.new(name, attributes, self, *@cargs)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def closed(element, value)
|
22
|
+
@elements << value
|
23
|
+
end
|
24
|
+
|
25
|
+
def finalize
|
26
|
+
@parent.closed(self, @elements)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
class RecordElement < AbstractElement
|
4
|
+
attr_reader :name, :attributes, :parent, :target
|
5
|
+
|
6
|
+
def initialize(name, attributes, parent, target, parsers, *arguments)
|
7
|
+
@name = name
|
8
|
+
@attributes = attributes
|
9
|
+
@parent = parent
|
10
|
+
@target = target
|
11
|
+
@parsers = parsers
|
12
|
+
@elements = Hash.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def open(name, attributes)
|
16
|
+
parser, *arguments = @parsers[name]
|
17
|
+
if parser
|
18
|
+
parser.new(name, attributes, self, *arguments)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def closed(element, value)
|
23
|
+
parser, *arguments = @parsers[element.name]
|
24
|
+
if arguments.include?(:repeated)
|
25
|
+
(@elements[element.name] ||= []) << value
|
26
|
+
else
|
27
|
+
@elements[element.name] = value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def finalize
|
32
|
+
@elements.each{|name, value| @target.instance_variable_set("@#{name}", value) }
|
33
|
+
@parent.closed(self, @target) if @parent
|
34
|
+
end
|
35
|
+
|
36
|
+
class Proxy
|
37
|
+
def initialize(base, parsers)
|
38
|
+
@base, @parsers = base, parsers
|
39
|
+
end
|
40
|
+
|
41
|
+
def new(name, attributes, parent, *arguments)
|
42
|
+
RecordElement.new(name, attributes, parent, @base.new, @parsers)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
#
|
4
|
+
# Parses content into a ruby String value. This discards comments and children
|
5
|
+
# elements so "abc <!-- foo --> def <b>whatever</b>" is read as "abc def"
|
6
|
+
#
|
7
|
+
class StringElement < AbstractElement
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name, attributes, parent, *arguments)
|
11
|
+
@name, @parent, @string = name, parent, ""
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(text)
|
15
|
+
@string << text
|
16
|
+
end
|
17
|
+
|
18
|
+
def finalize
|
19
|
+
@parent.closed(self, @string)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
#
|
4
|
+
# Parses content into a ruby String value. Discards comments and flattens children
|
5
|
+
# elements so "abc def <b>what<!-- foo -->ever</b>" is read as "abc def whatever"
|
6
|
+
#
|
7
|
+
class TextElement < XmlElement::Node
|
8
|
+
attr_reader :name, :attributes, :parent
|
9
|
+
|
10
|
+
def initialize(name, attributes, parent, *arguments)
|
11
|
+
@name, @attributes, @parent, @text = name, attributes, parent, ""
|
12
|
+
end
|
13
|
+
|
14
|
+
def open(name, attributes)
|
15
|
+
TextElement.new(name, attributes, self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(text)
|
19
|
+
@text << text
|
20
|
+
end
|
21
|
+
|
22
|
+
def comment(text)
|
23
|
+
end
|
24
|
+
|
25
|
+
def closed(element, value)
|
26
|
+
@text << value
|
27
|
+
end
|
28
|
+
|
29
|
+
def finalize
|
30
|
+
@parent.closed(self, @text)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
module XmlParsable
|
4
|
+
module Elements
|
5
|
+
#
|
6
|
+
# Parses content into a ruby Time value
|
7
|
+
#
|
8
|
+
class TimeElement < AbstractElement
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name, attributes, parent, *arguments)
|
12
|
+
@name, @parent, @string = name, parent, ""
|
13
|
+
end
|
14
|
+
|
15
|
+
def read(text)
|
16
|
+
@string << text
|
17
|
+
end
|
18
|
+
|
19
|
+
def finalize
|
20
|
+
stripped = @string.strip
|
21
|
+
@parent.closed(self, Time.parse(stripped)) if stripped != ""
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
#
|
4
|
+
# Parses content into a generic tree structure, consisting of three
|
5
|
+
# classes -- Node, Text, and Comment.
|
6
|
+
#
|
7
|
+
class XmlElement
|
8
|
+
|
9
|
+
def self.parsable
|
10
|
+
Node
|
11
|
+
end
|
12
|
+
|
13
|
+
class Node < AbstractElement
|
14
|
+
attr_reader :name, :attributes, :parent, :children
|
15
|
+
|
16
|
+
def initialize(name, attributes, parent, *arguments)
|
17
|
+
@parent, @name, @attributes, @children = parent, name, attributes, Array.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def open(name, attributes)
|
21
|
+
Node.new(name, attributes, self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def read(text)
|
25
|
+
@children << Text.new(text, self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def comment(text)
|
29
|
+
@children << Comment.new(text, self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def closed(element, value)
|
33
|
+
@children << value
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalize
|
37
|
+
@parent.closed(self, self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s(buffer = "")
|
41
|
+
buffer << "<#{@name}"
|
42
|
+
@attributes.each{|name, value| buffer << %[ #{name}='#{value}'] }
|
43
|
+
|
44
|
+
if @children.empty?
|
45
|
+
buffer << "/>"
|
46
|
+
else
|
47
|
+
buffer << ">"
|
48
|
+
@children.each{|child| child.to_s(buffer) }
|
49
|
+
buffer << "</#{@name}>"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Text
|
55
|
+
attr_reader :text, :parent
|
56
|
+
|
57
|
+
def initialize(text, parent)
|
58
|
+
@text, @parent = text, parent
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s(buffer = "")
|
62
|
+
buffer << @text
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Comment
|
67
|
+
attr_reader :text, :parent
|
68
|
+
|
69
|
+
def initialize(text, parent)
|
70
|
+
@text, @parent = text, parent
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s(buffer = "")
|
74
|
+
buffer << "<!--" << @text << "-->"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
module Elements
|
3
|
+
autoload :AbstractElement, "xmlparsable/elements/abstract"
|
4
|
+
autoload :DateElement, "xmlparsable/elements/date"
|
5
|
+
autoload :ListElement, "xmlparsable/elements/list"
|
6
|
+
autoload :RecordElement, "xmlparsable/elements/record"
|
7
|
+
autoload :StringElement, "xmlparsable/elements/string"
|
8
|
+
autoload :TextElement, "xmlparsable/elements/text"
|
9
|
+
autoload :TimeElement, "xmlparsable/elements/time"
|
10
|
+
autoload :XmlElement, "xmlparsable/elements/xml"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module XmlParsable
|
2
|
+
class Parser
|
3
|
+
attr_reader :documents
|
4
|
+
|
5
|
+
def initialize(constructed = nil, &constructor)
|
6
|
+
@documents = Array.new
|
7
|
+
@constructor = constructor || lambda { constructed }
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_start_document
|
11
|
+
@stack = [@constructor.call]
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_end_document
|
15
|
+
@documents << @stack.shift.finalize
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_characters(text)
|
19
|
+
@stack.first.read(text)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_comment(comment)
|
23
|
+
@stack.first.comment(comment)
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_start_element_ns(name, attributes, prefix, uri, namespaces)
|
27
|
+
@stack.unshift(@stack.first.open(name, attributes) || StubElement)
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_end_element_ns(name, prefix, uri)
|
31
|
+
@stack.shift.finalize
|
32
|
+
end
|
33
|
+
|
34
|
+
module StubElement
|
35
|
+
def self.open(name, attributes) self end
|
36
|
+
def self.close(element, value) end
|
37
|
+
def self.comment(text) end
|
38
|
+
def self.read(text) end
|
39
|
+
def self.finalize; end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/xmlparsable.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require "libxml"
|
2
|
+
|
3
|
+
module XmlParsable
|
4
|
+
|
5
|
+
autoload :Parser, "xmlparsable/parser"
|
6
|
+
autoload :Elements, "xmlparsable/elements"
|
7
|
+
|
8
|
+
def self.parser(input)
|
9
|
+
case input
|
10
|
+
when String then LibXML::XML::SaxParser.string(input)
|
11
|
+
when File then LibXML::XML::SaxParser.file(input)
|
12
|
+
when IO then LibXML::XML::SaxParser.io(input)
|
13
|
+
else raise ArgumentError, "argument must be a String, File, or IO"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(base)
|
18
|
+
base.extend(ClassMethods)
|
19
|
+
base.__send__(:include, Elements)
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def element(name, *arguments)
|
24
|
+
element ||= arguments.pop.delete(:class) if arguments.last.is_a?(Hash)
|
25
|
+
element ||= arguments.shift if arguments.first.is_a?(Class)
|
26
|
+
element ||= Elements::StringElement
|
27
|
+
|
28
|
+
(@parsers ||= {})[name.to_s] = [element.parsable, *arguments]
|
29
|
+
|
30
|
+
define_method(name) { instance_variable_get("@#{name}") }
|
31
|
+
end
|
32
|
+
|
33
|
+
def repeated(name, *arguments)
|
34
|
+
element ||= arguments.pop.delete(:class) if arguments.last.is_a?(Hash)
|
35
|
+
element ||= arguments.shift if arguments.first.is_a?(Class)
|
36
|
+
element ||= Elements::StringElement
|
37
|
+
|
38
|
+
(@parsers ||= {})[name.to_s.singularize] = [element.parsable, *arguments.push(:repeated)]
|
39
|
+
|
40
|
+
define_method(name) { instance_variable_get("@#{name.to_s.singularize}") }
|
41
|
+
end
|
42
|
+
|
43
|
+
def collection(name, *arguments)
|
44
|
+
item ||= arguments.pop.delete(:class) if arguments.last.is_a?(Hash)
|
45
|
+
item ||= arguments.shift if arguments.first.is_a?(Class)
|
46
|
+
item ||= Elements::StringElement
|
47
|
+
|
48
|
+
if arguments.empty?
|
49
|
+
single = name.to_s.singularize
|
50
|
+
plural = name.to_s.pluralize
|
51
|
+
else
|
52
|
+
plural = name
|
53
|
+
single = arguments.shift
|
54
|
+
end
|
55
|
+
|
56
|
+
element(plural, Elements::ListElement, single, item)
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse(input)
|
60
|
+
root = XmlParsable::Elements::RecordElement.new(nil, nil, nil, new, @parsers)
|
61
|
+
stack = XmlParsable::Parser.new(root)
|
62
|
+
parser = XmlParsable.parser(input)
|
63
|
+
|
64
|
+
parser.callbacks = stack
|
65
|
+
parser.parse
|
66
|
+
root.target
|
67
|
+
end
|
68
|
+
|
69
|
+
def parsable
|
70
|
+
Elements::RecordElement::Proxy.new(self, @parsers)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe XmlParsable::Elements::RecordElement do
|
4
|
+
|
5
|
+
class Record
|
6
|
+
include XmlParsable
|
7
|
+
|
8
|
+
element :date, DateElement
|
9
|
+
element :record, self
|
10
|
+
element :string, StringElement
|
11
|
+
element :text, TextElement
|
12
|
+
element :time, TimeElement
|
13
|
+
element :xml, XmlElement
|
14
|
+
|
15
|
+
collection :kids, self
|
16
|
+
collection :list, :item
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "date" do
|
20
|
+
it "parses date into a ruby Date" do
|
21
|
+
Record.parse("<date>#{Date.today}</date>").date.should == Date.today
|
22
|
+
end
|
23
|
+
|
24
|
+
it "parses missing date as nil" do
|
25
|
+
Record.parse("<date><!-- nope --></date>").date.should == nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it "ignores children" do
|
29
|
+
Record.parse("<date><x>2010-01-01</x></date>").date.should == nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "ignores comments" do
|
33
|
+
Record.parse("<date>#{Date.today} <!-- that's today --></date>").date.should == Date.today
|
34
|
+
end
|
35
|
+
|
36
|
+
it "ignores whitespace" do
|
37
|
+
Record.parse("<date>\n #{Date.today}\n </date>").date.should == Date.today
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "list of strings" do
|
42
|
+
it "parses empty list into an empty Array" do
|
43
|
+
Record.parse("<list/>").list.should == []
|
44
|
+
end
|
45
|
+
|
46
|
+
it "parses non-empty list into an Array" do
|
47
|
+
Record.parse("<list><item>a</item><item>b</item></list>").list.should == ["a", "b"]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "ignores non-matching children" do
|
51
|
+
Record.parse("<list><fail/><item/><boat/><item/><item/></list>").list.should == ["", "", ""]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "ignores comments" do
|
55
|
+
Record.parse("<list><!-- <item>x</item> --><item><!-- y --></item></list>").list.should == [""]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "ignores whitespace between children" do
|
59
|
+
Record.parse(<<-XML).list.should == ["a", "b", "c"]
|
60
|
+
<list>
|
61
|
+
<item>a</item>
|
62
|
+
<item>b</item>
|
63
|
+
<item>c</item>
|
64
|
+
</list>
|
65
|
+
XML
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "list of records" do
|
70
|
+
it "parses empty list into an empty Array" do
|
71
|
+
Record.parse("<kids/>").kids.should == []
|
72
|
+
end
|
73
|
+
|
74
|
+
it "parses non-empty list into an array" do
|
75
|
+
x = Record.parse(<<-XML)
|
76
|
+
<kids>
|
77
|
+
<kid><text>first</text></kid>
|
78
|
+
<kid><text>second</text></kid>
|
79
|
+
<kid><text>third</text></kid>
|
80
|
+
</kids>
|
81
|
+
XML
|
82
|
+
|
83
|
+
x.kids.should have(3).items
|
84
|
+
x.kids[0].text.should == "first"
|
85
|
+
x.kids[1].text.should == "second"
|
86
|
+
x.kids[2].text.should == "third"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "string" do
|
91
|
+
it "parses empty string" do
|
92
|
+
Record.parse("<string></string>").string.should == ""
|
93
|
+
end
|
94
|
+
|
95
|
+
it "parses non-empty string" do
|
96
|
+
Record.parse("<string>lo and behold</string>").string.should == "lo and behold"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "ignores comments" do
|
100
|
+
Record.parse("<string>hi<!-- secret message --></string>").string.should == "hi"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "ignores children" do
|
104
|
+
Record.parse("<string><b>bold</b>, <i>italic</i>, plain</string>").string.should == ", , plain"
|
105
|
+
end
|
106
|
+
|
107
|
+
it "does not ignore whitespace" do
|
108
|
+
Record.parse("<string> foob ar</string>").string.should == " foob ar"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "text" do
|
113
|
+
it "parses text" do
|
114
|
+
Record.parse("<text>lo and behold</text>").text.should == "lo and behold"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "ignores comments" do
|
118
|
+
Record.parse("<text>lo <!--and--> behold</text>").text.should == "lo behold"
|
119
|
+
end
|
120
|
+
|
121
|
+
it "strips away markup" do
|
122
|
+
Record.parse("<text>lo <span class='x'><i>and</i> behold</span></text>").text.should == "lo and behold"
|
123
|
+
end
|
124
|
+
|
125
|
+
it "does not ignore whitespace" do
|
126
|
+
Record.parse("<text> foob ar</text>").text.should == " foob ar"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "time" do
|
131
|
+
it "parses time" do
|
132
|
+
time = Time.now
|
133
|
+
Record.parse("<time>#{time}</time>").time.to_i.should == time.to_i
|
134
|
+
end
|
135
|
+
|
136
|
+
it "ignores comments" do
|
137
|
+
Record.parse("<time><!-- #{Time.now} --></time>").time.should be_nil
|
138
|
+
end
|
139
|
+
|
140
|
+
it "ignores children" do
|
141
|
+
Record.parse("<time><b>#{Time.now}</b></time>").time.should be_nil
|
142
|
+
end
|
143
|
+
|
144
|
+
it "ignores whitespace" do
|
145
|
+
time = Time.now
|
146
|
+
Record.parse("<time> #{time} </time>").time.to_i.should == time.to_i
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "record" do
|
151
|
+
it "parses date" do
|
152
|
+
x = Record.parse("<record><date>#{Date.today}</date></record>")
|
153
|
+
x.date.should be_nil
|
154
|
+
x.record.date.should == Date.today
|
155
|
+
end
|
156
|
+
|
157
|
+
it "parses list" do
|
158
|
+
x = Record.parse("<record><list/></record>")
|
159
|
+
x.list.should be_nil
|
160
|
+
x.record.list.should == []
|
161
|
+
end
|
162
|
+
|
163
|
+
it "parses string" do
|
164
|
+
x = Record.parse("<record><string>abc</string></record>")
|
165
|
+
x.string.should be_nil
|
166
|
+
x.record.string.should == "abc"
|
167
|
+
end
|
168
|
+
|
169
|
+
it "parses text" do
|
170
|
+
x = Record.parse("<record><text><b>big</b><i>little</i></text></record>")
|
171
|
+
x.text.should be_nil
|
172
|
+
x.record.text.should == "biglittle"
|
173
|
+
end
|
174
|
+
|
175
|
+
it "parses time" do
|
176
|
+
time = Time.now
|
177
|
+
Record.parse("<record><time>#{time}</time></record>").record.time.to_i.should == time.to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
it "parses multiple elements" do
|
181
|
+
time = Time.now
|
182
|
+
x = Record.parse <<-XML
|
183
|
+
<record>
|
184
|
+
<date>#{Date.today}</date>
|
185
|
+
<list>
|
186
|
+
<item>a</item>
|
187
|
+
<item>b</item>
|
188
|
+
<item>c</item>
|
189
|
+
</list>
|
190
|
+
<string>xyz</string>
|
191
|
+
<text>
|
192
|
+
<ul>
|
193
|
+
<li>vim</li>
|
194
|
+
<li>emacs</li>
|
195
|
+
<li>ctags</li>
|
196
|
+
<li>lint</li>
|
197
|
+
<li>gdb</li>
|
198
|
+
</ul>
|
199
|
+
</text>
|
200
|
+
<time>#{time}</time>
|
201
|
+
</record>
|
202
|
+
XML
|
203
|
+
|
204
|
+
x.record.date.should == Date.today
|
205
|
+
x.record.list.should == ["a","b","c"]
|
206
|
+
x.record.string.should == "xyz"
|
207
|
+
x.record.text.strip.should == <<-DOC.strip
|
208
|
+
vim
|
209
|
+
emacs
|
210
|
+
ctags
|
211
|
+
lint
|
212
|
+
gdb
|
213
|
+
DOC
|
214
|
+
|
215
|
+
x.record.time.to_i.should == time.to_i
|
216
|
+
end
|
217
|
+
|
218
|
+
it "parses recursive records" do
|
219
|
+
x = Record.parse("<record><record><record><record></record></record></record></record>")
|
220
|
+
x.should be_a(Record)
|
221
|
+
x.record.should be_a(Record)
|
222
|
+
x.record.record.should be_a(Record)
|
223
|
+
x.record.record.record.should be_a(Record)
|
224
|
+
x.record.record.record.record.should be_a(Record)
|
225
|
+
x.record.record.record.record.record.should be_nil
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xmlparsable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kyle Putnam
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-14 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Simple declarative XML parsing
|
15
|
+
email: putnam.kyle@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- Rakefile
|
22
|
+
- lib/xmlparsable/elements/abstract.rb
|
23
|
+
- lib/xmlparsable/elements/date.rb
|
24
|
+
- lib/xmlparsable/elements/list.rb
|
25
|
+
- lib/xmlparsable/elements/record.rb
|
26
|
+
- lib/xmlparsable/elements/string.rb
|
27
|
+
- lib/xmlparsable/elements/text.rb
|
28
|
+
- lib/xmlparsable/elements/time.rb
|
29
|
+
- lib/xmlparsable/elements/xml.rb
|
30
|
+
- lib/xmlparsable/elements.rb
|
31
|
+
- lib/xmlparsable/parser.rb
|
32
|
+
- lib/xmlparsable.rb
|
33
|
+
- spec/examples/record.example
|
34
|
+
- spec/spec_helper.rb
|
35
|
+
homepage: https://github.com/kputnam/xmlparsable
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.8.24
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Simple declarative XML parsing
|
59
|
+
test_files:
|
60
|
+
- spec/examples/record.example
|