rubyrest 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rubyrest/atom.rb CHANGED
@@ -5,245 +5,269 @@
5
5
  #
6
6
  # $Id:$
7
7
  module RubyRest
8
-
9
8
  module Atom
10
- include RubyRest::Tools
11
-
12
- NAMESPACES = {
9
+
10
+ NAMESPACES = {
13
11
  "xmlns" => "http://www.w3.org/2005/Atom",
14
- "xmlns:moodisland" => "http://www.moodisland.com/ns#"
15
- }
16
-
17
- ATOM_TYPE = "application/atom+xml".freeze
18
- ATOMSERV_TYPE = "application/atomserv+xml".freeze
19
- HTML_TYPE = "text/html".freeze
20
- WORKSPACE_METHOD = "dashboard".freeze
21
- MODULEID = "Ruby-on-Rest (http://rubyrest.rubyforge.org)".freeze
22
-
23
-
24
-
25
-
26
- #
27
- # Formats the response as a Atom feed, Atom Entry or
28
- # Atom Service Document
29
- def format_response( request, response )
30
-
31
- builder = Builder::XmlMarkup.new( :target => response.body )
32
- builder.instruct!
33
-
34
- if @service_method == "dashboard"
35
- response[ "content-type" ] = ATOMSERV_TYPE
36
- build_service_document( @result, builder, request.request_uri )
37
- else
38
- response[ "content-type" ] = ATOM_TYPE
39
- if @result.respond_to? "each"
40
- title = @property || @model
41
- build_feed( @result, builder, request.request_uri, request.path, title )
42
- else build_entry( @result, builder, request.path ) end
43
- end
44
- end
45
-
46
-
47
- # Builds an Atom Service Document. This is a representation of the
48
- # user's dashboard or initial workspace.
49
- def build_service_document( collections, builder, uri )
50
- builder.service( NAMESPACES ){
51
- builder.workspace {
52
- builder.title "Dashboard"
53
- if collections != nil and collections.respond_to?( :each )
54
- collections.each { |col|
55
- builder.collection( { "href" => "/#{col}" } ) {
56
- builder.title col
57
- builder.accept "entry"
58
- }
59
- }
60
- end
61
- }
62
- }
63
- end
64
-
65
- # Builds an Atom Feed representation of the specified collection
66
- # of entries
67
- #
68
- def build_feed( entries, builder, uri, path, title )
69
- builder.feed( NAMESPACES ) {
70
- builder.id uri
71
- builder.link( { "rel" => "self", "href" => path, "type" => ATOM_TYPE } )
72
- builder.link( { "rel" => "alternate", "href" => uri, "type" => HTML_TYPE } )
73
- builder.title title
74
- builder.updated format_atom_date( Time.now )
75
- entries.each { |object| build_entry( object, builder, uri ) }
76
- }
77
- end
78
-
79
- # Builds an Atom Entry representation of the specified
80
- # object.
81
- #
82
- # The object is supposed to implement the following mandatory methods:
83
- # atom_id, atom_title, atom_author, atom_updated, atom_summary
84
- #
85
- # The object can implement the following optionnal methods:
86
- # atom_related, atom_content
87
- #
88
- def build_entry( object, builder, uri )
89
-
90
- entry_link = uri
91
- entry_link = "#{uri}/#{object.atom_id}" if @id == nil
92
-
93
- builder.entry( NAMESPACES ) {
94
- builder.title object.atom_title
95
- builder.author { builder.name object.atom_author }
96
- builder.updated format_atom_date( object.atom_updated )
97
- builder.id entry_link
98
- builder.summary object.atom_summary
99
- builder.link( :rel => :alternate, :href => entry_link )
100
-
101
- if object.respond_to?( :atom_related )
102
- related_entities = object.atom_related( @principal )
103
- if related_entities != nil
104
- related_entities.each{ |related|
105
- case related[:type]
106
- when :child
107
- builder.link( :rel => related[:type], :href => "#{entry_link}/#{related[:model]}", :title => related[:model], :type => ATOM_TYPE )
108
- when :parent
109
- builder.link( :rel => related[:type], :href => "/#{related[:model]}/#{object.send related[:model]}", :title => related[:model], :type => ATOM_TYPE )
110
- else :siblings
111
- builder.link( :rel => related[:type], :href => "/#{related[:model]}", :title => related[:model], :type => ATOM_TYPE )
112
- end
113
- }
114
- end
115
- end
116
-
117
- builder.moodisland :content do
118
- object.atom_content( builder ) if object.respond_to? :atom_content
119
- end
120
- }
12
+ "xmlns:rubyrest" => "http://rubyrest.rubyforge.org/ns#"
13
+ }
14
+
15
+ ATOM_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
121
16
 
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 )
122
26
  end
