rubyrest 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ == Release 0.1.1
2
+
3
+ * New Application hierarchy introduces SimpleApplication and SecureApplication
4
+ * Method RubyRest::Client::Default#hash2entry fixed and now more robust
5
+ * Method 'load' has been replaced by method 'single' when loading resources via the domain, so that we avoid name clashes with Sequel.
6
+ * The 'service' and the 'model' are now merged into the 'domain'. This results in much simpler Ruby-on-Rest applications.
7
+ * Sequel::Application now imports a <service>.sql at startup, if :destroy == true
8
+
1
9
  == Release 0.1.0
2
10
 
3
11
  * A rewritten from scratch architecture and implementation that offers: pluggability, better object orientation, thread safety and backwards incompatibility (sorry for this one!)
data/README CHANGED
@@ -23,7 +23,7 @@ If you have any comments or suggestions please send an email to pedro dot gutier
23
23
 
24
24
  sudo gem install rubyrest
25
25
 
26
- === Learning by example
26
+ == Learning by example
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
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "rubyrest"
9
- VERS = "0.1.0"
9
+ VERS = "0.1.1"
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",
@@ -1,15 +1,12 @@
1
1
  module RubyRest
2
2
 
3
+ SYS_FILES = [ "application", "client" ].freeze
4
+
3
5
  # Base Application class for all applications deployed
4
6
  # with Ruby-on-Rest
5
- class Application
7
+ class AbstractApplication
6
8
  attr_reader :config
7
9
 
8
- # Defines whether update and create operations should
9
- # automatically load and create new domain model, and automatically
10
- # bind values from the request body document. Can be disabled
11
- # in subclasses.
12
- def auto_bind; true end
13
10
 
14
11
  # To be overriden by applications that use databases
15
12
  def init_database
@@ -19,35 +16,21 @@ module RubyRest
19
16
  def initialize( config )
20
17
  @config = config
21
18
  setup
22
- register_model
23
- register_resources
19
+ @resources_by_path = Hash.new
20
+ @resources_by_name = Hash.new
21
+ @resources_by_domain = Hash.new
22
+ register_domain
24
23
  register_formatters
25
24
  init_database
26
25
  end
27
26
 
28
27
  # Loads all the domain files provided by the developer
29
- def register_model
30
- path_to_model = @doc_base + "/domain"
31
- Dir.foreach( path_to_model ) { |filename|
28
+ def register_domain
29
+ Dir.foreach( @doc_base ) { |filename|
32
30
  filename = File.basename( filename, ".rb" )
33
- if !(File.basename( filename )[0] == ?.) && !FileTest.directory?( filename )
34
- require path_to_model + "/" + filename
35
- end
36
- }
37
- end
38
-
39
- # Loads all the resources configured under the 'resources'
40
- # folder relative to the application doc base
41
- def register_resources
42
- @resources_by_path = Hash.new
43
- @resources_by_model = Hash.new
44
- path_to_resources = @doc_base + "/resources"
45
-
46
- Dir.foreach( @doc_base + "/resources" ) { |filename|
47
- if !(File.basename( filename )[0] == ?.) && !FileTest.directory?( filename )
48
- filename = File.basename( filename, ".rb" )
49
- require path_to_resources + "/" + filename
50
- register_resource( to_resource_class( filename ).new( self ) )
31
+ if !(filename[0] == ?.) && !FileTest.directory?( filename ) && !SYS_FILES.include?( filename )
32
+ require @doc_base + "/" + filename
33
+ register_resource( filename.to_s.intern )
51
34
  end
52
35
  }
53
36
  end
@@ -64,7 +47,7 @@ module RubyRest
64
47
 
65
48
  # Resolves the Ruby-on-Rest resource descriptor for the
66
49
  # specified request path
67
- def resource_for_path( path )
50
+ def resource_by_path( path )
68
51
  mount_point = "/"
69
52
  path_tokens = path.split( "/" )
70
53
  mount_point = "/"+path_tokens[1] if path_tokens.length > 0
@@ -75,22 +58,39 @@ module RubyRest
75
58
 
76
59
  # Retrieves the Ruby-on-Rest resource descriptor for the specified
77
60
  # model object
78
- def resource_for_model( model )
79
- resource = @resources_by_model[model.class]
80
- raise "no resource mounted for model #{model.class}" if !resource
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
81
70
  return resource
82
71
  end
83
72
 
84
- # Registers a new resource instance
85
- def register_resource( resource )
86
- @resources_by_path[resource.class.mount_point] = resource
87
- @resources_by_model[resource.model]=resource.class
88
- puts "resource mounted: #{resource}"
73
+ # Registers a new resource for the given name
74
+ def register_resource( name )
75
+ puts "mounting resource: #{name} ..."
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]"
89
84
  end
