rubyrest 0.0.1
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/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:
|