123
27
 
124
- # This module provides some default, arbitrary implementations
125
- # for methods required by RubyRest in order to
126
- # provide an Atom Entry representation out of a domain object.
127
- #
128
- # Developpers can choose whether to use this implementation
129
- # or to provide their own.
130
- module Entry
131
-
132
- # Returns the Atom Entry title
133
- def atom_title
134
- self.name
135
- end
136
-
137
- # Returns the Atom Entry Summary. Synonym of atom_title
138
- def atom_summary
139
- atom_title
140
- end
28
+ # Resolves the missing method into a content property
29
+ # and returns its text value
30
+ def get_value( name, required=true )
31
+ location = "/entry/rubyrest:content/#{name}"
32
+ value = text( location )
33
+ raise "missing value at location #{location} in #{self.to_s}" if required && !value
34
+ return value
35
+ end
36
+
37
+ def set_value( name, value )
38
+ get_text( "/entry/rubyrest:content/#{name}" ).value = value
39
+ end
141
40
 
142
- # Returns the generated token
143
- def atom_id
144
- self.id
145
- end
41
+ # Shortcut for the id element contained
42
+ # within the content.
43
+ def id
44
+ text( "/entry/rubyrest:content/id" )
45
+ end
146
46
 
147
- # Returns Time.now
148
- def atom_updated
149
- self.updated
150
- end
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
51
+ end
151
52
 
152
- # Not a very relevant buy necessary information
153
- def atom_author
154
- self.createdby
155
- end
53
+ end
54
+
55
+ # Main Atom formatter class, composed of more specialized objects
56
+ # such as Feed, Entry, ServiceDocument and property formatters
57
+ class Formatter
58
+
59
+ attr_reader :app
60
+
61
+ # Inits all the specific formatters and property handlers used by this
62
+ # main formatter
63
+ def initialize( app )
64
+ @app = app
156
65
 
157
- # Adds some convenience methods to a standard REXML
158
- # document. This class supposes that the document is an Atom
159
- # entry
160
- class Document < REXML::Document
161
-
162
- # Resolves the missing method into a content property
163
- # and returns its text value
164
- def method_missing( name, *args )
165
- location = "/entry/moodisland:content/#{name}"
166
- method_name = name.to_s.chomp
167
- if method_name == name.to_s
168
- text( location )
169
- else get_text( location ).value = args[0] end
170
- end
171
-
172
- # Shortcut for the id element contained
173
- # within the content.
174
- def id
175
- text( "/entry/moodisland:content/id" )
176
- end
177
-
178
- # Overrides the default implementation
179
- # by returning a new Ruby-on-Rest Atom Document
180
- def clone
181
- self.class.new self.to_s
182
- end
183
-
184
- end
185
-
66
+ @formatters=Hash.new
67
+ @formatters[:feed]=FeedFormatter.new( self )
68
+ @formatters[:entry]=EntryFormatter.new( self )
69
+ @formatters[:service_doc]=ServiceDocFormatter.new( self )
70
+
71
+ @props=Hash.new
72
+ @props[:simple]=SimpleProperty.new( self )
73
+ @props[:date]=DateProperty.new( self )
186
74
  end
187
75
 
76
+ # Resolves the specified name into a formatter
77
+ # object
78
+ def formatter_for_name( name )
79
+ raise "No formatter was found for name #{name}" if !@formatters[name]
80
+ return @formatters[name]
81
+ end
188
82
 
