rubyrest 0.1.1 → 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/CHANGELOG +12 -0
- data/README +7 -8
- data/Rakefile +1 -1
- data/bin/rubyrest +1 -0
- data/lib/rubyrest.rb +2 -1
- data/lib/rubyrest/application.rb +164 -152
- data/lib/rubyrest/atom.rb +60 -28
- data/lib/rubyrest/client.rb +56 -27
- data/lib/rubyrest/engine.rb +5 -1
- data/lib/rubyrest/resource.rb +64 -29
- data/lib/rubyrest/webrick.rb +13 -8
- data/lib/rubyrest/yaml.rb +33 -0
- metadata +3 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
== Release 0.2.0
|
2
|
+
|
3
|
+
* The Resource infrastructure has been almost rewritten from scratch. Ruby-on-Rest applications are now much easier to write.
|
4
|
+
* Atom is now deprecated. The default formatter is now based on Yaml.
|
5
|
+
* Simpler support for Sequel based applications
|
6
|
+
* The Hello sample app now uses RSpec to describe its behaviour and provides a starting point for developing new Ruby-on-Rest applications.
|
7
|
+
|
8
|
+
== Release 0.1.2
|
9
|
+
|
10
|
+
* Sequel:Application now supports SQLite rather than PostgreSQL, plus a better database import filename construction and logging at startup.
|
11
|
+
* The application is now injected into Domain classes, so the domain can access the application's global configuration (Domain must define #app and #app= methods).
|
12
|
+
|
1
13
|
== Release 0.1.1
|
2
14
|
|
3
15
|
* New Application hierarchy introduces SimpleApplication and SecureApplication
|
data/README
CHANGED
@@ -27,7 +27,11 @@ If you have any comments or suggestions please send an email to pedro dot gutier
|
|
27
27
|
|
28
28
|
Maybe the easiest way to learn how Ruby-on-Rest works is to take a look at the following examples, included in the source distribution:
|
29
29
|
|
30
|
-
* The 'Hello' application can be used as a skeletton for new Ruby-on-Rest applications
|
30
|
+
* The 'Hello' application can be used as a skeletton for new Ruby-on-Rest applications. You can test it by running the specifications:
|
31
|
+
|
32
|
+
spec hello_spec.rb
|
33
|
+
|
34
|
+
and hopefully everything goes green :D
|
31
35
|
|
32
36
|
== Starting the service
|
33
37
|
|
@@ -45,13 +49,8 @@ Ruby-on-Rest can be embedded in Ruby code. This is useful for testing purposes:
|
|
45
49
|
|
46
50
|
config = {
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
"version" => "v1.0",
|
51
|
-
"webserver" => :webrick,
|
52
|
-
"destroy" => false ,
|
53
|
-
"daemon" => true,
|
54
|
-
"http_port" => 10000
|
52
|
+
( ... config options here, see the hello app to get started ...)
|
53
|
+
|
55
54
|
}
|
56
55
|
|
57
56
|
engine = RubyRest::Engine.new( config )
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
|
|
6
6
|
include FileUtils
|
7
7
|
|
8
8
|
NAME = "rubyrest"
|
9
|
-
VERS = "0.
|
9
|
+
VERS = "0.2.0"
|
10
10
|
CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
|
11
11
|
RDOC_OPTS = ['--quiet', '--title', "Ruby-on-Rest: A simple REST framework for Ruby",
|
12
12
|
"--opname", "index.html",
|
data/bin/rubyrest
CHANGED
@@ -23,6 +23,7 @@ else
|
|
23
23
|
# deploy specific configuration, and build a hash out of it
|
24
24
|
params = YAML::load_file( "#{service}_#{deploy_mode}.yaml" )
|
25
25
|
params[ "service" ] = service
|
26
|
+
params[ "deploy_mode" ] = deploy_mode
|
26
27
|
|
27
28
|
engine = RubyRest::Engine.new( params )
|
28
29
|
engine.start
|
data/lib/rubyrest.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
# Entry point to the framework.
|
4
4
|
# Loads all the files under the 'rubyrest' subdirectory
|
5
5
|
require "rubygems"
|
6
|
+
require "logger"
|
6
7
|
require "sequel"
|
7
|
-
require "sequel/postgres"
|
8
8
|
require "sequel/sqlite"
|
9
9
|
require "extensions/all"
|
10
10
|
require "rexml/document"
|
@@ -18,4 +18,5 @@ require File.join( dir, "resource" )
|
|
18
18
|
require File.join( dir, "application" )
|
19
19
|
require File.join( dir, "engine" )
|
20
20
|
require File.join( dir, "atom" )
|
21
|
+
require File.join( dir, "yaml" )
|
21
22
|
require File.join( dir, "client" )
|
data/lib/rubyrest/application.rb
CHANGED
@@ -1,43 +1,116 @@
|
|
1
1
|
module RubyRest
|
2
2
|
|
3
|
+
# Logger configuration, can be reused in RubyRest and Non RubyRest applications
|
4
|
+
module ApplicationLogger
|
5
|
+
|
6
|
+
def init_logger
|
7
|
+
case @config[:deploy_mode]
|
8
|
+
when "dev"
|
9
|
+
@logger = Logger.new( STDOUT )
|
10
|
+
@logger.level = Logger::DEBUG
|
11
|
+
when "live"
|
12
|
+
raise "you need to specify a logfile when deploying in LIVE mode" if !@config[:log]
|
13
|
+
@logger = Logger.new( @config[:log], 10, 1024000 )
|
14
|
+
@logger.level = Logger::ERROR
|
15
|
+
else
|
16
|
+
@logger = Logger.new( STDOUT )
|
17
|
+
@logger.level = Logger::DEBUG
|
18
|
+
end
|
19
|
+
@logger.info "log level set to #{@logger.level} [OK]" if @logger.info?
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
3
26
|
SYS_FILES = [ "application", "client" ].freeze
|
4
27
|
|
5
28
|
# Base Application class for all applications deployed
|
6
29
|
# with Ruby-on-Rest
|
7
30
|
class AbstractApplication
|
8
|
-
|
31
|
+
include RubyRest::ApplicationLogger
|
9
32
|
|
33
|
+
attr_reader :config, :logger
|
10
34
|
|
11
35
|
# To be overriden by applications that use databases
|
12
36
|
def init_database
|
37
|
+
|
38
|
+
end
|
13
39
|
|
40
|
+
# To be overriden
|
41
|
+
def create_tables
|
42
|
+
|
14
43
|
end
|
15
44
|
|
16
45
|
def initialize( config )
|
17
46
|
@config = config
|
18
|
-
|
19
|
-
@resources_by_path = Hash.new
|
20
|
-
@resources_by_name = Hash.new
|
21
|
-
@resources_by_domain = Hash.new
|
22
|
-
register_domain
|
23
|
-
register_formatters
|
47
|
+
init_logger
|
24
48
|
init_database
|
49
|
+
if @config[:destroy]==true
|
50
|
+
drop_schema
|
51
|
+
create_schema
|
52
|
+
load_initial_data if self.respond_to?( :load_initial_data )
|
53
|
+
end
|
54
|
+
register_resources
|
55
|
+
register_formatters
|
56
|
+
register_services
|
25
57
|
end
|
26
58
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
59
|
+
# Checks the configuration and issues an error if the specified
|
60
|
+
# key was not found
|
61
|
+
def check_config( key )
|
62
|
+
raise "missing configuration key: #{key}" if !@config[key]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Specifies the list of resources to be persisted
|
66
|
+
def self.with_resources *resources
|
67
|
+
@resources = resources
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the list of persistent resources
|
71
|
+
def self.resources
|
72
|
+
@resources = [] if !@resources
|
73
|
+
return @resources
|
74
|
+
end
|
75
|
+
|
76
|
+
def register_resources
|
77
|
+
@resources = Hash.new
|
78
|
+
self.class.resources.each{ |name|
|
79
|
+
resource_klass = to_resource_class( name )
|
80
|
+
resource = resource_klass.new( self, name )
|
81
|
+
@resources[resource_klass.mount_point]=resource
|
82
|
+
@resources[name]=resource
|
83
|
+
@logger.info "resource mounted: #{resource} [OK]" if @logger.info?
|
35
84
|
}
|
36
85
|
end
|
37
86
|
|
38
|
-
#
|
39
|
-
def
|
87
|
+
# Registers a single service
|
88
|
+
def register_service( service )
|
89
|
+
module_key = "#{service}_module".intern
|
90
|
+
module_key = :module if @config[module_key] == nil
|
91
|
+
|
92
|
+
host_key = "#{service}_host".intern
|
93
|
+
port_key = "#{service}_port".intern
|
94
|
+
role_key = "#{service}_role".intern
|
40
95
|
|
96
|
+
check_config( role_key )
|
97
|
+
check_config( port_key )
|
98
|
+
|
99
|
+
client_class = to_service_class( @config[module_key], service )
|
100
|
+
@services[@config[role_key].intern] = client_class.new( @config[host_key]||"localhost", @config[port_key] )
|
101
|
+
|
102
|
+
@logger.info "service registered #{service} as #{@config[role_key].intern}" if @logger.info?
|
103
|
+
end
|
104
|
+
|
105
|
+
# Registers all the services configured at startup
|
106
|
+
def register_services
|
107
|
+
@services = Hash.new
|
108
|
+
@config[:services].each{ |service| register_service( service ) } if @config[:services] != nil
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a service, by name
|
112
|
+
def service( name )
|
113
|
+
@services.fetch( name )
|
41
114
|
end
|
42
115
|
|
43
116
|
# Returns the description and version
|
@@ -51,65 +124,57 @@ module RubyRest
|
|
51
124
|
mount_point = "/"
|
52
125
|
path_tokens = path.split( "/" )
|
53
126
|
mount_point = "/"+path_tokens[1] if path_tokens.length > 0
|
54
|
-
resource
|
55
|
-
raise "no resource mounted at #{mount_point}" if !resource
|
56
|
-
return resource
|
57
|
-
end
|
58
|
-
|
59
|
-
# Retrieves the Ruby-on-Rest resource descriptor for the specified
|
60
|
-
# model object
|
61
|
-
def resource_by_domain( model )
|
62
|
-
resource = @resources_by_domain[model.class]
|
63
|
-
raise "no resource mounted for domain #{model.class}" if !resource
|
64
|
-
return resource
|
65
|
-
end
|
66
|
-
|
67
|
-
def resource_by_name( name )
|
68
|
-
resource = @resources_by_name[name]
|
69
|
-
raise "no resource mounted for name #{name}" if !resource
|
70
|
-
return resource
|
127
|
+
resource(mount_point)
|
71
128
|
end
|
72
129
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
resource_klass = to_resource_class( name )
|
77
|
-
domain_klass = to_domain_class( name )
|
78
|
-
resource_klass.set_domain domain_klass
|
79
|
-
resource = resource_klass.new( self )
|
80
|
-
@resources_by_path[resource_klass.mount_point] = resource
|
81
|
-
@resources_by_domain[domain_klass]=resource
|
82
|
-
@resources_by_name[name]=resource
|
83
|
-
puts "resource mounted: #{resource} [OK]"
|
130
|
+
def resource( key )
|
131
|
+
raise "no resource found at key: #{key}" if @resources.key?( key ) == false
|
132
|
+
@resources[key]
|
84
133
|
end
|
85
134
|
|
86
135
|
# Returns the resource class name for the given name
|
87
136
|
def to_resource_class( name )
|
88
|
-
Class.by_name( @config[:module].capitalize + "::" + @config[:service].capitalize + "::
|
137
|
+
Class.by_name( @config[:module].to_s.capitalize + "::" + @config[:service].to_s.capitalize + "::" + name.to_s.capitalize )
|
89
138
|
end
|
90
139
|
|
91
|
-
|
92
|
-
|
93
|
-
Class.by_name( @config[:module].capitalize + "::" + @config[:service].capitalize + "::Domain::" + name.to_s.capitalize )
|
140
|
+
def to_service_class( module_name, service_name )
|
141
|
+
Class.by_name( module_name.to_s.capitalize + "::" + service_name.to_s.capitalize + "::Client" )
|
94
142
|
end
|
95
143
|
|
96
144
|
# Register all the formatters supported by the application
|
97
145
|
# For the moment, only the Atom representation of resources
|
98
146
|
# is supported
|
99
147
|
def register_formatters
|
100
|
-
@formatters = {
|
148
|
+
@formatters = {
|
149
|
+
:atom => RubyRest::Atom::Formatter.new( self ),
|
150
|
+
:yaml => RubyRest::Format::Yaml.new( self )
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_request( params, request )
|
155
|
+
data = formatter( params ).parse_request( request.body )
|
156
|
+
@logger.info( data ) if @logger.info?
|
157
|
+
return data
|
101
158
|
end
|
102
159
|
|
160
|
+
|
103
161
|
# Resolves the formatter to use in the returned
|
104
162
|
# representation
|
105
163
|
def formatter( params )
|
106
|
-
@formatters[params[:format]]||@formatters[:
|
164
|
+
@formatters[params[:format]]||@formatters[:yaml]
|
107
165
|
end
|
108
166
|
|
109
167
|
def render_model( params, model )
|
110
168
|
formatter( params ).format( model, params )
|
111
169
|
end
|
112
170
|
|
171
|
+
# Binds values found in the specified source in
|
172
|
+
# properties of the specified domain object
|
173
|
+
# Delegates to the formatter
|
174
|
+
def bind( object, params )
|
175
|
+
formatter( params ).bind( object, params[:body], params )
|
176
|
+
end
|
177
|
+
|
113
178
|
# Tells whether the specified model will be rendered
|
114
179
|
# as a feed or entry. To be subclassed
|
115
180
|
def is_a_collection( model )
|
@@ -118,55 +183,15 @@ module RubyRest
|
|
118
183
|
|
119
184
|
# Implement me in subclasses
|
120
185
|
def is_a_service_doc( model )
|
121
|
-
|
186
|
+
model != nil && model.is_a?( RubyRest::Atom::ServiceDocument )
|
122
187
|
end
|
123
188
|
|
124
|
-
|
125
189
|
end
|
126
190
|
|
127
|
-
#
|
128
|
-
class SimpleApplication < RubyRest::AbstractApplication
|
129
|
-
|
130
|
-
# Invoked by the web layer, on a GET request.
|
131
|
-
# Retrieves the collection or resource, and formats the
|
132
|
-
# result as a feed, entry or service document
|
133
|
-
def retrieve( params )
|
134
|
-
res = resource_by_path( params[:path] )
|
135
|
-
render_model( params, res.domain.retrieve( params[:target], params[:property] ) )
|
136
|
-
end
|
137
|
-
|
138
|
-
# Invoked by the web layer, on a POST request.
|
139
|
-
# This method delegates to the resource's service and provides
|
140
|
-
# a fresh new model object populated with the data
|
141
|
-
# found in the request body.
|
142
|
-
def create( params )
|
143
|
-
res = resource_by_path( params[:path] )
|
144
|
-
render_model( params, res.domain.create( params[:body] ) )
|
145
|
-
end
|
146
|
-
|
147
|
-
# Invoked by the web layer, on a PUT request
|
148
|
-
# This method delegates to the resource's service and provides an existing model
|
149
|
-
# object, loaded and populated with the data found in the request body.
|
150
|
-
def update( params )
|
151
|
-
res = resource_by_path( params[:path] )
|
152
|
-
render_model( params, res.domain.update( params[:target], params[:body] ) )
|
153
|
-
end
|
154
|
-
|
155
|
-
# Invoked by the web layer, on a DELETE request
|
156
|
-
def delete( params )
|
157
|
-
res = resource_by_path( params[:path] )
|
158
|
-
res.domain.delete( params[:target] )
|
159
|
-
end
|
160
|
-
|
161
|
-
|
162
|
-
end
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
# Specialization of a Simple application, that introduces the notion
|
191
|
+
# Specialization of an Abstract application, that introduces the notion
|
167
192
|
# of security services and principals. It overrides the retrieve, create, update
|
168
193
|
# and delete methods and adds some extras like automatic binding.
|
169
|
-
class SecureApplication < RubyRest::
|
194
|
+
class SecureApplication < RubyRest::AbstractApplication
|
170
195
|
|
171
196
|
# Defines whether update and create operations should
|
172
197
|
# automatically load and create new domain model, and automatically
|
@@ -178,25 +203,24 @@ module RubyRest
|
|
178
203
|
# its security service
|
179
204
|
def initialize( config )
|
180
205
|
super( config )
|
181
|
-
register_security
|
206
|
+
register_security if !@config[:disable_security]
|
182
207
|
end
|
183
208
|
|
184
|
-
# Register the security service, if specified in the
|
185
|
-
# application configuration hash
|
186
209
|
def register_security
|
187
|
-
|
188
|
-
|
189
|
-
|
210
|
+
if !@services[:security]
|
211
|
+
raise "no security service was defined in application #{self}"
|
212
|
+
end
|
190
213
|
end
|
191
214
|
|
192
215
|
# Returns the security service, of nil if not configured
|
193
216
|
def security
|
194
|
-
@security
|
217
|
+
@services[:security]
|
195
218
|
end
|
196
219
|
|
197
220
|
# Resolves the principal, by inoking the security service
|
198
221
|
def resolve_principal( params )
|
199
|
-
|
222
|
+
return params if @config[:disable_security]
|
223
|
+
principal = security.principal( params[:authkey] )
|
200
224
|
raise "No principal was found for authentication key: #{params[:authkey]}" if !principal
|
201
225
|
params[:principal]=principal
|
202
226
|
return params
|
@@ -208,13 +232,15 @@ module RubyRest
|
|
208
232
|
def retrieve( params )
|
209
233
|
params = resolve_principal( params )
|
210
234
|
res = resource_by_path( params[:path] )
|
235
|
+
params[:resource]=res
|
236
|
+
|
211
237
|
if params[:target] == nil
|
212
|
-
objects = res.
|
238
|
+
objects = res.list( params[:principal] )
|
213
239
|
else
|
214
240
|
if params[:property] == nil
|
215
|
-
objects = res.
|
241
|
+
objects = res.single( params[:target], params[:principal] )
|
216
242
|
else
|
217
|
-
objects = res.
|
243
|
+
objects = res.list_related( params[:target], params[:property], params[:principal] )
|
218
244
|
end
|
219
245
|
end
|
220
246
|
render_model( params, objects )
|
@@ -227,8 +253,9 @@ module RubyRest
|
|
227
253
|
def create( params )
|
228
254
|
params = resolve_principal( params )
|
229
255
|
res = resource_by_path( params[:path] )
|
230
|
-
params[:
|
231
|
-
|
256
|
+
params[:resource]=res
|
257
|
+
params[:body] = bind( res.new_instance( params[:principal] ), params ) if auto_bind == true
|
258
|
+
object = res.save_new( params[:body], params[:principal] )
|
232
259
|
render_model( params, object )
|
233
260
|
end
|
234
261
|
|
@@ -238,8 +265,9 @@ module RubyRest
|
|
238
265
|
def update( params )
|
239
266
|
params = resolve_principal( params )
|
240
267
|
res = resource_by_path( params[:path] )
|
241
|
-
params[:
|
242
|
-
|
268
|
+
params[:resource]=res
|
269
|
+
params[:body] = bind( res.single( params[:target], params[:principal] ), params ) if auto_bind == true
|
270
|
+
object = res.save_existing( params[:body], params[:principal] )
|
243
271
|
render_model( params, object )
|
244
272
|
end
|
245
273
|
|
@@ -247,7 +275,8 @@ module RubyRest
|
|
247
275
|
def delete( params )
|
248
276
|
params = resolve_principal( params )
|
249
277
|
res = resource_by_path( params[:path] )
|
250
|
-
|
278
|
+
params[:resource]=res
|
279
|
+
res.delete_existing( res.single( params[:target], params[:principal] ), params[:principal] )
|
251
280
|
end
|
252
281
|
|
253
282
|
end
|
@@ -257,59 +286,42 @@ module RubyRest
|
|
257
286
|
# implementing the 'init_database' method
|
258
287
|
class SequelApplication < RubyRest::SecureApplication
|
259
288
|
|
260
|
-
|
261
|
-
def self.with_persistent_resources *resources
|
262
|
-
@persistent = resources
|
263
|
-
end
|
264
|
-
|
265
|
-
# Returns the list of persistent resources
|
266
|
-
def self.persistent_resources
|
267
|
-
@persistent = [] if !@persistent
|
268
|
-
return @persistent
|
269
|
-
end
|
289
|
+
attr_reader :db
|
270
290
|
|
271
|
-
#
|
272
|
-
|
273
|
-
|
291
|
+
# Tells whether the specified model will be rendered
|
292
|
+
# as a feed or entry. To be subclassed
|
293
|
+
def is_a_collection( model )
|
294
|
+
super( model ) || model.is_a?( Sequel::Dataset )
|
274
295
|
end
|
275
296
|
|
276
|
-
# Connects all persistent resources to the database and optionnally
|
277
|
-
# recreates the database schema. For the moment this only
|
278
|
-
# works with PostgreSQL databases
|
279
297
|
def init_database
|
280
|
-
|
281
|
-
|
282
|
-
#@db = Sequel.open "sqlite:///#{to_database_path}"
|
283
|
-
self.class.persistent_resources.each{ |r|
|
284
|
-
resource = resource_by_name( r )
|
285
|
-
resource.domain.db=@db
|
286
|
-
if @config[:destroy]
|
287
|
-
resource.domain.recreate_table
|
288
|
-
end
|
289
|
-
}
|
290
|
-
import_data if @config[:destroy]
|
298
|
+
check_config( :database )
|
299
|
+
@db = Sequel.open( "sqlite:///#{@config[:database]}", :max_connections => 10, :logger => @logger )
|
291
300
|
end
|
292
301
|
|
293
|
-
#
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
302
|
+
# Convenience method that hides that safely
|
303
|
+
# drops a table
|
304
|
+
def drop_table( name )
|
305
|
+
|
306
|
+
begin
|
307
|
+
@db << "DROP TABLE #{name}"
|
308
|
+
rescue => e
|
309
|
+
@logger.warn e.message if @logger.warn?
|
300
310
|
end
|
301
|
-
|
311
|
+
|
302
312
|
end
|
303
313
|
|
304
|
-
#
|
305
|
-
|
306
|
-
|
307
|
-
|
314
|
+
# Convenience method that safely drops a view
|
315
|
+
def drop_view( name )
|
316
|
+
|
317
|
+
begin
|
318
|
+
@db << "DROP VIEW #{name}"
|
319
|
+
rescue => e
|
320
|
+
@logger.warn e.message if @logger.warn?
|
321
|
+
end
|
322
|
+
|
308
323
|
end
|
309
324
|
|
310
|
-
def is_a_service_doc( model )
|
311
|
-
super( model ) || model.is_a?( RubyRest::Atom::ServiceDocument )
|
312
|
-
end
|
313
325
|
|
314
326
|
end
|
315
327
|
|
data/lib/rubyrest/atom.rb
CHANGED
@@ -15,22 +15,14 @@ module RubyRest
|
|
15
15
|
ATOM_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
16
16
|
|
17
17
|
|
18
|
-
|
19
|
-
# Ruby-on-Rest specialization of the REXML document
|
20
|
-
# class. Adds some convenient ways of accessing data from
|
21
|
-
# a Atom document
|
22
|
-
class Document < REXML::Document
|
23
|
-
|
24
|
-
def method_missing( name, *args )
|
25
|
-
get_value( name )
|
26
|
-
end
|
18
|
+
module Extensions
|
27
19
|
|
28
20
|
# Resolves the missing method into a content property
|
29
21
|
# and returns its text value
|
30
22
|
def get_value( name, required=true )
|
31
23
|
location = "/entry/rubyrest:content/#{name}"
|
32
24
|
value = text( location )
|
33
|
-
raise "missing value at location #{location} in #{self.to_s}" if required && !value
|
25
|
+
raise "missing value at location #{location} in #{self.to_s}" if required && (!value || value.length == 0)
|
34
26
|
return value
|
35
27
|
end
|
36
28
|
|
@@ -38,18 +30,27 @@ module RubyRest
|
|
38
30
|
get_text( "/entry/rubyrest:content/#{name}" ).value = value
|
39
31
|
end
|
40
32
|
|
41
|
-
|
42
|
-
|
43
|
-
def id
|
44
|
-
text( "/entry/rubyrest:content/id" )
|
33
|
+
def method_missing( name, *args )
|
34
|
+
get_value( name )
|
45
35
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def clone
|
50
|
-
self.class.new self.to_s
|
36
|
+
|
37
|
+
def new_copy
|
38
|
+
REXML::Document.new self.to_s
|
51
39
|
end
|
52
|
-
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class REXML::Element
|
44
|
+
include RubyRest::Atom::Extensions
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# Ruby-on-Rest specialization of the REXML document
|
49
|
+
# class. Adds some convenient ways of accessing data from
|
50
|
+
# a Atom document
|
51
|
+
class REXML::Document
|
52
|
+
include RubyRest::Atom::Extensions
|
53
|
+
|
53
54
|
end
|
54
55
|
|
55
56
|
# Main Atom formatter class, composed of more specialized objects
|
@@ -71,6 +72,8 @@ module RubyRest
|
|
71
72
|
@props=Hash.new
|
72
73
|
@props[:simple]=SimpleProperty.new( self )
|
73
74
|
@props[:date]=DateProperty.new( self )
|
75
|
+
@props[:nested]=NestedProperty.new( self )
|
76
|
+
|
74
77
|
end
|
75
78
|
|
76
79
|
# Resolves the specified name into a formatter
|
@@ -106,13 +109,18 @@ module RubyRest
|
|
106
109
|
@parent = parent
|
107
110
|
end
|
108
111
|
|
112
|
+
# Shortcut method
|
113
|
+
def app
|
114
|
+
@parent.app
|
115
|
+
end
|
116
|
+
|
109
117
|
def tag( options )
|
110
118
|
value = options[:tag]||options[:property]
|
111
119
|
return value.to_s
|
112
120
|
end
|
113
121
|
|
114
122
|
def object_value( object, options )
|
115
|
-
object
|
123
|
+
object[ options[:property] ]
|
116
124
|
end
|
117
125
|
|
118
126
|
def xml_value( object, options )
|
@@ -122,7 +130,7 @@ module RubyRest
|
|
122
130
|
end
|
123
131
|
|
124
132
|
def bind( object, options, value )
|
125
|
-
object
|
133
|
+
object[ options[:property] ]=value
|
126
134
|
end
|
127
135
|
|
128
136
|
def parse( object, options, xml )
|
@@ -151,6 +159,7 @@ module RubyRest
|
|
151
159
|
|
152
160
|
def self.format_date( date )
|
153
161
|
date = Time.now if !date
|
162
|
+
date = Time.at( date ) if date.is_a?( Bignum )
|
154
163
|
date.strftime( ATOM_DATE_FORMAT )
|
155
164
|
end
|
156
165
|
|
@@ -158,8 +167,9 @@ module RubyRest
|
|
158
167
|
self.class.format_date( date )
|
159
168
|
end
|
160
169
|
|
161
|
-
def string2date(
|
162
|
-
Date.strptime(
|
170
|
+
def string2date( str )
|
171
|
+
date = Date.strptime( str, ATOM_DATE_FORMAT )
|
172
|
+
Time.gm( date.year, date.month, date.day, 0, 0, 0, 0 ).to_i
|
163
173
|
end
|
164
174
|
|
165
175
|
def value_from_model( options, object )
|
@@ -172,6 +182,28 @@ module RubyRest
|
|
172
182
|
|
173
183
|
end
|
174
184
|
|
185
|
+
|
186
|
+
class NestedProperty < SimpleProperty
|
187
|
+
|
188
|
+
NESTED_PROPERTY_SEPARATOR = '_'.freeze
|
189
|
+
|
190
|
+
def bind( object, options, value )
|
191
|
+
prop = options[:property]
|
192
|
+
tokens = prop.to_s.split( NESTED_PROPERTY_SEPARATOR )
|
193
|
+
property = tokens[0].intern
|
194
|
+
nested_prop = tokens[1]
|
195
|
+
object[property]= app.resource( property ).method( "first_by_#{nested_prop}").call( value )[:id]
|
196
|
+
end
|
197
|
+
|
198
|
+
#def object_value( object, options )
|
199
|
+
# prop = options[:property]
|
200
|
+
# value = object
|
201
|
+
# prop.to_s.split( NESTED_PROPERTY_SEPARATOR ).each{ |token| value = value.send token.to_s }
|
202
|
+
# return value
|
203
|
+
#end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
175
207
|
class DomainFormatter
|
176
208
|
|
177
209
|
# Creates a new formatter, as a child of the
|
@@ -213,17 +245,17 @@ module RubyRest
|
|
213
245
|
end
|
214
246
|
|
215
247
|
def format_entry( object, xml, params )
|
216
|
-
resource =
|
248
|
+
resource = params[:resource]
|
217
249
|
raise "no resource found for entry #{object}" if !resource
|
218
250
|
entry = xml.add_element( "entry", NAMESPACES )
|
219
251
|
entry.add_element( "title" )
|
220
|
-
entry.add_element( "author" ).add_element( "name" ).add_text( object
|
221
|
-
entry.add_element( "updated" ).add_text( DateProperty.format_date( object
|
252
|
+
entry.add_element( "author" ).add_element( "name" ).add_text( object[:modifiedby] )
|
253
|
+
entry.add_element( "updated" ).add_text( DateProperty.format_date( object[:modified] ) )
|
222
254
|
entry.add_element( "id" )
|
223
255
|
entry.add_element( "summary" )
|
224
256
|
|
225
257
|
resource.class.links.each{ |link|
|
226
|
-
render = params[:principal].profile.split( "," ).include?( link[:role].to_s )
|
258
|
+
render = link[:role] == nil || params[:principal].profile.split( "," ).include?( link[:role].to_s )
|
227
259
|
entry.add_element( "link", { "title" => link[:title].to_s, "rel" => link[:rel].to_s} ) if render
|
228
260
|
}
|
229
261
|
|
data/lib/rubyrest/client.rb
CHANGED
@@ -7,7 +7,7 @@ module RubyRest
|
|
7
7
|
# Default client for web services deployed with
|
8
8
|
# Ruby-on-Rest. Translates create, retrieve, update and
|
9
9
|
# delete methods into POST, GET, PUT and DELETE http requests
|
10
|
-
class
|
10
|
+
class Abstract
|
11
11
|
|
12
12
|
# Configures the server name and port
|
13
13
|
def initialize( host, port )
|
@@ -29,7 +29,7 @@ module RubyRest
|
|
29
29
|
path << to_query_string( data ) if data
|
30
30
|
headers = prepare_headers( api_key )
|
31
31
|
rsp = http.get( encode_path( path ), headers )
|
32
|
-
return
|
32
|
+
return hidrate_data( rsp ) if rsp.code.to_i == 200
|
33
33
|
end
|
34
34
|
|
35
35
|
# Converts the specified hash of data into a
|
@@ -38,10 +38,10 @@ module RubyRest
|
|
38
38
|
# the response status code is 201.
|
39
39
|
def create( path, data, api_key = nil )
|
40
40
|
body = nil
|
41
|
-
body =
|
41
|
+
body = serialize_data( data ).to_s if data != nil
|
42
42
|
headers = prepare_headers( api_key )
|
43
43
|
rsp = http.post( encode_path( path ), body, headers )
|
44
|
-
return
|
44
|
+
return hidrate_data( rsp ) if rsp.code.to_i == 201
|
45
45
|
end
|
46
46
|
|
47
47
|
# Converts the specified hash of data into a
|
@@ -50,10 +50,10 @@ module RubyRest
|
|
50
50
|
# the response status code is 200.
|
51
51
|
def update( path, data, api_key = nil)
|
52
52
|
body = nil
|
53
|
-
body =
|
53
|
+
body = serialize_data( data ).to_s if data != nil
|
54
54
|
headers = prepare_headers( api_key )
|
55
55
|
rsp = http.put( encode_path( path ), body, headers )
|
56
|
-
return
|
56
|
+
return hidrate_data( rsp ) if rsp.code.to_i == 200
|
57
57
|
end
|
58
58
|
|
59
59
|
# Converts the specified hash of data into a
|
@@ -63,7 +63,8 @@ module RubyRest
|
|
63
63
|
path << to_query_string( data ) if data
|
64
64
|
headers = prepare_headers( api_key )
|
65
65
|
rsp = http.delete( encode_path( path ), headers )
|
66
|
-
rsp.code.to_i
|
66
|
+
raise "error when deleting" if rsp.code.to_i != 200
|
67
|
+
return nil
|
67
68
|
end
|
68
69
|
|
69
70
|
# Convenience method that returns the
|
@@ -83,42 +84,70 @@ module RubyRest
|
|
83
84
|
def prepare_headers( api_key = nil )
|
84
85
|
headers = { "Content-Type" => "text/xml; charset=utf-8" }
|
85
86
|
headers[ "token" ] = api_key if api_key != nil
|
87
|
+
headers["format"]=@format
|
86
88
|
return headers
|
87
89
|
end
|
88
90
|
|
89
|
-
#
|
90
|
-
#
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
91
|
+
# Reusable method that creates a query string
|
92
|
+
# with the specified hash of data
|
93
|
+
def to_query_string( data )
|
94
|
+
qs = "?"
|
95
|
+
params.each{ |k,v| qs << "#{k}=#{v}&" }
|
96
|
+
qs.chop!
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
class Yaml < Abstract
|
103
|
+
|
104
|
+
def initialize( host, port )
|
105
|
+
super( host, port )
|
106
|
+
@format = "yaml"
|
107
|
+
end
|
108
|
+
|
109
|
+
def hidrate_data( response )
|
110
|
+
YAML::load( response.body ) if response.body
|
111
|
+
end
|
112
|
+
|
113
|
+
def serialize_data( hash )
|
114
|
+
hash.to_yaml
|
96
115
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
class Atom < Abstract
|
121
|
+
|
122
|
+
def initialize( host, port )
|
123
|
+
super( host, port )
|
124
|
+
@format = "atom"
|
125
|
+
end
|
126
|
+
|
127
|
+
def hidrate_data( response )
|
101
128
|
begin
|
102
|
-
|
129
|
+
REXML::Document.new( response.body ) if response.body
|
103
130
|
rescue => e
|
104
131
|
puts "unable to parse response body: " + e.message
|
105
132
|
puts "---- response body ----"
|
106
|
-
puts
|
133
|
+
puts response.body
|
107
134
|
puts "-----------------------"
|
108
135
|
return nil
|
109
136
|
end
|
110
137
|
end
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
qs.chop!
|
138
|
+
|
139
|
+
def serialize_data( hash )
|
140
|
+
doc = REXML::Document.new
|
141
|
+
content = doc.add_element( "entry" ).add_element( "rubyrest:content" )
|
142
|
+
hash.each { |name,value| content.add_element( name.to_s ).add_text( value.to_s ) }
|
143
|
+
return doc
|
118
144
|
end
|
145
|
+
|
146
|
+
|
119
147
|
|
120
148
|
end
|
121
149
|
|
150
|
+
|
122
151
|
end
|
123
152
|
|
124
153
|
end
|
data/lib/rubyrest/engine.rb
CHANGED
@@ -12,7 +12,7 @@ module RubyRest
|
|
12
12
|
# the statement +require @service+ works.
|
13
13
|
def initialize( config )
|
14
14
|
params = intern_config( config )
|
15
|
-
|
15
|
+
require_library( params )
|
16
16
|
@app = Class.by_name( params[:module].capitalize + "::" + params[:service].capitalize + "::Application" ).new( params )
|
17
17
|
if params[:daemon ] != nil
|
18
18
|
@daemon = params[:daemon ]
|
@@ -20,6 +20,10 @@ module RubyRest
|
|
20
20
|
@server = Class.by_name( "RubyRest::" + params[:webserver].to_s.capitalize + "::Server" ).new( @app )
|
21
21
|
end
|
22
22
|
|
23
|
+
# Requires the library params
|
24
|
+
def require_library( params )
|
25
|
+
require params[:gem] if params[:gem]
|
26
|
+
end
|
23
27
|
|
24
28
|
# Returns a new hash, with all the keys as symbols
|
25
29
|
def intern_config( hash )
|
data/lib/rubyrest/resource.rb
CHANGED
@@ -1,27 +1,25 @@
|
|
1
1
|
module RubyRest
|
2
|
+
|
3
|
+
module Resource
|
4
|
+
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
|
7
|
+
# Base REST Resource class that acts as a wrapper for the actual
|
8
|
+
# business logic, data formatting (response time) and validations (request time)
|
9
|
+
class Base
|
10
|
+
|
11
|
+
attr_reader :app, :name
|
6
12
|
|
7
13
|
# Creates a new resource, under the given application
|
8
|
-
def initialize( app )
|
14
|
+
def initialize( app, name )
|
9
15
|
@app = app
|
16
|
+
@logger = app.logger
|
17
|
+
@name = name
|
10
18
|
end
|
11
19
|
|
12
|
-
#
|
13
|
-
def
|
14
|
-
@
|
15
|
-
end
|
16
|
-
|
17
|
-
# Returns the domain implementation
|
18
|
-
def self.domain
|
19
|
-
@domain
|
20
|
-
end
|
21
|
-
|
22
|
-
# Convenience method
|
23
|
-
def domain
|
24
|
-
self.class.domain
|
20
|
+
# Shortcut method to access other resources
|
21
|
+
def resource( name )
|
22
|
+
@app.resource( name )
|
25
23
|
end
|
26
24
|
|
27
25
|
# Defines the url type the resource is going to
|
@@ -34,6 +32,24 @@ module RubyRest
|
|
34
32
|
@mount_point
|
35
33
|
end
|
36
34
|
|
35
|
+
# Sets the list of properties thqt can be updated
|
36
|
+
def self.with_mutable_props *props
|
37
|
+
@mutable_props = *props
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the list of properties thqt can be
|
41
|
+
# updated
|
42
|
+
def self.mutable_props; @mutable_props end
|
43
|
+
|
44
|
+
def self.related opts
|
45
|
+
self.related_resources[opts[:property]] =opts[:resource]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.related_resources
|
49
|
+
@related_resources = Hash.new if !@related_resources
|
50
|
+
return @related_resources
|
51
|
+
end
|
52
|
+
|
37
53
|
def self.atom modifiers
|
38
54
|
self.props << modifiers
|
39
55
|
end
|
@@ -56,21 +72,40 @@ module RubyRest
|
|
56
72
|
return @links
|
57
73
|
end
|
58
74
|
|
59
|
-
# Updates the specified object with the data found
|
60
|
-
# in the specified xml object, and the rules defined in
|
61
|
-
# the resource parsers
|
62
|
-
def bind( object, xml )
|
63
|
-
self.class.props.each{ |p|
|
64
|
-
@app.formatter(p).property( p ).parse( object, p, xml )
|
65
|
-
}
|
66
|
-
return object
|
67
|
-
end
|
68
|
-
|
69
75
|
# String representation of a resource
|
70
76
|
def to_s
|
71
|
-
"path #{self.class.mount_point},
|
77
|
+
"path #{self.class.mount_point}, class #{self.class}"
|
72
78
|
end
|
73
79
|
|
74
|
-
|
80
|
+
end
|
75
81
|
|
82
|
+
class Sequel < Base
|
83
|
+
|
84
|
+
attr_reader :dataset
|
85
|
+
|
86
|
+
# Creates a new resource, under the given application
|
87
|
+
def initialize( app, name )
|
88
|
+
super( app, name )
|
89
|
+
@dataset = app.db[self.class.table_name]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Provides access to an arbitrary table
|
93
|
+
def table( name )
|
94
|
+
app.db[name]
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.with_table_name table_name
|
98
|
+
@table_name = table_name
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.table_name
|
102
|
+
@table_name
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
76
111
|
end
|
data/lib/rubyrest/webrick.rb
CHANGED
@@ -11,7 +11,7 @@ module RubyRest
|
|
11
11
|
# Builds a new Webrick adapter instance
|
12
12
|
# for the given application
|
13
13
|
def initialize( app )
|
14
|
-
super( :Port => app.config[:http_port] )
|
14
|
+
super( :Port => app.config[:http_port], :Logger => app.logger )
|
15
15
|
@app = app
|
16
16
|
servlet = RubyRest::Webrick::Servlet.new( self )
|
17
17
|
mount "/", servlet
|
@@ -49,10 +49,11 @@ module RubyRest
|
|
49
49
|
args = Hash.new
|
50
50
|
url_tokens = decode_path( request.path ).split( "/")
|
51
51
|
args[:authkey] = request["token"]
|
52
|
+
args[:format]= request["format"]
|
52
53
|
args[:target] = url_tokens[2] if url_tokens.length>2
|
53
54
|
args[:property] = url_tokens[3] if url_tokens.length>3
|
54
55
|
args[:path] = request.path
|
55
|
-
args[:body] =
|
56
|
+
args[:body] = @app.parse_request( args, request ) if request.body
|
56
57
|
return args
|
57
58
|
end
|
58
59
|
|
@@ -60,27 +61,27 @@ module RubyRest
|
|
60
61
|
params = args(request )
|
61
62
|
response.status = 200
|
62
63
|
params[:action]=:retrieve
|
63
|
-
|
64
|
+
data = @app.retrieve( params )
|
64
65
|
response["content-type"]=params[:content_type]
|
65
|
-
|
66
|
+
response.body = data
|
66
67
|
end
|
67
68
|
|
68
69
|
def do_POST( request, response )
|
69
70
|
params = args(request )
|
70
71
|
response.status = 201
|
71
72
|
params[:action]=:create
|
72
|
-
|
73
|
+
data = @app.create( params )
|
73
74
|
response["content-type"]=params[:content_type]
|
74
|
-
|
75
|
+
response.body = data
|
75
76
|
end
|
76
77
|
|
77
78
|
def do_PUT( request, response )
|
78
79
|
params = args(request )
|
79
80
|
response.status = 200
|
80
81
|
params[:action]=:update
|
81
|
-
|
82
|
+
data = @app.update( params )
|
82
83
|
response["content-type"]=params[:content_type]
|
83
|
-
|
84
|
+
response.body = data
|
84
85
|
end
|
85
86
|
|
86
87
|
def do_DELETE( request, response )
|
@@ -90,6 +91,10 @@ module RubyRest
|
|
90
91
|
@app.delete( params )
|
91
92
|
end
|
92
93
|
|
94
|
+
def to_s
|
95
|
+
"Ruby-on-Rest WEBrick servlet"
|
96
|
+
end
|
97
|
+
|
93
98
|
end
|
94
99
|
|
95
100
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RubyRest
|
2
|
+
|
3
|
+
module Format
|
4
|
+
|
5
|
+
class Yaml
|
6
|
+
|
7
|
+
def initialize( app )
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def format( model, params )
|
12
|
+
model.to_yaml
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_request( request )
|
16
|
+
YAML::load( request )
|
17
|
+
end
|
18
|
+
|
19
|
+
# Treats the object and the source
|
20
|
+
# as a hash, and updates the object
|
21
|
+
def bind( object, source, params )
|
22
|
+
# preprocesses the source hash
|
23
|
+
# and updates the domain data
|
24
|
+
resource = params[:resource]
|
25
|
+
resource.process_request_data( source ) if resource.respond_to?( :process_request_data )
|
26
|
+
object.update( source )
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.1
|
|
3
3
|
specification_version: 1
|
4
4
|
name: rubyrest
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-08-17 00:00:00 +02:00
|
8
8
|
summary: REST framework for Ruby.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -43,6 +43,7 @@ files:
|
|
43
43
|
- lib/rubyrest/engine.rb
|
44
44
|
- lib/rubyrest/resource.rb
|
45
45
|
- lib/rubyrest/webrick.rb
|
46
|
+
- lib/rubyrest/yaml.rb
|
46
47
|
test_files: []
|
47
48
|
|
48
49
|
rdoc_options:
|