tokamak 1.0.0.beta2
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/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +77 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +69 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/tokamak.rb +21 -0
- data/lib/tokamak/atom.rb +8 -0
- data/lib/tokamak/atom/base.rb +87 -0
- data/lib/tokamak/atom/builder.rb +107 -0
- data/lib/tokamak/atom/helpers.rb +13 -0
- data/lib/tokamak/error.rb +6 -0
- data/lib/tokamak/json.rb +10 -0
- data/lib/tokamak/json/base.rb +83 -0
- data/lib/tokamak/json/builder.rb +98 -0
- data/lib/tokamak/json/helpers.rb +13 -0
- data/lib/tokamak/representation.rb +3 -0
- data/lib/tokamak/representation/atom.rb +18 -0
- data/lib/tokamak/representation/atom/atom.rng +597 -0
- data/lib/tokamak/representation/atom/base.rb +140 -0
- data/lib/tokamak/representation/atom/category.rb +39 -0
- data/lib/tokamak/representation/atom/entry.rb +56 -0
- data/lib/tokamak/representation/atom/factory.rb +48 -0
- data/lib/tokamak/representation/atom/feed.rb +108 -0
- data/lib/tokamak/representation/atom/link.rb +66 -0
- data/lib/tokamak/representation/atom/person.rb +46 -0
- data/lib/tokamak/representation/atom/source.rb +57 -0
- data/lib/tokamak/representation/atom/tag_collection.rb +36 -0
- data/lib/tokamak/representation/atom/xml.rb +94 -0
- data/lib/tokamak/representation/generic.rb +20 -0
- data/lib/tokamak/representation/json.rb +11 -0
- data/lib/tokamak/representation/json/base.rb +25 -0
- data/lib/tokamak/representation/json/keys_as_methods.rb +72 -0
- data/lib/tokamak/representation/json/link.rb +27 -0
- data/lib/tokamak/representation/json/link_collection.rb +21 -0
- data/lib/tokamak/representation/links.rb +9 -0
- data/lib/tokamak/values.rb +29 -0
- data/lib/tokamak/xml.rb +12 -0
- data/lib/tokamak/xml/base.rb +60 -0
- data/lib/tokamak/xml/builder.rb +115 -0
- data/lib/tokamak/xml/helpers.rb +13 -0
- data/lib/tokamak/xml/link.rb +31 -0
- data/lib/tokamak/xml/links.rb +35 -0
- data/spec/integration/atom/atom_spec.rb +191 -0
- data/spec/integration/full_atom.xml +92 -0
- data/spec/integration/full_json.js +46 -0
- data/spec/integration/json/json_spec.rb +172 -0
- data/spec/integration/xml/xml_spec.rb +203 -0
- data/spec/spec_helper.rb +12 -0
- metadata +248 -0
data/lib/tokamak/xml.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Tokamak
|
4
|
+
module Xml
|
5
|
+
autoload :Base, 'tokamak/xml/base'
|
6
|
+
autoload :Builder, 'tokamak/xml/builder'
|
7
|
+
autoload :Helpers, 'tokamak/xml/helpers'
|
8
|
+
autoload :Links, 'tokamak/xml/links'
|
9
|
+
autoload :Link, 'tokamak/xml/link'
|
10
|
+
extend Base::ClassMethods
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'active_support/core_ext/hash/conversions'
|
2
|
+
|
3
|
+
module Tokamak
|
4
|
+
module Xml
|
5
|
+
module Base
|
6
|
+
module ClassMethods
|
7
|
+
mattr_reader :media_type_name
|
8
|
+
@@media_type_name = 'application/xml'
|
9
|
+
|
10
|
+
mattr_reader :headers
|
11
|
+
@@headers = {
|
12
|
+
:post => { 'Content-Type' => media_type_name }
|
13
|
+
}
|
14
|
+
|
15
|
+
def marshal(entity, options = {})
|
16
|
+
to_xml(entity, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unmarshal(string)
|
20
|
+
Hash.from_xml string
|
21
|
+
end
|
22
|
+
|
23
|
+
mattr_reader :recipes
|
24
|
+
@@recipes = {}
|
25
|
+
|
26
|
+
def describe_recipe(recipe_name, options={}, &block)
|
27
|
+
raise 'Undefined recipe' unless block_given?
|
28
|
+
raise 'Undefined recipe_name' unless recipe_name
|
29
|
+
@@recipes[recipe_name] = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_xml(obj, options = {}, &block)
|
33
|
+
return obj if obj.kind_of?(String)
|
34
|
+
|
35
|
+
if block_given?
|
36
|
+
recipe = block
|
37
|
+
elsif options[:recipe]
|
38
|
+
recipe = @@recipes[options[:recipe]]
|
39
|
+
elsif obj.kind_of?(Hash) && obj.size==1
|
40
|
+
root = obj.values.first
|
41
|
+
return root.to_xml(:root => obj.keys.first)
|
42
|
+
else
|
43
|
+
return obj.to_xml
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create representation and proxy
|
47
|
+
builder = Builder.new(obj, options)
|
48
|
+
|
49
|
+
# Check recipe arity size before calling it
|
50
|
+
recipe.call(*[builder, obj, options][0,recipe.arity])
|
51
|
+
builder.doc.to_xml
|
52
|
+
end
|
53
|
+
|
54
|
+
def helper
|
55
|
+
Helpers
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Tokamak
|
2
|
+
module Xml
|
3
|
+
# Implements the interface for marshal Xml media type requests (application/xml)
|
4
|
+
class Builder
|
5
|
+
attr_reader :doc
|
6
|
+
def initialize(obj, options = {})
|
7
|
+
@doc = Nokogiri::XML::Document.new
|
8
|
+
@obj = obj
|
9
|
+
root = options[:root] || Tokamak.root_element_for(obj)
|
10
|
+
@parent = @doc.create_element(root)
|
11
|
+
@parent.parent = @doc
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(sym, *args, &block)
|
15
|
+
values do |v|
|
16
|
+
v.send sym, *args, &block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def values(options = {}, &block)
|
21
|
+
options.each do |key,value|
|
22
|
+
attr = key.to_s
|
23
|
+
if attr =~ /^xmlns(:\w+)?$/
|
24
|
+
ns = attr.split(":", 2)[1]
|
25
|
+
@parent.add_namespace_definition(ns, value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
yield Values.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def insert_value(name, prefix, *args, &block)
|
32
|
+
node = create_element(name.to_s, prefix, *args)
|
33
|
+
node.parent = @parent
|
34
|
+
|
35
|
+
if block_given?
|
36
|
+
@parent = node
|
37
|
+
block.call
|
38
|
+
@parent = node.parent
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def link(relationship, uri, options = {})
|
43
|
+
options["rel"] = relationship.to_s
|
44
|
+
options["href"] = uri
|
45
|
+
options["type"] ||= "application/xml"
|
46
|
+
insert_value("link", nil, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def members(a_collection = nil, options = {}, &block)
|
50
|
+
collection = a_collection || @obj
|
51
|
+
raise Error::BuilderError("Members method require a collection to execute") unless collection.respond_to?(:each)
|
52
|
+
collection.each do |member|
|
53
|
+
root = options[:root] || Tokamak.root_element_for(member)
|
54
|
+
entry = @doc.create_element(root)
|
55
|
+
entry.parent = @parent
|
56
|
+
@parent = entry
|
57
|
+
block.call(self, member)
|
58
|
+
@parent = entry.parent
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create_element(node, prefix, *args)
|
65
|
+
node = @doc.create_element(node) do |n|
|
66
|
+
if prefix
|
67
|
+
if namespace = prefix_valid?(prefix)
|
68
|
+
# Adding namespace prefix
|
69
|
+
n.namespace = namespace
|
70
|
+
namespace = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
args.each do |arg|
|
75
|
+
case arg
|
76
|
+
# Adding XML attributes
|
77
|
+
when Hash
|
78
|
+
arg.each { |k,v|
|
79
|
+
key = k.to_s
|
80
|
+
if key =~ /^xmlns(:\w+)?$/
|
81
|
+
ns_name = key.split(":", 2)[1]
|
82
|
+
n.add_namespace_definition(ns_name, v)
|
83
|
+
next
|
84
|
+
end
|
85
|
+
n[k.to_s] = v.to_s
|
86
|
+
}
|
87
|
+
# Adding XML node content
|
88
|
+
else
|
89
|
+
content = if arg.kind_of?(Time) || arg.kind_of?(DateTime)
|
90
|
+
arg.xmlschema
|
91
|
+
else
|
92
|
+
arg
|
93
|
+
end
|
94
|
+
n.content = content
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def prefix_valid?(prefix)
|
101
|
+
ns = @parent.namespace_definitions.find { |x| x.prefix == prefix.to_s }
|
102
|
+
|
103
|
+
unless ns
|
104
|
+
@parent.ancestors.each do |a|
|
105
|
+
next if a == @doc
|
106
|
+
ns = a.namespace_definitions.find { |x| x.prefix == prefix.to_s }
|
107
|
+
break if ns
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
return ns
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tokamak
|
2
|
+
module Xml
|
3
|
+
class Link
|
4
|
+
def initialize(options = {})
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
def href
|
8
|
+
@options["href"]
|
9
|
+
end
|
10
|
+
def rel
|
11
|
+
@options["rel"]
|
12
|
+
end
|
13
|
+
def content_type
|
14
|
+
@options["type"]
|
15
|
+
end
|
16
|
+
def type
|
17
|
+
content_type
|
18
|
+
end
|
19
|
+
def follow
|
20
|
+
r = Restfulie.at(href)
|
21
|
+
r = r.as(content_type) if content_type
|
22
|
+
r
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"<link to #{@options}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Tokamak
|
2
|
+
module Xml
|
3
|
+
|
4
|
+
# an object to represent a list of links that can be invoked
|
5
|
+
class Links
|
6
|
+
|
7
|
+
def initialize(links)
|
8
|
+
@hash = {}
|
9
|
+
links = [links] unless links.kind_of? Array
|
10
|
+
links = [] unless links
|
11
|
+
links.each { |l|
|
12
|
+
link = Tokamak::Xml::Link.new(l)
|
13
|
+
@hash[link.rel.to_s] = link
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](name)
|
18
|
+
@hash[name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
@hash.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def keys
|
26
|
+
@hash.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(sym, *args)
|
30
|
+
raise "Links can not receive arguments" unless args.empty?
|
31
|
+
self[sym.to_s]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
module Tokamak::Test
|
4
|
+
class SimpleClass
|
5
|
+
attr_accessor :id, :title, :updated
|
6
|
+
def initialize(id,title,updated)
|
7
|
+
@id, @title, @updated = id, title, updated
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Tokamak do
|
13
|
+
describe 'Atom' do
|
14
|
+
|
15
|
+
describe "Feed" do
|
16
|
+
it "should create a feed from builder DSL" do
|
17
|
+
time = Time.now
|
18
|
+
some_articles = [
|
19
|
+
{:id => 1, :title => "a great article", :updated => time},
|
20
|
+
{:id => 2, :title => "another great article", :updated => time}
|
21
|
+
]
|
22
|
+
|
23
|
+
feed = to_atom(some_articles) do |collection|
|
24
|
+
collection.values do |values|
|
25
|
+
values.id "http://example.com/feed"
|
26
|
+
values.title "Feed"
|
27
|
+
values.updated time
|
28
|
+
|
29
|
+
values.author {
|
30
|
+
values.name "John Doe"
|
31
|
+
values.email "joedoe@example.com"
|
32
|
+
}
|
33
|
+
|
34
|
+
values.author {
|
35
|
+
values.name "Foo Bar"
|
36
|
+
values.email "foobar@example.com"
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
collection.link("next", "http://a.link.com/next")
|
41
|
+
collection.link("previous", "http://a.link.com/previous")
|
42
|
+
|
43
|
+
collection.members do |member, article|
|
44
|
+
member.values do |values|
|
45
|
+
values.id "uri:#{article[:id]}"
|
46
|
+
values.title article[:title]
|
47
|
+
values.updated article[:updated]
|
48
|
+
end
|
49
|
+
|
50
|
+
member.link("image", "http://example.com/image/1")
|
51
|
+
member.link("image", "http://example.com/image/2", :type => "application/atom+xml")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
feed.atom_type.should == "feed"
|
56
|
+
feed.id.should == "http://example.com/feed"
|
57
|
+
feed.title.should == "Feed"
|
58
|
+
feed.updated.should == DateTime.parse(time.xmlschema)
|
59
|
+
feed.authors.first.name.should == "John Doe"
|
60
|
+
feed.authors.last.email.should == "foobar@example.com"
|
61
|
+
|
62
|
+
feed.entries.first.id.should == "uri:1"
|
63
|
+
feed.entries.first.title.should == "a great article"
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should create a feed from a string input" do
|
67
|
+
full_atom = IO.read(File.dirname(__FILE__) + '/../full_atom.xml')
|
68
|
+
feed = to_atom(full_atom)
|
69
|
+
|
70
|
+
feed.id.should == "http://example.com/albums/1"
|
71
|
+
feed.title.should == "Albums feed"
|
72
|
+
feed.updated.should be_kind_of(Time)
|
73
|
+
feed.updated.should == Time.parse("2010-05-03T16:29:26-03:00")
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "Entry" do
|
79
|
+
it "should create an entry from builder DSL" do
|
80
|
+
time = Time.now
|
81
|
+
an_article = {:id => 1, :title => "a great article", :updated => time}
|
82
|
+
|
83
|
+
entry = to_atom(an_article, :atom_type => :entry) do |member, article|
|
84
|
+
member.values do |values|
|
85
|
+
values.id "uri:#{article[:id]}"
|
86
|
+
values.title article[:title]
|
87
|
+
values.updated article[:updated]
|
88
|
+
end
|
89
|
+
|
90
|
+
member.link("image", "http://example.com/image/1")
|
91
|
+
member.link("image", "http://example.com/image/2", :type => "application/atom+xml")
|
92
|
+
end
|
93
|
+
|
94
|
+
entry.atom_type.should == "entry"
|
95
|
+
entry.id.should == "uri:1"
|
96
|
+
entry.title.should == "a great article"
|
97
|
+
entry.updated.should == DateTime.parse(time.xmlschema)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should be able to declare links inside values block" do
|
101
|
+
time = Time.now
|
102
|
+
an_article = {:id => 1, :title => "a great article", :updated => time}
|
103
|
+
|
104
|
+
entry = to_atom(an_article, :atom_type => :entry) do |member, article|
|
105
|
+
member.values do |values|
|
106
|
+
values.id "uri:#{article[:id]}"
|
107
|
+
values.title article[:title]
|
108
|
+
values.updated article[:updated]
|
109
|
+
|
110
|
+
values.domain("xmlns" => "http://a.namespace.com") {
|
111
|
+
member.link("image", "http://example.com/image/1")
|
112
|
+
member.link("image", "http://example.com/image/2", :type => "application/atom+xml")
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
entry.atom_type.should == "entry"
|
118
|
+
entry.id.should == "uri:1"
|
119
|
+
entry.title.should == "a great article"
|
120
|
+
entry.updated.should == DateTime.parse(time.xmlschema)
|
121
|
+
|
122
|
+
entry.doc.xpath("xmlns:domain", "xmlns" => "http://a.namespace.com").children.first.node_name.should == "link"
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should create an entry from an already declared recipe" do
|
126
|
+
|
127
|
+
describe_recipe(:simple_entry) do |member, article|
|
128
|
+
member.values do |values|
|
129
|
+
values.id "uri:#{article[:id]}"
|
130
|
+
values.title article[:title]
|
131
|
+
values.updated article[:updated]
|
132
|
+
end
|
133
|
+
|
134
|
+
member.link("image", "http://example.com/image/1")
|
135
|
+
member.link("image", "http://example.com/image/2", :type => "application/atom+xml")
|
136
|
+
end
|
137
|
+
|
138
|
+
time = Time.now
|
139
|
+
an_article = {:id => 1, :title => "a great article", :updated => time}
|
140
|
+
|
141
|
+
entry = to_atom(an_article, :atom_type => :entry, :recipe => :simple_entry)
|
142
|
+
|
143
|
+
entry.atom_type.should == "entry"
|
144
|
+
entry.id.should == "uri:1"
|
145
|
+
entry.title.should == "a great article"
|
146
|
+
entry.updated.should == DateTime.parse(time.xmlschema)
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "Errors" do
|
152
|
+
it "should raise error for converter without recipe" do
|
153
|
+
lambda {
|
154
|
+
to_atom
|
155
|
+
}.should raise_error(Tokamak::ConverterError, "Recipe required")
|
156
|
+
end
|
157
|
+
|
158
|
+
it "raise error to invalid atom type" do
|
159
|
+
lambda {
|
160
|
+
obj = Object.new
|
161
|
+
describe_recipe(:simple_entry) do |member, article|
|
162
|
+
member.values do |values|
|
163
|
+
values.id "uri:#{article[:id]}"
|
164
|
+
values.title article[:title]
|
165
|
+
values.updated article[:updated]
|
166
|
+
end
|
167
|
+
|
168
|
+
member.link("image", "http://example.com/image/1")
|
169
|
+
member.link("image", "http://example.com/image/2", :type => "application/atom+xml")
|
170
|
+
end
|
171
|
+
|
172
|
+
Tokamak::Atom.to_atom(obj, :recipe => :simple_entry, :atom_type => :foo)
|
173
|
+
}.should raise_error(Tokamak::ConverterError, "Undefined atom type foo")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
def to_atom(*args, &recipe)
|
180
|
+
Tokamak::Atom.to_atom(*args, &recipe)
|
181
|
+
end
|
182
|
+
|
183
|
+
def describe_recipe(*args, &recipe)
|
184
|
+
Tokamak::Atom.describe_recipe(*args, &recipe)
|
185
|
+
end
|
186
|
+
|
187
|
+
def simple_object(*args)
|
188
|
+
Tokamak::Test::SimpleClass.new(*args)
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|