189
- # This module provides a failsafe implementation for
190
- # methods required by RubyRest in order to
191
- # provide an Atom Entry representation out of a domain object.
192
- #
193
- # Developpers should rather use RubyRest::Atom::Entry or
194
- # provide their own.
195
- module DummyEntry
196
-
197
- # Returns the object's class name
198
- def atom_title
199
- self.class.name
200
- end
201
-
202
- # Returns the Atom Entry Summary.
203
- # Synonym of atom_title
204
- def atom_summary
205
- atom_title
206
- end
207
-
208
- # Generates an id from the current time value
209
- def atom_id
210
- Time.now.to_i
211
- end
212
-
213
- # Returns Time.now
214
- def atom_updated
215
- Time.now
216
- end
217
-
218
- # Overrides the implementation provided
219
- # by RubyRest::Atom::Entry
220
- def atom_author
221
- MODULEID
222
- end
223
-
83
+ # Resolves the formatter to use
84
+ def formatter_for_model( object )
85
+ return @formatters[:feed] if @app.is_a_collection( object )
86
+ return @formatters[:service_doc] if @app.is_a_service_doc( object )
87
+ @formatters[:entry]
88
+ end
89
+
90
+ # Performs actual formatting
91
+ def format( model, params )
92
+ formatter_for_model( model ).format( model, params )
93
+ end
94
+
95
+ # Resolves the specified options into the appropiate
96
+ # property handler
97
+ def property( options )
98
+ @props[options[:type]]||@props[:simple]
99
+ end
100
+
101
+ end
102
+
103
+ class SimpleProperty
104
+
105
+ def initialize( parent )
106
+ @parent = parent
107
+ end
108
+
109
+ def tag( options )
110
+ value = options[:tag]||options[:property]
111
+ return value.to_s
112
+ end
113
+
114
+ def object_value( object, options )
115
+ object.send options[:property]
116
+ end
117
+
118
+ def xml_value( object, options )
119
+ return object.get_value( tag( options ), options[:required]||true )
120
+ end
121
+
122
+ def bind( object, options, value )
123
+ object.send options[:property].to_s + "=", value
124
+ end
125
+
126
+ def parse( object, options, xml )
127
+ return if options[:bind] == :response
128
+ bind( object, options, value_from_xml( options, xml ) )
224
129
  end
225
130
 
226
- # Very simple module that adds some binding facilities
227
- #
228
- module EntryBinder
131
+ def value_from_xml( options, xml )
132
+ xml_value( xml, options )
133
+ end
134
+
135
+ def value_from_model( options, object )
136
+ object_value( object, options ).to_s
137
+ end
138
+
139
+ def format( object, options, xml )
140
+ return if options[:bind] == :request
141
+ value = value_from_model( options, object )
142
+ new_element = xml.add_element( tag( options ) )
143
+ new_element.add_text( value.to_s ) if value != nil
144
+ end
145
+
146
+ end
147
+
148
+ class DateProperty < SimpleProperty
149
+
150
+ def date2string( date )
151
+ date = Time.now if !date
152
+ date.strftime( ATOM_DATE_FORMAT )
153
+ end
154
+
155
+ def string2date( date )
156
+ Date.strptime( value, ATOM_DATE_FORMAT )
157
+ end
158
+
159
+ def value_from_model( options, object )
160
+ date2string( object_value( object, options ) )
161
+ end
162
+
163
+ def value_from_xml( options, xml )
164
+ string2date( xml_value( xml, options ) )
165
+ end
166
+
167
+ end
168
+
169
+ class DomainFormatter
170
+ def initialize( parent )
171
+ @parent = parent
172
+ end
173
+ end
174
+
175
+ class FeedFormatter < DomainFormatter
176
+
177
+ def format( objects, params )
178
+ params[:content_type]="application/atom+xml"
179
+ xml = REXML::Document.new
180
+ xml << REXML::XMLDecl.default
181
+ feed = xml.add_element( "feed", NAMESPACES )
182
+ feed.add_element( "id" ).add_text( params[:path] )
183
+ feed.add_element( "title" ).add_text( params[:path] )
184
+ objects.each{ |o| @parent.formatter_for_name(:entry).format_entry( o, feed, params ) } if objects
185
+ return xml
186
+ end
187
+
188
+ end
189
+
190
+ class EntryFormatter < DomainFormatter
191
+
192
+ def format( object, params )
193
+ params[:content_type]="application/atom+xml"
194
+ xml = REXML::Document.new
195
+ xml << REXML::XMLDecl.default
196
+ format_entry( object, xml, params )
197
+ return xml
198
+ end
199
+
200
+ def format_entry( object, xml, params )
201
+ resource = @parent.app.resource_for_model( object )
202
+ raise "no resource found for entry #{object}" if !resource
229
203
 