90
85
 
91
- # Returns the class name for the given resource name
86
+ # Returns the resource class name for the given name
92
87
  def to_resource_class( name )
93
- Class.by_name( @config[:module].capitalize + "::" + @config[:service].capitalize + "::" + name.to_s.capitalize )
88
+ Class.by_name( @config[:module].capitalize + "::" + @config[:service].capitalize + "::Resource::" + name.to_s.capitalize )
89
+ end
90
+
91
+ # Returns the domain class name for the given name
92
+ def to_domain_class( name )
93
+ Class.by_name( @config[:module].capitalize + "::" + @config[:service].capitalize + "::Domain::" + name.to_s.capitalize )
94
94
  end
95
95
 
96
96
  # Register all the formatters supported by the application
@@ -106,28 +106,115 @@ module RubyRest
106
106
  @formatters[params[:format]]||@formatters[:atom]
107
107
  end
108
108
 
109
+ def render_model( params, model )
110
+ formatter( params ).format( model, params )
111
+ end
112
+
113
+ # Tells whether the specified model will be rendered
114
+ # as a feed or entry. To be subclassed
115
+ def is_a_collection( model )
116
+ model == nil || model.is_a?( Array )
117
+ end
118
+
119
+ # Implement me in subclasses
120
+ def is_a_service_doc( model )
121
+
122
+ end
123
+
124
+
125
+ end
126
+
127
+ # Basic CRUD application
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
+
109
155
  # Invoked by the web layer, on a DELETE request
110
156
  def delete( params )
111
- res = resource_for_path( params[:path] )
112
- res.service.delete( res.service.load( params[:target], params[:principal] ), params[:principal] )
157
+ res = resource_by_path( params[:path] )
158
+ res.domain.delete( params[:target] )
113
159
  end
114
160
 
115
- def render_model( params, model )
116
- formatter( params ).format( model, params )
161
+
162
+ end
163
+
164
+
165
+
166
+ # Specialization of a Simple application, that introduces the notion
167
+ # of security services and principals. It overrides the retrieve, create, update
168
+ # and delete methods and adds some extras like automatic binding.
169
+ class SecureApplication < RubyRest::SimpleApplication
170
+
171
+ # Defines whether update and create operations should
172
+ # automatically load and create new domain model, and automatically
173
+ # bind values from the request body document. Can be disabled
174
+ # in subclasses.
175
+ def auto_bind; true end
176
+
177
+ # Builds a new secured application and register
178
+ # its security service
179
+ def initialize( config )
180
+ super( config )
181
+ register_security
182
+ end
183
+
184
+ # Register the security service, if specified in the
185
+ # application configuration hash
186
+ def register_security
187
+ raise "no security service was defined in application #{self}" if !@config[:security_service]
188
+ client_class = Class.by_name( (@config[:security_module]||@config[:module]).to_s.capitalize + "::" + @config[:security_service].to_s.capitalize + "::Client" )
189
+ @security = client_class.new( @config[:security_host]||"localhost", @config[:security_port] )
190
+ end
191
+
192
+ # Returns the security service, of nil if not configured
193
+ def security
194
+ @security
195
+ end
196
+
197
+ # Resolves the principal, by inoking the security service
198
+ def resolve_principal( params )
199
+ principal = @security.principal( params[:authkey] )
200
+ raise "No principal was found for authentication key: #{params[:authkey]}" if !principal
201
+ params[:principal]=principal
202
+ return params
117
203
  end
118
204
 
119
205
  # Invoked by the web layer, on a GET request.
120
206
  # Retrieves the collection or resource, and formats the
121
207
  # result as a feed, entry or service document
122
208
  def retrieve( params )
