rubyrest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6 -0
- data/COPYING +18 -0
- data/README +44 -0
- data/Rakefile +106 -0
- data/bin/rubyrest +67 -0
- data/examples/hello.rb +57 -0
- data/lib/rubyrest/atom.rb +204 -0
- data/lib/rubyrest/config.rb +78 -0
- data/lib/rubyrest/engine.rb +77 -0
- data/lib/rubyrest/servlets.rb +235 -0
- data/lib/rubyrest/tools.rb +72 -0
- data/lib/rubyrest.rb +24 -0
- metadata +82 -0
data/CHANGELOG
ADDED
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2007 Pedro Gutierrez
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
== Ruby-on-Rest: Simple REST Framework for Ruby
|
2
|
+
|
3
|
+
Ruby-on-Rest (rubyrest) provides a simple framework to help you expose
|
4
|
+
your business objects as web resources.
|
5
|
+
|
6
|
+
Ruby-on-Rest provides an programming model and a security framework that
|
7
|
+
lets you create new REST services without too much effort.
|
8
|
+
|
9
|
+
== Resources
|
10
|
+
|
11
|
+
* {Project Documentation}[http://rubyrest.rubyforge.org]
|
12
|
+
* {Project Page at Rubyforge}[http://rubyforge.org/projects/rubyrest]
|
13
|
+
* {Project Page at Google Code}[http://code.google.com/p/rubyrest]
|
14
|
+
|
15
|
+
To check out the source code:
|
16
|
+
|
17
|
+
svn checkout http://rubyrest.rubyforge.org/svn/trunk
|
18
|
+
|
19
|
+
=== Contact
|
20
|
+
|
21
|
+
If you have any comments or suggestions please send an email to pedro dot gutierrez at netcourrier dot com and I'll get back to you.
|
22
|
+
|
23
|
+
== Installation
|
24
|
+
|
25
|
+
sudo gem install rubyrest
|
26
|
+
|
27
|
+
== Getting Started
|
28
|
+
|
29
|
+
=== Learning by example
|
30
|
+
|
31
|
+
Please have a look at the examples provided, they are simple enough to let you grasp how rubyrest works.
|
32
|
+
|
33
|
+
=== Starting and stopping the service
|
34
|
+
|
35
|
+
Ruby-on-Rest provides a shell command. Open your console, and type the following:
|
36
|
+
|
37
|
+
rubyrest start|stop <my_service> [<my_service_module>]
|
38
|
+
|
39
|
+
=== Configuring your service
|
40
|
+
|
41
|
+
Ruby-on-Rest will try to load a file name <my_service>.rb which must provide access to
|
42
|
+
the service configuration, service and model.
|
43
|
+
|
44
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
NAME = "rubyrest"
|
9
|
+
VERS = "0.0.1"
|
10
|
+
CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
|
11
|
+
RDOC_OPTS = ['--quiet', '--title', "Ruby-on-Rest: A simple REST framework for Ruby",
|
12
|
+
"--opname", "index.html",
|
13
|
+
"--line-numbers",
|
14
|
+
"--main", "README",
|
15
|
+
"--inline-source"]
|
16
|
+
|
17
|
+
desc "Packages up Ruby-on-Rest."
|
18
|
+
task :default => [:package]
|
19
|
+
task :package => [:clean]
|
20
|
+
|
21
|
+
task :doc => [:rdoc]
|
22
|
+
|
23
|
+
Rake::RDocTask.new do |rdoc|
|
24
|
+
rdoc.rdoc_dir = 'doc/rdoc'
|
25
|
+
rdoc.options += RDOC_OPTS
|
26
|
+
rdoc.main = "README"
|
27
|
+
rdoc.title = "Ruby-on-Rest Documentation"
|
28
|
+
rdoc.rdoc_files.add ['README', 'COPYING', 'lib/rubyrest.rb', 'lib/rubyrest/**/*.rb', 'examples/**/*.rb' ]
|
29
|
+
end
|
30
|
+
|
31
|
+
spec = Gem::Specification.new do |s|
|
32
|
+
s.name = NAME
|
33
|
+
s.version = VERS
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.has_rdoc = true
|
36
|
+
s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING", 'examples/hello.rb' ]
|
37
|
+
s.rdoc_options += RDOC_OPTS + [ '--exclude', 'lib/rubyrest.rb', '--include', 'examples/*.rb' ]
|
38
|
+
s.summary = "REST framework for Ruby."
|
39
|
+
s.description = s.summary
|
40
|
+
s.author = "Pedro Gutierrez"
|
41
|
+
s.email = 'pedro.gutierrrez@netcourrier.com'
|
42
|
+
s.homepage = 'http://rubyrest.rubyforge.org'
|
43
|
+
s.executables = ['rubyrest']
|
44
|
+
|
45
|
+
s.add_dependency('metaid')
|
46
|
+
s.required_ruby_version = '>= 1.8.2'
|
47
|
+
|
48
|
+
s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,lib}/**/*")
|
49
|
+
|
50
|
+
s.require_path = "lib"
|
51
|
+
s.bindir = "bin"
|
52
|
+
end
|
53
|
+
|
54
|
+
Rake::GemPackageTask.new(spec) do |p|
|
55
|
+
p.need_tar = true
|
56
|
+
p.gem_spec = spec
|
57
|
+
end
|
58
|
+
|
59
|
+
task :install do
|
60
|
+
sh %{rake package}
|
61
|
+
sh %{sudo gem install pkg/#{NAME}-#{VERS}}
|
62
|
+
end
|
63
|
+
|
64
|
+
task :uninstall => [:clean] do
|
65
|
+
sh %{sudo gem uninstall #{NAME}}
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'Update docs and upload to rubyforge.org'
|
69
|
+
task :doc_rforge do
|
70
|
+
sh %{rake doc}
|
71
|
+
sh %{scp -r doc/rdoc/* sicozu@rubyforge.org:/var/www/gforge-projects/rubyrest}
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
desc 'Make a release in Rubyforge'
|
76
|
+
task :release => [:clean, :package, :doc_rforge ] do
|
77
|
+
# create a svn tag
|
78
|
+
sh %{svn copy svn+ssh://rubyforge.org/var/svn/rubyrest/trunk svn+ssh://rubyforge.org/var/svn/rubyrest/tags/#{VERS}}
|
79
|
+
# upload the gem
|
80
|
+
sh %{scp pkg/#{NAME}-#{VERS}.gem sicozu@rubyforge.org:/var/www/gforge-projects/rubyrest/}
|
81
|
+
end
|
82
|
+
|
83
|
+
require 'spec/rake/spectask'
|
84
|
+
|
85
|
+
desc "Run specs with coverage"
|
86
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
87
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
88
|
+
t.rcov = true
|
89
|
+
end
|
90
|
+
|
91
|
+
##############################################################################
|
92
|
+
# Statistics
|
93
|
+
##############################################################################
|
94
|
+
|
95
|
+
STATS_DIRECTORIES = [
|
96
|
+
%w(Code lib/),
|
97
|
+
%w(Spec spec/)
|
98
|
+
].collect { |name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir) }
|
99
|
+
|
100
|
+
desc "Report code statistics (KLOCs, etc) from the application"
|
101
|
+
task :stats do
|
102
|
+
require 'extra/stats'
|
103
|
+
verbose = true
|
104
|
+
CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
105
|
+
end
|
106
|
+
|
data/bin/rubyrest
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# RubyRest $Id:$
|
4
|
+
#
|
5
|
+
# UNIX Command Line.
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rubyrest'
|
8
|
+
|
9
|
+
|
10
|
+
module RubyRest
|
11
|
+
|
12
|
+
|
13
|
+
class RubyRestBoot
|
14
|
+
include RubyRest::Tools
|
15
|
+
|
16
|
+
def initialize( option, service, prefix = nil )
|
17
|
+
|
18
|
+
if option == nil or service == nil or !self.respond_to?( option )
|
19
|
+
help_and_exit
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
@option = option
|
24
|
+
@service = service
|
25
|
+
@service_prefix = prefix
|
26
|
+
|
27
|
+
#begin
|
28
|
+
self.method( @option ).call
|
29
|
+
#rescue => e
|
30
|
+
# puts "ERROR #{e.message}"
|
31
|
+
#end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def help_and_exit
|
36
|
+
puts "Usage: rubyrest start|stop <your_service> "
|
37
|
+
puts "Ruby-on-Rest: Simple REST for Ruby."
|
38
|
+
puts
|
39
|
+
puts "Examples:"
|
40
|
+
puts " rubyrest start grape"
|
41
|
+
puts " rubyrest stop grape"
|
42
|
+
puts
|
43
|
+
puts "For more information see http://rubyrest.rubyforge.org"
|
44
|
+
end
|
45
|
+
|
46
|
+
def start
|
47
|
+
service_module = to_module_name( @service_prefix, @service )
|
48
|
+
require @service
|
49
|
+
puts "=> Starting service '#{@service}' in module '#{service_module}'"
|
50
|
+
config = to_class( service_module, "config" ).new
|
51
|
+
config[ :servicemodule ] = service_module
|
52
|
+
RubyRest::Engine.new( config ).start
|
53
|
+
puts "=> Service #{@service} running!"
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def stop
|
58
|
+
puts "=> Stopping service #{@service}..."
|
59
|
+
|
60
|
+
|
61
|
+
puts "=> Service #{@service} stopped!"
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
RubyRest::RubyRestBoot.new( ARGV[0], ARGV[1], ARGV[2] )
|
data/examples/hello.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Ruby-on-Rest HelloWorld application
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#
|
5
|
+
module Acme
|
6
|
+
|
7
|
+
module Hello
|
8
|
+
|
9
|
+
# The configuration for the Acme::Hello service.
|
10
|
+
# This is a simple service that does not need database
|
11
|
+
# connectivity.
|
12
|
+
class Config < RubyRest::SimpleConfig
|
13
|
+
|
14
|
+
# Inits the internal hash of
|
15
|
+
# configuration options. This is the minimal expression
|
16
|
+
# of a Ruby-on-Rest configuration hash.
|
17
|
+
def initialize
|
18
|
+
@hash = {
|
19
|
+
:servicemodel => [ "welcomeservice" ],
|
20
|
+
:serviceport => 9001
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# The domain service that actually implements
|
27
|
+
# 'business' logic. In this case, it's just going to say "Hello!"
|
28
|
+
# by returning a HelloBean object
|
29
|
+
class Welcomeservice
|
30
|
+
|
31
|
+
# Returns a simple object that says hello
|
32
|
+
# This is the method invoked by RubyRest::CRUDServlet
|
33
|
+
# when it receives a GET request with no particular resource id
|
34
|
+
def self.rest_retrieve( principal )
|
35
|
+
HelloBean.new
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# The object that encapsulates the so sophisticated
|
41
|
+
# message. By including RubyRest::Atom::Entry, we get some convenience
|
42
|
+
# methods that help to provide an atom entry representation
|
43
|
+
# of the bean, without too much effort.
|
44
|
+
class HelloBean
|
45
|
+
include RubyRest::Atom::DummyEntry
|
46
|
+
|
47
|
+
# The hello message, included as a 'message'
|
48
|
+
# node inside an Atom Entry content.
|
49
|
+
def atom_content( builder )
|
50
|
+
builder.message "Hello!"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# Simple ATOM Feed Generator, used by REST
|
2
|
+
# Servlets as the representation of resources returned
|
3
|
+
# to the client.
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# $Id:$
|
7
|
+
module RubyRest
|
8
|
+
|
9
|
+
module Atom
|
10
|
+
include RubyRest::Tools
|
11
|
+
|
12
|
+
NAMESPACES = {
|
13
|
+
"xmlns" => "http://www.w3.org/2005/Atom",
|
14
|
+
"xmlns:moodisland" => "http://www.moodisland.com/ns#"
|
15
|
+
}
|
16
|
+
|
17
|
+
ATOM_TYPE = "application/atom+xml".freeze
|
18
|
+
ATOMSERV_TYPE = "application/atomserv+xml".freeze
|
19
|
+
HTML_TYPE = "text/html".freeze
|
20
|
+
WORKSPACE_METHOD = "dashboard".freeze
|
21
|
+
MODULEID = "Ruby-on-Rest (http://rubyrest.rubyforge.org)".freeze
|
22
|
+
|
23
|
+
#
|
24
|
+
# Formats the response as a Atom feed, Atom Entry or
|
25
|
+
# Atom Service Document
|
26
|
+
def format_response( request, response )
|
27
|
+
|
28
|
+
builder = Builder::XmlMarkup.new( :target => response.body )
|
29
|
+
builder.instruct!
|
30
|
+
|
31
|
+
if @service_method == "dashboard"
|
32
|
+
response[ "content-type" ] = ATOMSERV_TYPE
|
33
|
+
build_service_document( @result, builder, request.request_uri )
|
34
|
+
else
|
35
|
+
response[ "content-type" ] = ATOM_TYPE
|
36
|
+
if @result.respond_to? "each"
|
37
|
+
build_feed( @result, builder, request.request_uri, request.path, @method )
|
38
|
+
else build_entry( @result, builder, request.request_uri ) end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Builds an Atom Service Document. This is a representation of the
|
44
|
+
# user's dashboard or initial workspace.
|
45
|
+
def build_service_document( workspace, builder, uri )
|
46
|
+
builder.service( NAMESPACES ){
|
47
|
+
builder.workspace {
|
48
|
+
builder.title "Dashboard"
|
49
|
+
workspace.each { |w|
|
50
|
+
builder.collection( { "href" => "#{uri}#{w}" } ) {
|
51
|
+
builder.title "#{w}_all"
|
52
|
+
builder.accept "entry"
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Builds an Atom Feed representation of the specified collection
|
60
|
+
# of entries
|
61
|
+
#
|
62
|
+
def build_feed( entries, builder, uri, path, title )
|
63
|
+
builder.feed( NAMESPACES ) {
|
64
|
+
builder.id uri
|
65
|
+
builder.link( { "rel" => "self", "href" => path, "type" => ATOM_TYPE } )
|
66
|
+
builder.link( { "rel" => "alternate", "href" => uri, "type" => HTML_TYPE } )
|
67
|
+
builder.title title
|
68
|
+
builder.updated format_atom_date( Time.now )
|
69
|
+
entries.each { |object| build_entry( object, builder, uri ) }
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Builds an Atom Entry representation of the specified
|
74
|
+
# object.
|
75
|
+
#
|
76
|
+
# The object is supposed to implement the following mandatory methods:
|
77
|
+
# atom_id, atom_title, atom_author, atom_updated, atom_summary
|
78
|
+
#
|
79
|
+
# The object can implement the following optionnal methods:
|
80
|
+
# atom_related, atom_content
|
81
|
+
#
|
82
|
+
def build_entry( object, builder, uri )
|
83
|
+
|
84
|
+
entry_link = uri
|
85
|
+
entry_link = "#{uri}/#{object.atom_id}" if @id == nil
|
86
|
+
|
87
|
+
builder.entry( NAMESPACES ) {
|
88
|
+
builder.title object.atom_title
|
89
|
+
builder.author { builder.name object.atom_author }
|
90
|
+
builder.updated format_atom_date( object.atom_updated )
|
91
|
+
builder.id entry_link
|
92
|
+
builder.summary object.atom_summary
|
93
|
+
builder.link( "rel" => "alternate", "href" => entry_link )
|
94
|
+
|
95
|
+
if object.respond_to? :atom_related
|
96
|
+
object.atom_related.each{ |related|
|
97
|
+
builder.link( "rel" => "related", "href" => "#{entry_link}/#{related}", "title" => "#{related}", "type" => ATOM_TYPE )
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
builder.moodisland :content do
|
102
|
+
object.atom_content( builder ) if object.respond_to? :atom_content
|
103
|
+
end
|
104
|
+
}
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
# This module provides some default, arbitrary implementations
|
109
|
+
# for methods required by RubyRest in order to
|
110
|
+
# provide an Atom Entry representation out of a domain object.
|
111
|
+
#
|
112
|
+
# Developpers can choose whether to use this implementation
|
113
|
+
# or to provide their own.
|
114
|
+
module Entry
|
115
|
+
|
116
|
+
# Returns the Atom Entry title
|
117
|
+
def atom_title
|
118
|
+
self.name
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the Atom Entry Summary. Synonym of atom_title
|
122
|
+
def atom_summary
|
123
|
+
atom_title
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns the generated token
|
127
|
+
def atom_id
|
128
|
+
self.id
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns Time.now
|
132
|
+
def atom_updated
|
133
|
+
self.updated
|
134
|
+
end
|
135
|
+
|
136
|
+
# Not a very relevant buy necessary information
|
137
|
+
def atom_author
|
138
|
+
self.createdby
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# This module provides a failsafe implementation for
|
145
|
+
# methods required by RubyRest in order to
|
146
|
+
# provide an Atom Entry representation out of a domain object.
|
147
|
+
#
|
148
|
+
# Developpers should rather use RubyRest::Atom::Entry or
|
149
|
+
# provide their own.
|
150
|
+
module DummyEntry
|
151
|
+
|
152
|
+
# Returns the object's class name
|
153
|
+
def atom_title
|
154
|
+
self.class.name
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the Atom Entry Summary.
|
158
|
+
# Synonym of atom_title
|
159
|
+
def atom_summary
|
160
|
+
atom_title
|
161
|
+
end
|
162
|
+
|
163
|
+
# Generates an id from the current time value
|
164
|
+
def atom_id
|
165
|
+
Time.now.to_i
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns Time.now
|
169
|
+
def atom_updated
|
170
|
+
Time.now
|
171
|
+
end
|
172
|
+
|
173
|
+
# Overrides the implementation provided
|
174
|
+
# by RubyRest::Atom::Entry
|
175
|
+
def atom_author
|
176
|
+
MODULEID
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
# Very simple module that adds some binding facilities
|
182
|
+
#
|
183
|
+
module EntryBinder
|
184
|
+
|
185
|
+
# Binds a property against the matching
|
186
|
+
# child element in the specified atom entry xml source.
|
187
|
+
# The base node to look into is /entry/content
|
188
|
+
def atom_bind( xml, property, mandatory = true )
|
189
|
+
location = "/entry/content/#{property}"
|
190
|
+
node = REXML::XPath.first( xml, location )
|
191
|
+
if node == nil
|
192
|
+
raise "no node found for location #{location}" if mandatory == true
|
193
|
+
return
|
194
|
+
end
|
195
|
+
if self.respond_to?( "#{property}=" )
|
196
|
+
self.method( "#{property}=").call( node.text )
|
197
|
+
else self[ property.intern ] = node.text end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# RubyRest: $Id:$
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#
|
5
|
+
module RubyRest
|
6
|
+
|
7
|
+
# This class is meant to be subclassed with customized
|
8
|
+
# behaviour that will be invoked by the engine at startup.
|
9
|
+
# Subclasses can provide connectivity to popular database
|
10
|
+
# frameworks such as ActiveRecord, Sequel or Og.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# module MyService
|
15
|
+
# class Config < RubyRest::SimpleConfig
|
16
|
+
# def initialize
|
17
|
+
# @hash = {
|
18
|
+
# ... my config here ..
|
19
|
+
# }
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
#
|
25
|
+
class SimpleConfig
|
26
|
+
include RubyRest::Tools
|
27
|
+
|
28
|
+
# Overrides the specified configuration option
|
29
|
+
# with its new value
|
30
|
+
def []=( name, value )
|
31
|
+
@hash[ name ] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a given configuration option value
|
35
|
+
def [](name)
|
36
|
+
raise error( 000, name ) if !has( name )
|
37
|
+
return @hash[ name ]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if the specified name matches
|
41
|
+
# a valid configuration option
|
42
|
+
def has( name )
|
43
|
+
@hash[ name ] != nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# Dumps all the configuration options
|
47
|
+
def to_s
|
48
|
+
@hash.each{ |k,v| puts "#{k} = #{v}" }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
# Specialization of SimpleConfig that provides
|
54
|
+
# hooks for database initialization during startup
|
55
|
+
class DatabaseConfig < SimpleConfig
|
56
|
+
|
57
|
+
# Connects and returns a database instance.
|
58
|
+
#
|
59
|
+
# This method
|
60
|
+
# will delegate to a more specialized method, according to the
|
61
|
+
# :adapter config option
|
62
|
+
def connect_to_database
|
63
|
+
adapter_method = "#{@hash[:dbadapter]}_connect"
|
64
|
+
error( 004, self, adapter_method ) if !self.respond_to?( adapter_method )
|
65
|
+
@db = self.method( adapter_method ).call
|
66
|
+
error( 005, self, adapter_method ) if @db == nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# This method must be implemented in subclasses
|
70
|
+
# Intentionnaly left empty
|
71
|
+
def init_database
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# RubyRest: $Id:$
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#
|
5
|
+
|
6
|
+
module RubyRest
|
7
|
+
|
8
|
+
# Objects of this class take a configuration as argument
|
9
|
+
# then launch a new server instance.
|
10
|
+
class Engine
|
11
|
+
|
12
|
+
# Enables external objects to read the
|
13
|
+
# engine configuration
|
14
|
+
attr_reader :config
|
15
|
+
|
16
|
+
# Creates a new RubyRest engine, for the specified
|
17
|
+
# configuration
|
18
|
+
def initialize( config )
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
# Starts the engine.
|
23
|
+
# The following operations are accomplished:
|
24
|
+
#
|
25
|
+
# 1. establish a connection to the database
|
26
|
+
# 2. initialize the database schema
|
27
|
+
# 3. load initial data into the database
|
28
|
+
#
|
29
|
+
def start
|
30
|
+
configure_database if @config.has( :dbadapter )
|
31
|
+
start_server
|
32
|
+
end
|
33
|
+
|
34
|
+
# Configures the database connectivity
|
35
|
+
def configure_database
|
36
|
+
@config.connect_to_database
|
37
|
+
@config.setup_persistence
|
38
|
+
if @config[ :dbdestroy ] == true
|
39
|
+
@config.init_schema
|
40
|
+
@config.load_initial_data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Configures and returns a new web server
|
45
|
+
# instance. For the moment, only WEBrick instances
|
46
|
+
# are supported
|
47
|
+
def start_server
|
48
|
+
server = RubyRest::Server.new( @config )
|
49
|
+
[ "INT", "TERM" ].each { |signal|
|
50
|
+
trap( signal ) { server.shutdown }
|
51
|
+
}
|
52
|
+
server.mount "/", CRUDServlet
|
53
|
+
server.start
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Adds some extra functionnality to the actual server
|
60
|
+
# implementation.
|
61
|
+
#
|
62
|
+
class Server < WEBrick::HTTPServer
|
63
|
+
|
64
|
+
# the configuration options
|
65
|
+
attr_accessor :rubyrest
|
66
|
+
|
67
|
+
# Creates a new WEBrick instance with the specified
|
68
|
+
# configuration
|
69
|
+
def initialize( config )
|
70
|
+
super( :Port => config[ :serviceport ] )
|
71
|
+
@rubyrest = config
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# WEBrick servlet that implements a simple mapping between
|
2
|
+
# HTTP methods and request paths into business methods, using some
|
3
|
+
# conventions.
|
4
|
+
#
|
5
|
+
# Developpers should only subclass this servlet, and implement
|
6
|
+
# standard methods that return a xml content.
|
7
|
+
#
|
8
|
+
# $Id:$
|
9
|
+
module RubyRest
|
10
|
+
|
11
|
+
# This is the servlet that actually exposes a REST API
|
12
|
+
# by translating HTTP requests into 'generic' or 'custom' service
|
13
|
+
# methods.
|
14
|
+
#
|
15
|
+
class RESTServlet < WEBrick::HTTPServlet::AbstractServlet
|
16
|
+
include RubyRest::Atom, RubyRest::Tools
|
17
|
+
|
18
|
+
# Injects some RubyRest configuration options
|
19
|
+
# from the server to the servlet
|
20
|
+
def initialize( server, *options )
|
21
|
+
super( server, options )
|
22
|
+
@servicemodule = server.rubyrest[ :servicemodule ]
|
23
|
+
|
24
|
+
if server.rubyrest.has( :authmodel )
|
25
|
+
@authmodel = server.rubyrest[ :authmodel ]
|
26
|
+
end
|
27
|
+
|
28
|
+
if server.rubyrest.has( :dashboard )
|
29
|
+
@dashboard = server.rubyrest[ :dashboard ]
|
30
|
+
else @dashboard = server.rubyrest[ :servicemodel ][0] end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Inspects the request, and resolve the parameters
|
35
|
+
# The format of a request is the following:
|
36
|
+
#
|
37
|
+
# /:model/:id/:property
|
38
|
+
#
|
39
|
+
def resolve_params( request )
|
40
|
+
params = request.path.split( "/" )
|
41
|
+
@http_method = request.request_method
|
42
|
+
@token = request[ 'token']
|
43
|
+
@model = params[1]
|
44
|
+
@id = params[2]
|
45
|
+
@property = params[3]
|
46
|
+
@body = REXML::Document.new( request.body ) if request.body != nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve_custom_method
|
50
|
+
if @property != nil
|
51
|
+
@custom_method = "#{@model}_#{@property}"
|
52
|
+
else
|
53
|
+
@custom_method = WORKSPACE_METHOD
|
54
|
+
@custom_method = "#{@model}_#{@generic_method}" if @model != nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolve_service_method
|
59
|
+
resolve_custom_method
|
60
|
+
return @custom_method if self.respond_to? @custom_method
|
61
|
+
return @generic_method
|
62
|
+
end
|
63
|
+
|
64
|
+
def dispatch( request, response )
|
65
|
+
|
66
|
+
@service_method = resolve_service_method
|
67
|
+
check_security( request ) if @authmodel != nil
|
68
|
+
@result = self.method( @service_method ).call( request )
|
69
|
+
|
70
|
+
if @result.respond_to? "unauthorized" and @result.unauthorized == true
|
71
|
+
raise WEBrick::HTTPStatus::Unauthorized
|
72
|
+
else
|
73
|
+
response.status = @success_code
|
74
|
+
format_response( request, response ) if @result != nil
|
75
|
+
raise WEBrick::HTTPStatus::OK
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def resolve_get_method
|
81
|
+
if @property != nil
|
82
|
+
@generic_method = :retrieve_related
|
83
|
+
else
|
84
|
+
if @id != nil
|
85
|
+
@generic_method = :show
|
86
|
+
else @generic_method = :retrieve end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def do_GET( request, response )
|
91
|
+
resolve_params( request )
|
92
|
+
@success_code = 200
|
93
|
+
resolve_get_method
|
94
|
+
dispatch( request, response )
|
95
|
+
end
|
96
|
+
|
97
|
+
def do_POST( request, response )
|
98
|
+
resolve_params( request )
|
99
|
+
incompatible_path( request ) if @id != nil
|
100
|
+
@generic_method = :create
|
101
|
+
@success_code = 201
|
102
|
+
dispatch( request, response )
|
103
|
+
end
|
104
|
+
|
105
|
+
def do_PUT( request, response )
|
106
|
+
resolve_params( request )
|
107
|
+
incompatible_path( request ) if @id == nil
|
108
|
+
@generic_method = :update
|
109
|
+
@success_code = 200
|
110
|
+
dispatch( request, response )
|
111
|
+
end
|
112
|
+
|
113
|
+
def do_DELETE( request, response )
|
114
|
+
resolve_params( request )
|
115
|
+
incompatible_path( request ) if @id == nil
|
116
|
+
@generic_method = :delete
|
117
|
+
@success_code = 200
|
118
|
+
dispatch( request, response )
|
119
|
+
end
|
120
|
+
|
121
|
+
# Raises an error stating that the current
|
122
|
+
# http method is not compatible with the requested path.
|
123
|
+
def incompatible_path( request )
|
124
|
+
error( 100, @http_method, request.path )
|
125
|
+
end
|
126
|
+
|
127
|
+
# Custom service method that authenticates a username/password pair
|
128
|
+
# found in the request body. The authentication is left to the class defined by the
|
129
|
+
# :authmodel configuration option.
|
130
|
+
#
|
131
|
+
# Developpers can provide their own implementation, however it is recommended to
|
132
|
+
# subclass the class *Credentials*
|
133
|
+
def credentials_create( request )
|
134
|
+
auth_class = to_class( @servicemodule, @authmodel )
|
135
|
+
auth = auth_class.new
|
136
|
+
auth_class.rest_bind( auth, @body )
|
137
|
+
auth = auth_class.authenticate( auth )
|
138
|
+
raise WEBrick::HTTPStatus::Unauthorized if auth == nil
|
139
|
+
return auth
|
140
|
+
end
|
141
|
+
|
142
|
+
ANONYMOUS_ACCESS = "POST/credentials"
|
143
|
+
|
144
|
+
# Defines whether the request is allowed to be processed
|
145
|
+
# without the need of a security token
|
146
|
+
#
|
147
|
+
def anonymous_access
|
148
|
+
"#{@http_method}/#{@model}" == ANONYMOUS_ACCESS
|
149
|
+
end
|
150
|
+
|
151
|
+
# Checks that a token is present in the request
|
152
|
+
# exception if doing a POST on a credentials property
|
153
|
+
def check_security( request )
|
154
|
+
auth_class = to_class( @servicemodule , @authmodel )
|
155
|
+
if @token == nil and !anonymous_access
|
156
|
+
raise WEBrick::HTTPStatus::Unauthorized
|
157
|
+
end
|
158
|
+
if @token != nil
|
159
|
+
@principal = auth_class.validate( @token )
|
160
|
+
raise WEBrick::HTTPStatus::Unauthorized if @principal == nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns the model defined by the configuration
|
165
|
+
# option :dashboard
|
166
|
+
#
|
167
|
+
def dashboard( request )
|
168
|
+
[ @dashboard ]
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
|
175
|
+
# Generic servlet, that implements the Moodisland Grape API
|
176
|
+
# This layer returns domain objects that can be included
|
177
|
+
# in atom feeds as entries.
|
178
|
+
#
|
179
|
+
class CRUDServlet < RESTServlet
|
180
|
+
|
181
|
+
# Creates and saves a new object. The object is then
|
182
|
+
# returned
|
183
|
+
def create( request )
|
184
|
+
clazz = to_class( @servicemodule, @model )
|
185
|
+
object = clazz.rest_create( @principal )
|
186
|
+
clazz.rest_bind( object, @body )
|
187
|
+
clazz.rest_save( object, @principal )
|
188
|
+
end
|
189
|
+
|
190
|
+
# Retrieves a list of objects
|
191
|
+
#
|
192
|
+
def retrieve( request )
|
193
|
+
clazz = to_class( @servicemodule, @model )
|
194
|
+
clazz.rest_retrieve( @principal )
|
195
|
+
end
|
196
|
+
|
197
|
+
# Retrieve a list of related objects
|
198
|
+
#
|
199
|
+
def retrieve_related( request )
|
200
|
+
clazz = to_class( @servicemodule, @model )
|
201
|
+
service_method = "rest_#{@property}"
|
202
|
+
error( 500, clazz.name, service_method ) if !clazz.respond_to?( service_method )
|
203
|
+
clazz.method( service_method ).call( @id, @principal )
|
204
|
+
end
|
205
|
+
|
206
|
+
# Retrieves a single object
|
207
|
+
#
|
208
|
+
def show( request )
|
209
|
+
clazz = to_class( @servicemodule, @model )
|
210
|
+
single = clazz.rest_single( @id, @principal )
|
211
|
+
raise error( 200, @model, @id ) if single == nil
|
212
|
+
return single
|
213
|
+
end
|
214
|
+
|
215
|
+
# Deletes a single object
|
216
|
+
#
|
217
|
+
def delete( request )
|
218
|
+
clazz = to_class( @servicemodule, @model )
|
219
|
+
clazz.rest_delete( @id, @principal )
|
220
|
+
end
|
221
|
+
|
222
|
+
# Retrieves, updates and saves an existing object.
|
223
|
+
# The object is then returned
|
224
|
+
#
|
225
|
+
def update( request )
|
226
|
+
object = show( request )
|
227
|
+
clazz = object.class
|
228
|
+
clazz.rest_bind( object, @body )
|
229
|
+
clazz.rest_save( object, @principal )
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# RubyRest: $Id:$
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#
|
5
|
+
|
6
|
+
module RubyRest
|
7
|
+
|
8
|
+
# This module provides a catalog of errors
|
9
|
+
# the application is supposed to throw.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
module Tools
|
13
|
+
|
14
|
+
|
15
|
+
ATOM_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
16
|
+
|
17
|
+
def nvl( value, default )
|
18
|
+
return value if value != nil
|
19
|
+
return default
|
20
|
+
end
|
21
|
+
|
22
|
+
def format_atom_date( value )
|
23
|
+
nvl( value, Time.now ).strftime( ATOM_DATE_FORMAT )
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_atom_date( value )
|
27
|
+
Date.strptime( value, ATOM_DATE_FORMAT )
|
28
|
+
end
|
29
|
+
|
30
|
+
# Resolves the specified module name and model
|
31
|
+
# into a class, and returns it
|
32
|
+
def to_class( modulename, model )
|
33
|
+
Class.by_name( "#{modulename}::#{model.capitalize}" )
|
34
|
+
end
|
35
|
+
|
36
|
+
# Builds a gem name
|
37
|
+
def to_gem_name( moduleprefix, modulename )
|
38
|
+
return modulename if moduleprefix == nil
|
39
|
+
return "#{moduleprefix}-#{modulename}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Builds a fully qualified module name
|
43
|
+
def to_module_name( moduleprefix, modulename )
|
44
|
+
return modulename.capitalize if moduleprefix == nil
|
45
|
+
return "#{moduleprefix.capitalize}::#{modulename.capitalize}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Catalog of error messages, indexed by error
|
49
|
+
# number
|
50
|
+
ERRORS = {
|
51
|
+
000 => "Missing configuration option",
|
52
|
+
001 => "Unable to connect to database. Missing method",
|
53
|
+
002 => "Unable to load schema. Missing method",
|
54
|
+
003 => "Unable to create table",
|
55
|
+
004 => "Please subclass and override",
|
56
|
+
005 => "Sorry, configuration method did not return a valid database connection",
|
57
|
+
006 => "Class was not property configured with its database connection (Sequel)",
|
58
|
+
100 => "Request path and HTTP method are not compatible",
|
59
|
+
200 => "No resource found for model and id",
|
60
|
+
500 => "No service method found in model class"
|
61
|
+
}
|
62
|
+
|
63
|
+
# Raises a new error. Resolves the specified
|
64
|
+
# number into a human readable message
|
65
|
+
def error( number, *params )
|
66
|
+
raise "\##{number}: #{ERRORS[number]}: #{params.join( ', ') }"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/lib/rubyrest.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# RubyRest: $Id:$
|
2
|
+
#
|
3
|
+
# Entry point to the framework.
|
4
|
+
# Loads all the files under the 'rubyrest' subdirectory
|
5
|
+
require "rubygems"
|
6
|
+
require "extensions/all"
|
7
|
+
require "builder"
|
8
|
+
require "rexml/document"
|
9
|
+
require "webrick"
|
10
|
+
require "atom"
|
11
|
+
|
12
|
+
dir = File.join( File.dirname( __FILE__ ), 'rubyrest' )
|
13
|
+
|
14
|
+
require File.join( dir, "tools" )
|
15
|
+
require File.join( dir, "atom" )
|
16
|
+
require File.join( dir, "servlets" )
|
17
|
+
require File.join( dir, "config" )
|
18
|
+
require File.join( dir, "engine" )
|
19
|
+
|
20
|
+
module RubyRest #:nodoc:
|
21
|
+
class << self
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.1
|
3
|
+
specification_version: 1
|
4
|
+
name: rubyrest
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2007-03-28 00:00:00 +02:00
|
8
|
+
summary: REST framework for Ruby.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: pedro.gutierrrez@netcourrier.com
|
12
|
+
homepage: http://rubyrest.rubyforge.org
|
13
|
+
rubyforge_project:
|
14
|
+
description: REST framework for Ruby.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.8.2
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Pedro Gutierrez
|
31
|
+
files:
|
32
|
+
- COPYING
|
33
|
+
- README
|
34
|
+
- Rakefile
|
35
|
+
- bin/rubyrest
|
36
|
+
- doc/rdoc
|
37
|
+
- lib/rubyrest
|
38
|
+
- lib/rubyrest.rb
|
39
|
+
- lib/rubyrest/atom.rb
|
40
|
+
- lib/rubyrest/config.rb
|
41
|
+
- lib/rubyrest/engine.rb
|
42
|
+
- lib/rubyrest/servlets.rb
|
43
|
+
- lib/rubyrest/tools.rb
|
44
|
+
- CHANGELOG
|
45
|
+
- examples/hello.rb
|
46
|
+
test_files: []
|
47
|
+
|
48
|
+
rdoc_options:
|
49
|
+
- --quiet
|
50
|
+
- --title
|
51
|
+
- "Ruby-on-Rest: A simple REST framework for Ruby"
|
52
|
+
- --opname
|
53
|
+
- index.html
|
54
|
+
- --line-numbers
|
55
|
+
- --main
|
56
|
+
- README
|
57
|
+
- --inline-source
|
58
|
+
- --exclude
|
59
|
+
- lib/rubyrest.rb
|
60
|
+
- --include
|
61
|
+
- examples/*.rb
|
62
|
+
extra_rdoc_files:
|
63
|
+
- README
|
64
|
+
- CHANGELOG
|
65
|
+
- COPYING
|
66
|
+
- examples/hello.rb
|
67
|
+
executables:
|
68
|
+
- rubyrest
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
dependencies:
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: metaid
|
76
|
+
version_requirement:
|
77
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.0.0
|
82
|
+
version:
|