230
- # Binds a property against the matching
231
- # child element in the specified atom entry xml source.
232
- # The base node to look into is /entry/content
233
- def atom_bind( xml, property, mandatory = true )
234
- location = "/entry/content/#{property}"
235
- node = REXML::XPath.first( xml, location )
236
- if node == nil
237
- raise "no node found for location #{location}" if mandatory == true
238
- return
239
- end
240
- if self.respond_to?( "#{property}=" )
241
- self.method( "#{property}=").call( node.text )
242
- else self[ property.intern ] = node.text end
243
-
244
- end
245
-
204
+ entry = xml.add_element( "entry", NAMESPACES )
205
+ entry.add_element( "title" )
206
+ entry.add_element( "author" )
207
+ entry.add_element( "updated" )
208
+ entry.add_element( "id" )
209
+ entry.add_element( "summary" )
210
+
211
+ resource.links.each{ |link|
212
+ entry.add_element( "link", link )
213
+ }
214
+
215
+ content = entry.add_element( "rubyrest:content" )
216
+ resource.props.each{ |map|
217
+ @parent.property( map ).format( object, map, content )
218
+ }
246
219
  end
220
+ end
221
+
222
+
223
+ class ServiceDocFormatter < DomainFormatter
224
+
225
+ def format( service_doc, params )
226
+ params[:content_type]="application/atomserv+xml"
227
+ xml = REXML::Document.new
228
+ xml << REXML::XMLDecl.default
229
+ service = xml.add_element( "service", NAMESPACES )
230
+ workspace = service.add_element( "workspace" )
231
+ workspace.add_element( "atom:title" )
232
+ service_doc.collections.each{ |col|
233
+ collection = workspace.add_element( "collection", { "href" => col.uri } )
234
+ collection.add_element( "atom:title" ).add_text( col.title )
235
+ collection.add_element( "accept" ).add_text( col.accept ) if col.accept
236
+ }
237
+ return xml
238
+ end
239
+
240
+ end
241
+
242
+ # Superclass provided by the framework, so that
243
+ # it's easy to identify resources to be formatted as
244
+ # service document entries
245
+ class ServiceDocument
246
+
247
+ attr_reader :collections
248
+
249
+ def initialize
250
+ @collections = []
251
+ end
252
+
253
+ def add( uri, title, accept )
254
+ @collections << ServiceCollection.new( uri, title, accept )
255
+ end
256
+
257
+ end
258
+
259
+ class ServiceCollection
260
+
261
+ attr_accessor :uri, :title, :accept
262
+
263
+ def initialize( uri, title, accept = nil )
264
+ @uri = uri
265
+ @title = title
266
+ @accept = accept
267
+ end
268
+
269
+ end
270
+
247
271
 
248
272
  end
249
273
  end
@@ -10,31 +10,25 @@ module RubyRest
10
10
  class Default
11
11
 
12
12
  # Configures the server name and port
13
- def self.with_server name, port
14
- @host = name
13
+ def initialize( host, port )
14
+ @host = host
15
15
  @port = port
16
16
  end
17
17
 
18
- # Returns the hostname of the service to
19
- # connect to
20
- def self.host
21
- @host
22
- end
23
-
24
- # Returns the port number on which to
25
- # connect to
26
- def self.port
27
- @port
18
+ # Encodes the specified path, by replacing spaces
19
+ # for the + character. The server will decode
20
+ def encode_path( path )
21
+ path.gsub( " ", "+" )
28
22
  end
