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 ADDED
@@ -0,0 +1,9 @@
1
+ === 0.2.0 / 2009-02-24
2
+
3
+ * Initial public release.
4
+ * Full suite of specs.
5
+
6
+ === 0.1.0 / 2009-02-22
7
+
8
+ * Initial project creation.
9
+
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