123
- res = resource_for_path( params[:path] )
209
+ params = resolve_principal( params )
210
+ res = resource_by_path( params[:path] )
124
211
  if params[:target] == nil
125
- objects = res.service.list( params[:principal] )
212
+ objects = res.domain.list( params[:principal] )
126
213
  else
127
214
  if params[:property] == nil
128
- objects = res.service.load( params[:target], params[:principal] )
215
+ objects = res.domain.single( params[:target], params[:principal] )
129
216
  else
130
- objects = res.service.list_related( params[:target], params[:property], params[:principal] )
217
+ objects = res.domain.list_related( params[:target], params[:property], params[:principal] )
131
218
  end
132
219
  end
133
220
  render_model( params, objects )
@@ -138,9 +225,10 @@ module RubyRest
138
225
  # a fresh new model object populated with the data
139
226
  # found in the request body.
140
227
  def create( params )
141
- res = resource_for_path( params[:path] )
142
- params[:body] = res.bind( res.service.create( params[:principal] ), params[:body] ) if auto_bind == true
143
- object = res.service.save_new( params[:body], params[:principal] )
228
+ params = resolve_principal( params )
229
+ res = resource_by_path( params[:path] )
230
+ params[:body] = res.bind( res.domain.create( params[:principal] ), params[:body] ) if auto_bind == true
231
+ object = res.domain.save_new( params[:body], params[:principal] )
144
232
  render_model( params, object )
145
233
  end
146
234
 
@@ -148,30 +236,26 @@ module RubyRest
148
236
  # This method delegates to the resource's service and provides an existing model
149
237
  # object, loaded and populated with the data found in the request body.
150
238
  def update( params )
151
- res = resource_for_path( params[:path] )
152
- params[:body] = res.bind( res.service.load( params[:target], params[:principal] ), params[:body] ) if auto_bind == true
153
- object = res.service.save_existing( params[:target], params[:body], params[:principal] )
239
+ params = resolve_principal( params )
240
+ res = resource_by_path( params[:path] )
241
+ params[:body] = res.bind( res.domain.single( params[:target], params[:principal] ), params[:body] ) if auto_bind == true
242
+ object = res.domain.save_existing( params[:target], params[:body], params[:principal] )
154
243
  render_model( params, object )
155
244
  end
156
245
 
157
- # Tells whether the specified model will be rendered
158
- # as a feed or entry. To be subclassed
159
- def is_a_collection( model )
160
- model == nil || model.is_a?( Array )
161
- end
162
-
163
- # Implement me in subclasses
164
- def is_a_service_doc( model )
165
-
246
+ # Invoked by the web layer, on a DELETE request
247
+ def delete( params )
248
+ params = resolve_principal( params )
249
+ res = resource_by_path( params[:path] )
250
+ res.domain.delete( res.domain.single( params[:target], params[:principal] ), params[:principal] )
166
251
  end
167
252
 
168
253
  end
169
254
 
170
-
171
- # Specialization of a basic application, that provides support for
255
+ # Specialization of a secure application, that provides support for
172
256
  # Sequel (http://sequel.rubyforge.org) configuration at startup by
173
257
  # implementing the 'init_database' method
174
- class SequelApplication < RubyRest::Application
258
+ class SequelApplication < RubyRest::SecureApplication
175
259
 
176
260
  # Specifies the list of resources to be persisted
177
261
  def self.with_persistent_resources *resources
@@ -184,26 +268,37 @@ module RubyRest
184
268
  return @persistent
185
269
  end
186
270
 
271
+ # Builds the path to the sqlite database file
272
+ def to_database_path
273
+ "#{@config[:workdir]}/#{@config[:service]}-#{@config[:database]}.database"
274
+ end
275
+
187
276
  # Connects all persistent resources to the database and optionnally
188
277
  # recreates the database schema. For the moment this only
189
278
  # works with PostgreSQL databases
190
279
  def init_database
191
280
  @db = Sequel::Postgres::Database.new( @config )
281
+
282
+ #@db = Sequel.open "sqlite:///#{to_database_path}"
192
283
  self.class.persistent_resources.each{ |r|
193
- to_resource_class( r ).model.db=@db
284
+ resource = resource_by_name( r )
285
+ resource.domain.db=@db
286
+ if @config[:destroy]
287
+ resource.domain.recreate_table
288
+ end
194
289
  }
