rubyrest 0.0.5 → 0.1.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/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 ----"