29
23
 
30
24
  # Converts the specified hash of data into a
31
25
  # query string, then performs a GET http request.
32
26
  # The response body is returned as an an Atom document if
33
27
  # the response status code is 200.
34
- def retrieve( path, data, api_key )
28
+ def retrieve( path, data = nil, api_key = nil )
35
29
  path << to_query_string( data ) if data
36
30
  headers = prepare_headers( api_key )
37
- rsp = http.get( path, headers )
31
+ rsp = http.get( encode_path( path ), headers )
38
32
  return to_xml( rsp ) if rsp.code.to_i == 200
39
33
  end
40
34
 
@@ -42,11 +36,11 @@ module RubyRest
42
36
  # simplified Atom Entry, then performs a POST http request.
43
37
  # The response body is returned as an an Atom Entry if
44
38
  # the response status code is 201.
45
- def create( path, data, api_key )
39
+ def create( path, data, api_key = nil )
46
40
  body = nil
47
41
  body = hash2entry( data ).to_s if data != nil
48
42
  headers = prepare_headers( api_key )
49
- rsp = http.post( path, body, headers )
43
+ rsp = http.post( encode_path( path ), body, headers )
50
44
  return to_xml( rsp ) if rsp.code.to_i == 201
51
45
  end
52
46
 
@@ -54,21 +48,21 @@ module RubyRest
54
48
  # simplified Atom Entry, then performs a PUT http request.
55
49
  # The response body is returned as an an Atom Entry if
56
50
  # the response status code is 200.
57
- def update( path, data, api_key )
51
+ def update( path, data, api_key = nil)
58
52
  body = nil
59
53
  body = hash2entry( data ).to_s if data != nil
60
54
  headers = prepare_headers( api_key )
61
- rsp = http.put( path, body, headers )
55
+ rsp = http.put( encode_path( path ), body, headers )
62
56
  return to_xml( rsp ) if rsp.code.to_i == 200
63
57
  end
64
58
 
65
59
  # Converts the specified hash of data into a
66
60
  # query string, then performs a DELETE http request.
67
61
  # This method simply returns the response status code
68
- def delete( path, data, api_key )
62
+ def delete( path, data =nil, api_key = nil )
69
63
  path << to_query_string( data ) if data
70
64
  headers = prepare_headers( api_key )
71
- rsp = http.delete( path, headers )
65
+ rsp = http.delete( encode_path( path ), headers )
72
66
  rsp.code.to_i
73
67
  end
74
68
 
@@ -81,7 +75,7 @@ module RubyRest
81
75
  # Convenience method that returns a new HTTP object
82
76
  # for each call
83
77
  def http
84
- Net::HTTP.new( self.class.host, self.class.port )
78
+ Net::HTTP.new( @host, @port )
85
79
  end
86
80
 
87
81
  # Builds a headers hash, with the specified
@@ -95,13 +89,9 @@ module RubyRest
95
89
  # Converts the specified hash of data into a simplified
96
90
  # Atom Entry document.
97
91
  def hash2entry( data )
98
- doc = RubyRest::Atom::Entry::Document.new
99
- entry = REXML::Element.new( "entry", doc )
100
- content = REXML::Element.new( "content", entry )
101
- data.each { |name,value|
102
- body = REXML::Element.new( name.to_s, content )
103
- body.text = value
104
- }
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 ) }
105
95
  return doc
106
96
  end
107
97
 
@@ -109,7 +99,7 @@ module RubyRest
109
99
  # which can be a Feed or Entry, or Service Document
110
100
  def to_xml( rsp )
111
101
  begin
112
- RubyRest::Atom::Entry::Document.new( rsp.body ) if rsp.body
102
+ RubyRest::Atom::Document.new( rsp.body ) if rsp.body
113
103
  rescue => e
114
104
  puts "unable to parse response body: " + e.message
115
105
  puts "---- response body ----"