195
- if @config[:destroy]
196
- self.class.persistent_resources.each{ |r|
197
- to_resource_class( r ).model.recreate_table
198
- }
199
- puts "importing initial data..."
200
- import_data
201
- end
290
+ import_data if @config[:destroy]
202
291
  end
203
292
 
204
293
  # PLease override this in subclasses
205
294
  def import_data
206
-
295
+ begin
296
+ import_file = @config[:resources_dir]||"" + @config[:service] + ".sql"
297
+ IO.readlines( import_file, ";" ).each{ |insert| @db.execute insert }
298
+ rescue => e
299
+ puts "skipping import of #{import_file}. Reason: #{e.message}"
300
+ end
301
+ puts "data import complete from #{import_file} ..."
207
302
  end
208
303
 
209
304
  # Tells whether the specified model will be rendered
data/lib/rubyrest/atom.rb CHANGED
@@ -116,7 +116,9 @@ module RubyRest
116
116
  end
117
117
 
118
118
  def xml_value( object, options )
119
- return object.get_value( tag( options ), options[:required]||true )
119
+ required = true
120
+ required = options[:required] if options[:required] != nil
121
+ return object.get_value( tag( options ), required )
120
122
  end
121
123
 
122
124
  def bind( object, options, value )
@@ -147,11 +149,15 @@ module RubyRest
147
149
 
148
150
  class DateProperty < SimpleProperty
149
151
 
150
- def date2string( date )
152
+ def self.format_date( date )
151
153
  date = Time.now if !date
152
154
  date.strftime( ATOM_DATE_FORMAT )
153
155
  end
154
156
 
157
+ def date2string( date )
158
+ self.class.format_date( date )
159
+ end
160
+
155
161
  def string2date( date )
156
162
  Date.strptime( value, ATOM_DATE_FORMAT )
157
163
  end
@@ -167,9 +173,18 @@ module RubyRest
167
173
  end
168
174
 
169
175
  class DomainFormatter
176
+
177
+ # Creates a new formatter, as a child of the
178
+ # specified parent formatter
170
179
  def initialize( parent )
171
180
  @parent = parent
172
181
  end
182
+
183
+ # shortcut to the application
184
+ def app
185
+ @parent.app
186
+ end
187
+
173
188
  end
174
189
 
175
190
  class FeedFormatter < DomainFormatter
@@ -198,22 +213,22 @@ module RubyRest
198
213
  end
199
214
 
200
215
  def format_entry( object, xml, params )
201
- resource = @parent.app.resource_for_model( object )
216
+ resource = @parent.app.resource_by_domain( object )
202
217
  raise "no resource found for entry #{object}" if !resource
203
-
204
218
  entry = xml.add_element( "entry", NAMESPACES )
205
219
  entry.add_element( "title" )
206
- entry.add_element( "author" )
207
- entry.add_element( "updated" )
220
+ entry.add_element( "author" ).add_element( "name" ).add_text( object.author )
221
+ entry.add_element( "updated" ).add_text( DateProperty.format_date( object.updated ) )
208
222
  entry.add_element( "id" )
209
223
  entry.add_element( "summary" )
210
224
 
