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 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: