objectify-xml 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/Manifest.txt +16 -0
- data/README.txt +120 -0
- data/Rakefile +15 -0
- data/lib/objectify_xml.rb +104 -0
- data/lib/objectify_xml/atom.rb +60 -0
- data/lib/objectify_xml/document_parser.rb +70 -0
- data/lib/objectify_xml/dsl.rb +156 -0
- data/lib/objectify_xml/element_parser.rb +18 -0
- data/spec/atom_spec.rb +63 -0
- data/spec/document_parser_spec.rb +156 -0
- data/spec/dsl_spec.rb +164 -0
- data/spec/element_parser_spec.rb +38 -0
- data/spec/objectify_xml_spec.rb +81 -0
- data/spec/sample/wikipedia.atom +3793 -0
- data/spec/spec_helper.rb +13 -0
- metadata +101 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
lib/objectify_xml.rb
|
6
|
+
lib/objectify_xml/atom.rb
|
7
|
+
lib/objectify_xml/document_parser.rb
|
8
|
+
lib/objectify_xml/dsl.rb
|
9
|
+
lib/objectify_xml/element_parser.rb
|
10
|
+
spec/spec_helper.rb
|
11
|
+
spec/atom_spec.rb
|
12
|
+
spec/document_parser_spec.rb
|
13
|
+
spec/dsl_spec.rb
|
14
|
+
spec/element_parser_spec.rb
|
15
|
+
spec/objectify_xml_spec.rb
|
16
|
+
spec/sample/wikipedia.atom
|
data/README.txt
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
= Objectify::Xml
|
2
|
+
|
3
|
+
* http://github.com/pangloss/objectify_xml
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Provides an easy to use DSL resembling ActiveRecord for defining objects
|
8
|
+
representing any XML document, including deeply nested ones. This project was
|
9
|
+
extracted from my ruby-picasa gem. You can find ruby-picasa at
|
10
|
+
http://github.com/pangloss/ruby_picasa or available as a gem.
|
11
|
+
|
12
|
+
The project also has significant (if not complete) Atom support.
|
13
|
+
|
14
|
+
== FEATURES:
|
15
|
+
|
16
|
+
* Capture and typecast standard attributes
|
17
|
+
* Define both has_one and has_many nested element 'associations'
|
18
|
+
* Significant (if not full) namespace support
|
19
|
+
* Cleanly ignore unknown attributes and namespaces
|
20
|
+
* Support documents that nest data unnecessarily without creating bogus
|
21
|
+
associated objects.
|
22
|
+
* Inheritable object definitions
|
23
|
+
|
24
|
+
== PROBLEMS:
|
25
|
+
|
26
|
+
* None known.
|
27
|
+
|
28
|
+
== SYNOPSIS:
|
29
|
+
|
30
|
+
The following are functioning early definitions for some of the objects used in
|
31
|
+
ruby-picasa in their entirety:
|
32
|
+
|
33
|
+
require 'objectify_xml'
|
34
|
+
require 'objectify_xml/atom'
|
35
|
+
module RubyPicasa
|
36
|
+
class PhotoUrl < Objectify::Xml::ElementParser
|
37
|
+
attr_accessor :url, :height, :width
|
38
|
+
end
|
39
|
+
|
40
|
+
class Album < Objectify::Xml::DocumentParser
|
41
|
+
attributes :id,
|
42
|
+
:published,
|
43
|
+
:updated,
|
44
|
+
:title,
|
45
|
+
:summary,
|
46
|
+
:rights,
|
47
|
+
:gphoto_id,
|
48
|
+
:name,
|
49
|
+
:access,
|
50
|
+
:numphotos,
|
51
|
+
:total_results,
|
52
|
+
:start_index,
|
53
|
+
:items_per_page,
|
54
|
+
:allow_downloads
|
55
|
+
has_many :links, Objectify::Atom::Link, 'link'
|
56
|
+
has_many :entries, :Photo, 'entry'
|
57
|
+
has_one :content, PhotoUrl, 'media:content'
|
58
|
+
has_many :thumbnails, PhotoUrl, 'media:thumbnail'
|
59
|
+
flatten 'media:group'
|
60
|
+
namespaces %w[openSearch gphoto media]
|
61
|
+
end
|
62
|
+
|
63
|
+
class Photo < Objectify::Xml::DocumentParser
|
64
|
+
attributes :id,
|
65
|
+
:published,
|
66
|
+
:updated,
|
67
|
+
:title,
|
68
|
+
:summary,
|
69
|
+
:gphoto_id,
|
70
|
+
:version,
|
71
|
+
:position,
|
72
|
+
:albumid,
|
73
|
+
:width,
|
74
|
+
:height,
|
75
|
+
:description,
|
76
|
+
:keywords
|
77
|
+
has_many :links, Objectify::Atom::Link, 'link'
|
78
|
+
has_one :content, PhotoUrl, 'media:content'
|
79
|
+
has_many :thumbnails, PhotoUrl, 'media:thumbnail'
|
80
|
+
namespaces %w[gphoto media]
|
81
|
+
flatten 'media:group'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
== REQUIREMENTS:
|
87
|
+
|
88
|
+
* nokogiri
|
89
|
+
* activeresource
|
90
|
+
|
91
|
+
== INSTALL:
|
92
|
+
|
93
|
+
* Installable either as a gem or vendored into a project.
|
94
|
+
* gem install objectify-xml
|
95
|
+
* gem install pangloss-objectify-xml --source http://gems.github.com
|
96
|
+
|
97
|
+
== LICENSE:
|
98
|
+
|
99
|
+
(The MIT License)
|
100
|
+
|
101
|
+
Copyright (c) 2009 Darrick Wiebe
|
102
|
+
|
103
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
104
|
+
a copy of this software and associated documentation files (the
|
105
|
+
'Software'), to deal in the Software without restriction, including
|
106
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
107
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
108
|
+
permit persons to whom the Software is furnished to do so, subject to
|
109
|
+
the following conditions:
|
110
|
+
|
111
|
+
The above copyright notice and this permission notice shall be
|
112
|
+
included in all copies or substantial portions of the Software.
|
113
|
+
|
114
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
115
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
116
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
117
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
118
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
119
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
120
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/objectify_xml.rb'
|
6
|
+
|
7
|
+
Hoe.new('objectify-xml', Objectify::Xml::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'objectify-xml' # if different than lowercase project name
|
9
|
+
p.developer('pangloss', 'darrick@innatesoftware.com')
|
10
|
+
p.extra_deps = %w[nokogiri activesupport]
|
11
|
+
#p.extra_dev_deps = []
|
12
|
+
p.url = 'http://github.com/pangloss/objectify_xml'
|
13
|
+
end
|
14
|
+
|
15
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,104 @@
|
|
1
|
+
gem 'activesupport'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/inflections'
|
4
|
+
require 'nokogiri'
|
5
|
+
require File.join(File.dirname(__FILE__), 'objectify_xml/dsl')
|
6
|
+
require File.join(File.dirname(__FILE__), 'objectify_xml/document_parser')
|
7
|
+
require File.join(File.dirname(__FILE__), 'objectify_xml/element_parser')
|
8
|
+
|
9
|
+
module Objectify
|
10
|
+
class Xml
|
11
|
+
VERSION = '0.2.0'
|
12
|
+
|
13
|
+
# When child nodes are created, they are given the name of the node
|
14
|
+
# that created them which is available here.
|
15
|
+
attr_reader :parent
|
16
|
+
|
17
|
+
# A hash containing the values of the xml document's nodes. The data is
|
18
|
+
# usually better accessed through the getter and setter methods that are
|
19
|
+
# created for all attributes, has_one and has_many associations.
|
20
|
+
attr_reader :attributes
|
21
|
+
|
22
|
+
def self.inherited(target)
|
23
|
+
# The Dsl module is added to every class that inherits from this
|
24
|
+
target.extend Dsl
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.first_element(xml)
|
28
|
+
return if xml.nil?
|
29
|
+
if xml.is_a?(String) or xml.is_a?(File)
|
30
|
+
xml = Nokogiri::XML.parse(xml)
|
31
|
+
end
|
32
|
+
# skip the <?xml?> tag
|
33
|
+
xml = xml.child if xml.class == Nokogiri::XML::Document
|
34
|
+
while xml.class == Nokogiri::XML::Node
|
35
|
+
# skips past things like xml-stylesheet declarations.
|
36
|
+
xml = xml.next
|
37
|
+
end
|
38
|
+
xml
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(xml, parent = nil)
|
42
|
+
@parent = parent
|
43
|
+
@attributes = {}
|
44
|
+
xml = self.class.first_element(xml)
|
45
|
+
primary_xml_element(xml) if xml
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
begin
|
50
|
+
attrs = (attributes || {}).map do |k,v|
|
51
|
+
if v.is_a? Objectify::Xml
|
52
|
+
"#{ k }:#{ v.class.name }"
|
53
|
+
elsif v.is_a? Array
|
54
|
+
"#{ k }:#{ v.length }"
|
55
|
+
else
|
56
|
+
k.to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
"<#{ self.class.name } #{ attrs.join(', ') }>"
|
60
|
+
rescue => e
|
61
|
+
"<#{ self.class.name } Error inspecting class: #{ e.name } #{ e.message }>"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def pretty_print(q)
|
66
|
+
begin
|
67
|
+
q.object_group(self) do
|
68
|
+
q.breakable
|
69
|
+
q.seplist(attributes, nil, :each_pair) do |k, v|
|
70
|
+
q.text "#{ k.to_s }: "
|
71
|
+
if v.is_a? String and v.length > 200
|
72
|
+
q.text "#{ v[0..80] }...".inspect
|
73
|
+
else
|
74
|
+
q.pp v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
rescue => e
|
79
|
+
q.text "<#{ self.class.name } Error inspecting class: #{ e.name } #{ e.message }>"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def xml_text_to_value(value)
|
86
|
+
value = value.strip
|
87
|
+
case value
|
88
|
+
when 'true'
|
89
|
+
true
|
90
|
+
when 'false'
|
91
|
+
false
|
92
|
+
when /\A\d{4}-\d\d-\d\d(T(\d\d[:]){2}\d\d.*)?/
|
93
|
+
DateTime.parse(value) rescue value
|
94
|
+
when /\A\d+\Z/
|
95
|
+
value.to_i
|
96
|
+
when /\A\d+\.\d+\Z/
|
97
|
+
value.to_f
|
98
|
+
else
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Objectify
|
2
|
+
module Atom
|
3
|
+
class Link < ElementParser
|
4
|
+
attributes :rel, :type, :href
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
class Category < ElementParser
|
9
|
+
attributes :scheme, :term
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class Content < ElementParser
|
14
|
+
attributes :type, :xml_lang, :xml_base, :src, :inner_html
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
class Generator < ElementParser
|
19
|
+
attributes :version, :uri, :inner_text
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
class Feed < DocumentParser
|
24
|
+
attributes :id,
|
25
|
+
:published,
|
26
|
+
:updated,
|
27
|
+
:title,
|
28
|
+
:subtitle,
|
29
|
+
:rights,
|
30
|
+
:icon
|
31
|
+
has_many :links, :Link, 'link'
|
32
|
+
has_many :entries, :Entry, 'entry'
|
33
|
+
has_one :generator, Generator, 'generator'
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class Entry < DocumentParser
|
38
|
+
attributes :id,
|
39
|
+
:published,
|
40
|
+
:updated,
|
41
|
+
:title,
|
42
|
+
:summary
|
43
|
+
has_many :links, Link, 'link'
|
44
|
+
has_one :category, Category, 'category'
|
45
|
+
has_many :contents, Content, 'content'
|
46
|
+
has_one :author, :Author, 'author'
|
47
|
+
has_many :contributors, :Contributor, 'contributor'
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
class Author < DocumentParser
|
52
|
+
attributes :name, :uri, :email
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
class Contributor < DocumentParser
|
57
|
+
attributes :name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Objectify
|
2
|
+
class DocumentParser < Xml
|
3
|
+
# The entry point to the parser, normally called by initialize after the
|
4
|
+
# initialization is complete.
|
5
|
+
def primary_xml_element(xml)
|
6
|
+
parse_xml(xml.child)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def qualified_name(x)
|
12
|
+
qn = x.name
|
13
|
+
qn = "#{ x.namespace }:#{ x.name }" if x.namespace
|
14
|
+
qn
|
15
|
+
end
|
16
|
+
|
17
|
+
def attribute_type(x)
|
18
|
+
self.class.attribute_type qualified_name(x)
|
19
|
+
end
|
20
|
+
|
21
|
+
def flatten?(x)
|
22
|
+
self.class.flatten?(qualified_name(x))
|
23
|
+
end
|
24
|
+
|
25
|
+
def collection?(x)
|
26
|
+
self.class.collection?(qualified_name(x))
|
27
|
+
end
|
28
|
+
|
29
|
+
def namespace?(x)
|
30
|
+
if x.namespace
|
31
|
+
self.class.namespace?(x.namespace)
|
32
|
+
else
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def attribute(x)
|
38
|
+
self.class.find_attribute(qualified_name(x), x.namespace, x.name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_xml(xml)
|
42
|
+
while xml
|
43
|
+
read_xml_element(xml)
|
44
|
+
xml = xml.next
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_xml_element(x)
|
49
|
+
return if x.is_a? Nokogiri::XML::Text
|
50
|
+
return unless namespace?(x)
|
51
|
+
if flatten?(x)
|
52
|
+
parse_xml(x.child)
|
53
|
+
elsif type = attribute_type(x)
|
54
|
+
set_attribute(x) { type.new(x, self) }
|
55
|
+
else
|
56
|
+
set_attribute(x) { xml_text_to_value(x.text) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_attribute(x)
|
61
|
+
if attr_name = attribute(x)
|
62
|
+
if collection?(x)
|
63
|
+
send(attr_name) << yield
|
64
|
+
else
|
65
|
+
send("#{attr_name}=", yield)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Objectify
|
2
|
+
class Xml
|
3
|
+
module Dsl
|
4
|
+
def self.extended(target)
|
5
|
+
target.init
|
6
|
+
end
|
7
|
+
|
8
|
+
def init
|
9
|
+
parent = ancestors[1]
|
10
|
+
unless /^Objectify::(Xml|ElementParser|DocumentParser)$/ =~ parent.name
|
11
|
+
@collections = parent.instance_variable_get('@collections').clone || []
|
12
|
+
@attributes = parent.instance_variable_get('@attributes').clone || []
|
13
|
+
@qualified_attributes = parent.instance_variable_get('@qualified_attributes').clone || {}
|
14
|
+
@flatten = parent.instance_variable_get('@flatten').clone || []
|
15
|
+
@namespaces = parent.instance_variable_get('@namespaces').clone || {}
|
16
|
+
@types = parent.instance_variable_get('@types').clone || {}
|
17
|
+
else
|
18
|
+
@collections = []
|
19
|
+
@attributes = []
|
20
|
+
@qualified_attributes = {}
|
21
|
+
@flatten = []
|
22
|
+
@namespaces = {}
|
23
|
+
@types = {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_one(name, type, qualified_name)
|
28
|
+
set_type(qualified_name, type)
|
29
|
+
attribute name, qualified_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_many(name, type, qualified_name)
|
33
|
+
set_type(qualified_name, type)
|
34
|
+
attribute name, qualified_name, true
|
35
|
+
end
|
36
|
+
|
37
|
+
def attributes(*names)
|
38
|
+
names.each { |n| attribute n }
|
39
|
+
@attributes + @qualified_attributes.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute(name, qualified_name = nil, collection = false)
|
43
|
+
name = name.to_s.underscore
|
44
|
+
@qualified_attributes[qualified_name] = name if qualified_name
|
45
|
+
@collections << (qualified_name || name).to_s if collection
|
46
|
+
@attributes << name unless qualified_name
|
47
|
+
module_eval %{
|
48
|
+
def #{name}=(value)
|
49
|
+
@attributes['#{name}'] = value
|
50
|
+
end
|
51
|
+
def #{name}
|
52
|
+
@attributes['#{name}']#{ collection ? ' ||= []' : '' }
|
53
|
+
end
|
54
|
+
}
|
55
|
+
name
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_attribute(qualified_name, namespace, name)
|
59
|
+
if qname = @qualified_attributes[qualified_name]
|
60
|
+
return qname
|
61
|
+
end
|
62
|
+
names = []
|
63
|
+
plural = collection?(qualified_name)
|
64
|
+
if plural
|
65
|
+
if namespace
|
66
|
+
names << "#{ namespace }_#{ name.pluralize }"
|
67
|
+
end
|
68
|
+
names << name.pluralize
|
69
|
+
end
|
70
|
+
if namespace
|
71
|
+
names << "#{ namespace }_#{ name }"
|
72
|
+
end
|
73
|
+
names << name
|
74
|
+
names.map { |n| n.underscore }.find do |n|
|
75
|
+
@attributes.include? n.underscore
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def flatten(qualified_name)
|
80
|
+
@flatten << qualified_name.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def flatten?(qualified_name)
|
84
|
+
@flatten.include? qualified_name
|
85
|
+
end
|
86
|
+
|
87
|
+
def namespace?(namespace)
|
88
|
+
@namespaces.keys.include? namespace
|
89
|
+
end
|
90
|
+
|
91
|
+
def namespaces(*namespaces)
|
92
|
+
namespaces.each do |ns|
|
93
|
+
namespace ns
|
94
|
+
end
|
95
|
+
@namespaces
|
96
|
+
end
|
97
|
+
|
98
|
+
def default_namespace(url)
|
99
|
+
@namespaces[''] = url
|
100
|
+
end
|
101
|
+
|
102
|
+
def namespace(name = nil, url = nil)
|
103
|
+
@namespaces[name.to_s] = url
|
104
|
+
end
|
105
|
+
|
106
|
+
def find_namespace(name = '')
|
107
|
+
@namespaces[name]
|
108
|
+
end
|
109
|
+
|
110
|
+
def attribute_type(qualified_name)
|
111
|
+
type = @types[qualified_name]
|
112
|
+
if type and not type.is_a? Class
|
113
|
+
type_name = type.to_s
|
114
|
+
begin
|
115
|
+
type = type_name.constantize
|
116
|
+
rescue
|
117
|
+
# Try to search the current object's namespace explicitly
|
118
|
+
sections = self.name.split(/::/)
|
119
|
+
while sections.length > 1
|
120
|
+
sections.pop
|
121
|
+
begin
|
122
|
+
sections.push(type_name)
|
123
|
+
type = sections.join('::').constantize
|
124
|
+
break
|
125
|
+
rescue
|
126
|
+
sections.pop
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
if type.nil?
|
131
|
+
raise "Unable to instantiate the constant '#{ type_name }'."
|
132
|
+
end
|
133
|
+
@types[qualified_name] = type
|
134
|
+
end
|
135
|
+
type
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_type(qualified_name, type)
|
139
|
+
@types[qualified_name] = type
|
140
|
+
end
|
141
|
+
|
142
|
+
def collection?(qualified_name)
|
143
|
+
@collections.include?(qualified_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
def metadata
|
147
|
+
{ :attributes => @attributes,
|
148
|
+
:qualified_attributes => @qualified_attributes,
|
149
|
+
:collections => @collections,
|
150
|
+
:flatten => @flatten,
|
151
|
+
:namespaces => @namespaces,
|
152
|
+
:types => @types }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|