211
- resource.links.each{ |link|
212
- entry.add_element( "link", link )
225
+ resource.class.links.each{ |link|
226
+ render = params[:principal].profile.split( "," ).include?( link[:role].to_s )
227
+ entry.add_element( "link", { "title" => link[:title].to_s, "rel" => link[:rel].to_s} ) if render
213
228
  }
214
229
 
215
230
  content = entry.add_element( "rubyrest:content" )
216
- resource.props.each{ |map|
231
+ resource.class.props.each{ |map|
217
232
  @parent.property( map ).format( object, map, content )
218
233
  }
219
234
  end
@@ -252,6 +267,7 @@ module RubyRest
252
267
 
253
268
  def add( uri, title, accept )
254
269
  @collections << ServiceCollection.new( uri, title, accept )
270
+ return self
255
271
  end
256
272
 
257
273
  end
@@ -91,7 +91,7 @@ module RubyRest
91
91
  def hash2entry( data )
92
92
  doc = RubyRest::Atom::Document.new
93
93
  content = doc.add_element( "entry" ).add_element( "rubyrest:content" )
94
- data.each { |name,value| content.add_element( name.to_s ).add_text( value ) }
94
+ data.each { |name,value| content.add_element( name.to_s ).add_text( value.to_s ) }
95
95
  return doc
96
96
  end
97
97
 
@@ -9,14 +9,19 @@ module RubyRest
9
9
  @app = app
10
10
  end
11
11
 
12
- # Convenience method
13
- def service
14
- self.class.service
12
+ # Sets the domain implementation for this resource
13
+ def self.set_domain domain_klass
14
+ @domain = domain_klass
15
+ end
16
+
17
+ # Returns the domain implementation
18
+ def self.domain
19
+ @domain
15
20
  end
16
21
 
17
22
  # Convenience method
18
- def model
19
- self.class.model
23
+ def domain
24
+ self.class.domain
20
25
  end
21
26
 
22
27
  # Defines the url type the resource is going to
@@ -29,22 +34,6 @@ module RubyRest
29
34
  @mount_point
30
35
  end
31
36
 
32
- def self.for_model model_klass
33
- @model = model_klass
34
- end
35
-
36
- def self.model
37
- @model || Class.by_name( self.name + "Model" )
38
- end
39
-
40
- def self.for_service service_klass
41
- @service = service_klass
42
- end
43
-
44
- def self.service
45
- @service || Class.by_name( self.name + "Service" )
46
- end
47
-
48
37
  def self.atom modifiers
49
38
  self.props << modifiers
50
39
  end
@@ -79,7 +68,7 @@ module RubyRest
79
68
 
80
69
  # String representation of a resource
81
70
  def to_s
82
- "path #{self.class.mount_point}, service #{self.class.service}, model #{self.class.model}"
71
+ "path #{self.class.mount_point}, domain #{self.domain}"
83
72
  end
84
73
 
85
74
  end
@@ -48,6 +48,7 @@ module RubyRest
48
48
  def args( request )
49
49
  args = Hash.new
50
50
  url_tokens = decode_path( request.path ).split( "/")
51
+ args[:authkey] = request["token"]
51
52
  args[:target] = url_tokens[2] if url_tokens.length>2
52
53
  args[:property] = url_tokens[3] if url_tokens.length>3
53
54
  args[:path] = request.path
@@ -58,6 +59,7 @@ module RubyRest
58
59
  def do_GET( request, response )
59
60
  params = args(request )
60
61
  response.status = 200
62
+ params[:action]=:retrieve
61
63
  xml = @app.retrieve( params )
62
64
  response["content-type"]=params[:content_type]
63
65
  xml.write( response.body )
@@ -66,6 +68,7 @@ module RubyRest
66
68
  def do_POST( request, response )
67
69
  params = args(request )
68
70
  response.status = 201
71
+ params[:action]=:create
69
72
  xml = @app.create( params )
70
73
  response["content-type"]=params[:content_type]
71
74
  xml.write( response.body )
@@ -74,6 +77,7 @@ module RubyRest
74
77
  def do_PUT( request, response )
75
78
  params = args(request )
76
79
  response.status = 200
80
+ params[:action]=:update
77
81
  xml = @app.update( params )
78
82
  response["content-type"]=params[:content_type]
79
83
  xml.write( response.body )
@@ -82,6 +86,7 @@ module RubyRest
82
86
  def do_DELETE( request, response )
83
87
  params = args(request )
84
88
  response.status = 200
89
+ params[:action]=:delete
85
90
  @app.delete( params )
86
91
  end
87
92
 
data/lib/rubyrest.rb CHANGED
@@ -5,6 +5,7 @@
5
5
  require "rubygems"
6
6
  require "sequel"
7
7
  require "sequel/postgres"
8
+ require "sequel/sqlite"
8
9
  require "extensions/all"
9
10
  require "rexml/document"
10
11
  require "webrick"
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.1.0
7
- date: 2007-06-04 00:00:00 +02:00
6
+ version: 0.1.1
7
+ date: 2007-06-10 00:00:00 +02:00
8
8
  summary: REST framework for Ruby.
9
9
  require_paths:
10
10
  - lib