objectify-xml 0.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.
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