rubyrest 0.1.0 → 0.1.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 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