rubyrest 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
- "module" => "acme",
49
- "service" => "hello",
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.1.1"
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" )
@@ -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
- attr_reader :config
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
- setup
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
- # Loads all the domain files provided by the developer
28
- def register_domain
29
- Dir.foreach( @doc_base ) { |filename|
30
- filename = File.basename( filename, ".rb" )
31
- if !(filename[0] == ?.) && !FileTest.directory?( filename ) && !SYS_FILES.include?( filename )
32
- require @doc_base + "/" + filename
33
- register_resource( filename.to_s.intern )
34
- end
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
- # Please override me if needed
39
- def setup
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 = @resources_by_path[mount_point]
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
- # 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]"
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 + "::Resource::" + name.to_s.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
- # 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 )
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 = { :atom => RubyRest::Atom::Formatter.new( self ) }
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[:atom]
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
- # 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
-
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::SimpleApplication
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
- 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] )
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
- principal = @security.principal( params[:authkey] )
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.domain.list( params[:principal] )
238
+ objects = res.list( params[:principal] )
213
239
  else
214
240
  if params[:property] == nil
215
- objects = res.domain.single( params[:target], params[:principal] )
241
+ objects = res.single( params[:target], params[:principal] )
216
242
  else
217
- objects = res.domain.list_related( params[:target], params[:property], params[:principal] )
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[:body] = res.bind( res.domain.create( params[:principal] ), params[:body] ) if auto_bind == true
231
- object = res.domain.save_new( params[:body], params[:principal] )
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[: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] )
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
- res.domain.delete( res.domain.single( params[:target], params[:principal] ), params[:principal] )
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
- # Specifies the list of resources to be persisted
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
- # Builds the path to the sqlite database file
272
- def to_database_path
273
- "#{@config[:workdir]}/#{@config[:service]}-#{@config[:database]}.database"
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
- @db = Sequel::Postgres::Database.new( @config )
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
- # PLease override this in subclasses
294
- def import_data
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}"
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
- puts "data import complete from #{import_file} ..."
311
+
302
312
  end
303
313
 
304
- # Tells whether the specified model will be rendered
305
- # as a feed or entry. To be subclassed
306
- def is_a_collection( model )
307
- super( model ) || model.is_a?( Sequel::Dataset )
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
- # Shortcut for the id element contained
42
- # within the content.
43
- def id
44
- text( "/entry/rubyrest:content/id" )
33
+ def method_missing( name, *args )
34
+ get_value( name )
45
35
  end
46
-
47
- # Overrides the default implementation
48
- # by returning a new Ruby-on-Rest Atom Document
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.send options[:property]
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.send options[:property].to_s + "=", value
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( date )
162
- Date.strptime( value, ATOM_DATE_FORMAT )
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 = @parent.app.resource_by_domain( object )
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.author )
221
- entry.add_element( "updated" ).add_text( DateProperty.format_date( object.updated ) )
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
 
@@ -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 Default
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 to_xml( rsp ) if rsp.code.to_i == 200
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 = hash2entry( data ).to_s if data != nil
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 to_xml( rsp ) if rsp.code.to_i == 201
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 = hash2entry( data ).to_s if data != nil
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 to_xml( rsp ) if rsp.code.to_i == 200
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
- # Converts the specified hash of data into a simplified
90
- # Atom Entry document.
91
- def hash2entry( data )
92
- doc = RubyRest::Atom::Document.new
93
- content = doc.add_element( "entry" ).add_element( "rubyrest:content" )
94
- data.each { |name,value| content.add_element( name.to_s ).add_text( value.to_s ) }
95
- return doc
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
- # Parses the response body string as a Ruby-on-Rest XML Document,
99
- # which can be a Feed or Entry, or Service Document
100
- def to_xml( rsp )
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
- RubyRest::Atom::Document.new( rsp.body ) if rsp.body
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 rsp.body
133
+ puts response.body
107
134
  puts "-----------------------"
108
135
  return nil
109
136
  end
110
137
  end
111
-
112
- # Reusable method that creates a query string
113
- # with the specified hash of data
114
- def to_query_string( data )
115
- qs = "?"
116
- params.each{ |k,v| qs << "#{k}=#{v}&" }
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
@@ -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
- require params[:service]
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 )
@@ -1,27 +1,25 @@
1
1
  module RubyRest
2
+
3
+ module Resource
4
+
2
5
 
3
- # Base REST Resource class that acts as a wrapper for the actual
4
- # business logic, data formatting (response time) and validations (request time)
5
- class Resource
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
- # 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
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}, domain #{self.domain}"
77
+ "path #{self.class.mount_point}, class #{self.class}"
72
78
  end
73
79
 
74
- end
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
@@ -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] = RubyRest::Atom::Document.new( request.body ) if request.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
- xml = @app.retrieve( params )
64
+ data = @app.retrieve( params )
64
65
  response["content-type"]=params[:content_type]
65
- xml.write( response.body )
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
- xml = @app.create( params )
73
+ data = @app.create( params )
73
74
  response["content-type"]=params[:content_type]
74
- xml.write( response.body )
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
- xml = @app.update( params )
82
+ data = @app.update( params )
82
83
  response["content-type"]=params[:content_type]
83
- xml.write( response.body )
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.1.1
7
- date: 2007-06-10 00:00:00 +02:00
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: