classy_resources 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ = Classy Resources
2
+
3
+ With a simple, declarative syntax, you can create active_resource compatible REST APIs incredibly quickly.
4
+
5
+ Think resource_controller, except for Sinatra.
6
+
7
+ = Installation
8
+
9
+ sudo gem install giraffesoft-classy_resources
10
+
11
+ = Usage
12
+
13
+ require 'rubygems'
14
+ require 'sinatra'
15
+ require 'classy_resources'
16
+ require 'classy_resources/active_record'
17
+ # ... or require 'classy_resources/sequel'
18
+ # more ORMs coming (it's also easy to implement your own)...
19
+
20
+ define_resource :posts, :member => [:get, :put, :delete],
21
+ :collection => [:get, :post],
22
+ :formats => [:xml, :json, :yaml]
23
+
24
+ The above declaration will create the five actions specified, each responding to all of the formats listed.
25
+
26
+ - GET /resources.format # => index
27
+ - POST /resources.format # => create
28
+ - GET /resources/1.format # => show
29
+ - PUT /resources/1.format # => update
30
+ - DELETE /resources/1.format # => destroy
31
+
32
+ Since ClassyResources was designed to be active resource compatible, the params formats and return values are what AR expects.
33
+
34
+ = Overrides
35
+
36
+ In the above example, :posts would map to a Post class. If your class is named differently, you just override class_for. For example, if your Post class was stored in a module:
37
+
38
+ def class_for(resource)
39
+ MyModule.const_get(resource.to_s.singularize.classify.constantize)
40
+ end
41
+
42
+ Or, if you wanted to change how objects were being serialized:
43
+
44
+ def serialize(object, format)
45
+ MySerializer.new(object, format).to_s
46
+ end
47
+
48
+ Other method signatures you might want to override:
49
+
50
+ - def load_collection(resource)
51
+ - def build_object(resource, params)
52
+ - def load_object(resource, id)
53
+ - def update_object(object, params) # Note that this doesn't save. It just changes the attributes.
54
+ - def destroy_object(object)
55
+
56
+ == Copyright
57
+
58
+ Copyright (c) 2008 James Golick, Daniel Haran, GiraffeSoft Inc.. See LICENSE for details.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 3
@@ -0,0 +1,122 @@
1
+ dir = File.expand_path(File.dirname(__FILE__))
2
+ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
3
+ require 'active_support'
4
+ require 'classy_resources/mime_type'
5
+ require 'classy_resources/post_body_params'
6
+
7
+ module ClassyResources
8
+ def class_for(resource)
9
+ resource.to_s.singularize.classify.constantize
10
+ end
11
+
12
+ def define_resource(*options)
13
+ ResourceBuilder.new(self, *options)
14
+ end
15
+
16
+ def collection_url_for(resource, format)
17
+ "/#{resource}.#{format}"
18
+ end
19
+
20
+ def object_route_url(resource, format)
21
+ "/#{resource}/:id.#{format}"
22
+ end
23
+
24
+ def object_url_for(resource, format, object)
25
+ "/#{resource}/#{object.id}.#{format}"
26
+ end
27
+
28
+ def set_content_type(format)
29
+ content_type Mime.const_get(format.to_s.upcase).to_s
30
+ end
31
+
32
+ def serialize(object, format)
33
+ object.send(:"to_#{format}")
34
+ end
35
+
36
+ class ResourceBuilder
37
+ attr_reader :resources, :options, :main, :formats
38
+
39
+ def initialize(main, *args)
40
+ @main = main
41
+ @options = args.pop if args.last.is_a?(Hash)
42
+ @resources = args
43
+ @formats = options[:formats] || :xml
44
+
45
+ build!
46
+ end
47
+
48
+ def build!
49
+ resources.each do |r|
50
+ [*formats].each do |f|
51
+ [:member, :collection].each do |t|
52
+ [*options[t]].each do |v|
53
+ send(:"define_#{t}_#{v}", r, f) unless v.nil?
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ protected
61
+ def define_collection_get(resource, format)
62
+ get collection_url_for(resource, format) do
63
+ set_content_type(format)
64
+ serialize(load_collection(resource), format)
65
+ end
66
+ end
67
+
68
+ def define_collection_post(resource, format)
69
+ post collection_url_for(resource, format) do
70
+ set_content_type(format)
71
+ object = build_object(resource, params[resource.to_s.singularize] || {})
72
+
73
+ if object.valid?
74
+ object.save
75
+
76
+ response['location'] = object_url_for(resource, format, object)
77
+ response.status = 201
78
+ serialize(object, format)
79
+ else
80
+ response.status = 422
81
+ serialize(object.errors, format)
82
+ end
83
+ end
84
+ end
85
+
86
+ def define_member_get(resource, format)
87
+ get object_route_url(resource, format) do
88
+ set_content_type(format)
89
+ object = load_object(resource, params[:id])
90
+ serialize(object, format)
91
+ end
92
+ end
93
+
94
+ def define_member_put(resource, format)
95
+ put object_route_url(resource, format) do
96
+ set_content_type(format)
97
+ object = load_object(resource, params[:id])
98
+ update_object(object, params[resource.to_s.singularize])
99
+
100
+ if object.valid?
101
+ object.save
102
+ serialize(object, format)
103
+ else
104
+ response.status = 422
105
+ serialize(object.errors, format)
106
+ end
107
+ end
108
+ end
109
+
110
+ def define_member_delete(resource, format)
111
+ delete object_route_url(resource, format) do
112
+ set_content_type(format)
113
+ object = load_object(resource, params[:id])
114
+ destroy_object(object)
115
+ ""
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ include ClassyResources
122
+
@@ -0,0 +1,30 @@
1
+ module ClassyResources
2
+ module ActiveRecord
3
+ def load_collection(resource)
4
+ class_for(resource).all
5
+ end
6
+
7
+ def build_object(resource, object_params)
8
+ class_for(resource).new(object_params)
9
+ end
10
+
11
+ def load_object(resource, id)
12
+ class_for(resource).find(id)
13
+ end
14
+
15
+ def update_object(object, params)
16
+ object.attributes = params
17
+ end
18
+
19
+ def destroy_object(object)
20
+ object.destroy
21
+ end
22
+
23
+ error ::ActiveRecord::RecordNotFound do
24
+ response.status = 404
25
+ end
26
+ end
27
+ end
28
+
29
+ include ClassyResources::ActiveRecord
30
+
@@ -0,0 +1,216 @@
1
+ # borrowed from activesupport
2
+ require 'set'
3
+
4
+ module Mime
5
+ SET = []
6
+ EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
7
+ LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
8
+
9
+ # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
10
+ #
11
+ # class PostsController < ActionController::Base
12
+ # def show
13
+ # @post = Post.find(params[:id])
14
+ #
15
+ # respond_to do |format|
16
+ # format.html
17
+ # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
18
+ # format.xml { render :xml => @people.to_xml }
19
+ # end
20
+ # end
21
+ # end
22
+ class Type
23
+ def self.html_types; @@html_types; end
24
+ def self.browser_generated_types; @@browser_generated_types; end
25
+
26
+ @@html_types = Set.new [:html, :all]
27
+
28
+ @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text]
29
+
30
+ # A simple helper class used in parsing the accept header
31
+ class AcceptItem #:nodoc:
32
+ attr_accessor :order, :name, :q
33
+
34
+ def initialize(order, name, q=nil)
35
+ @order = order
36
+ @name = name.strip
37
+ q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list
38
+ @q = ((q || 1.0).to_f * 100).to_i
39
+ end
40
+
41
+ def to_s
42
+ @name
43
+ end
44
+
45
+ def <=>(item)
46
+ result = item.q <=> q
47
+ result = order <=> item.order if result == 0
48
+ result
49
+ end
50
+
51
+ def ==(item)
52
+ name == (item.respond_to?(:name) ? item.name : item)
53
+ end
54
+ end
55
+
56
+ class << self
57
+ def lookup(string)
58
+ LOOKUP[string]
59
+ end
60
+
61
+ def lookup_by_extension(extension)
62
+ EXTENSION_LOOKUP[extension]
63
+ end
64
+
65
+ # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
66
+ # rendering different HTML versions depending on the user agent, like an iPhone.
67
+ def register_alias(string, symbol, extension_synonyms = [])
68
+ register(string, symbol, [], extension_synonyms, true)
69
+ end
70
+
71
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
72
+ Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) }
73
+
74
+ SET << Mime.const_get(symbol.to_s.upcase)
75
+
76
+ ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup
77
+ ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
78
+ end
79
+
80
+ def parse(accept_header)
81
+ if accept_header !~ /,/
82
+ [Mime::Type.lookup(accept_header)]
83
+ else
84
+ # keep track of creation order to keep the subsequent sort stable
85
+ list = []
86
+ accept_header.split(/,/).each_with_index do |header, index|
87
+ params, q = header.split(/;\s*q=/)
88
+ if params
89
+ params.strip!
90
+ list << AcceptItem.new(index, params, q) unless params.empty?
91
+ end
92
+ end
93
+ list.sort!
94
+
95
+ # Take care of the broken text/xml entry by renaming or deleting it
96
+ text_xml = list.index("text/xml")
97
+ app_xml = list.index(Mime::XML.to_s)
98
+
99
+ if text_xml && app_xml
100
+ # set the q value to the max of the two
101
+ list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
102
+
103
+ # make sure app_xml is ahead of text_xml in the list
104
+ if app_xml > text_xml
105
+ list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
106
+ app_xml, text_xml = text_xml, app_xml
107
+ end
108
+
109
+ # delete text_xml from the list
110
+ list.delete_at(text_xml)
111
+
112
+ elsif text_xml
113
+ list[text_xml].name = Mime::XML.to_s
114
+ end
115
+
116
+ # Look for more specific XML-based types and sort them ahead of app/xml
117
+
118
+ if app_xml
119
+ idx = app_xml
120
+ app_xml_type = list[app_xml]
121
+
122
+ while(idx < list.length)
123
+ type = list[idx]
124
+ break if type.q < app_xml_type.q
125
+ if type.name =~ /\+xml$/
126
+ list[app_xml], list[idx] = list[idx], list[app_xml]
127
+ app_xml = idx
128
+ end
129
+ idx += 1
130
+ end
131
+ end
132
+
133
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
134
+ list
135
+ end
136
+ end
137
+ end
138
+
139
+ def initialize(string, symbol = nil, synonyms = [])
140
+ @symbol, @synonyms = symbol, synonyms
141
+ @string = string
142
+ end
143
+
144
+ def to_s
145
+ @string
146
+ end
147
+
148
+ def to_str
149
+ to_s
150
+ end
151
+
152
+ def to_sym
153
+ @symbol || @string.to_sym
154
+ end
155
+
156
+ def ===(list)
157
+ if list.is_a?(Array)
158
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
159
+ else
160
+ super
161
+ end
162
+ end
163
+
164
+ def ==(mime_type)
165
+ return false if mime_type.blank?
166
+ (@synonyms + [ self ]).any? do |synonym|
167
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
168
+ end
169
+ end
170
+
171
+ # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
172
+ # ActionController::RequestForgeryProtection.
173
+ def verify_request?
174
+ browser_generated?
175
+ end
176
+
177
+ def html?
178
+ @@html_types.include?(to_sym) || @string =~ /html/
179
+ end
180
+
181
+ def browser_generated?
182
+ @@browser_generated_types.include?(to_sym)
183
+ end
184
+
185
+ private
186
+ def method_missing(method, *args)
187
+ if method.to_s =~ /(\w+)\?$/
188
+ $1.downcase.to_sym == to_sym
189
+ else
190
+ super
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # Build list of Mime types for HTTP responses
197
+ # http://www.iana.org/assignments/media-types/
198
+
199
+ Mime::Type.register "*/*", :all
200
+ Mime::Type.register "text/plain", :text, [], %w(txt)
201
+ Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
202
+ Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
203
+ Mime::Type.register "text/css", :css
204
+ Mime::Type.register "text/calendar", :ics
205
+ Mime::Type.register "text/csv", :csv
206
+ Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
207
+ Mime::Type.register "application/rss+xml", :rss
208
+ Mime::Type.register "application/atom+xml", :atom
209
+ Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
210
+
211
+ Mime::Type.register "multipart/form-data", :multipart_form
212
+ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
213
+
214
+ # http://www.ietf.org/rfc/rfc4627.txt
215
+ # http://www.json.org/JSONRequest.html
216
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ module ClassyResources
5
+
6
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
7
+ # not one of the standard supported types, like <tt>application/json</tt>.
8
+ #
9
+ # TODO: Find a better name.
10
+ #
11
+ class PostBodyParams
12
+
13
+ # Constants
14
+ #
15
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
16
+ POST_BODY = 'rack.input'.freeze
17
+ FORM_INPUT = 'rack.request.form_input'.freeze
18
+ FORM_HASH = 'rack.request.form_hash'.freeze
19
+
20
+ # Supported Content-Types
21
+ #
22
+ APPLICATION_JSON = 'application/json'.freeze
23
+
24
+ def initialize(app)
25
+ @app = app
26
+ end
27
+
28
+ def call(env)
29
+ case env[CONTENT_TYPE]
30
+ when APPLICATION_JSON
31
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
32
+ end
33
+ @app.call(env)
34
+ end
35
+
36
+ end
37
+ end
38
+
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ module ClassyResources
5
+
6
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
7
+ # not one of the standard supported types, like <tt>application/json</tt>.
8
+ #
9
+ # TODO: Find a better name.
10
+ #
11
+ class PostBodyParams
12
+
13
+ # Constants
14
+ #
15
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
16
+ POST_BODY = 'rack.input'.freeze
17
+ FORM_INPUT = 'rack.request.form_input'.freeze
18
+ FORM_HASH = 'rack.request.form_hash'.freeze
19
+
20
+ # Supported Content-Types
21
+ #
22
+ APPLICATION_JSON = 'application/json'.freeze
23
+ APPLICATION_XML = 'application/xml'.freeze
24
+
25
+ def initialize(app)
26
+ @app = app
27
+ end
28
+
29
+ def call(env)
30
+ case env[CONTENT_TYPE]
31
+ when APPLICATION_JSON
32
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
33
+ when APPLICATION_XML
34
+ env.update(FORM_HASH => Hash.from_xml(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
35
+ end
36
+ @app.call(env)
37
+ end
38
+
39
+ end
40
+ end
41
+
@@ -0,0 +1,35 @@
1
+ require 'classy_resources/sequel_errors_to_xml'
2
+
3
+ module ClassyResources
4
+ module Sequel
5
+ class ResourceNotFound < RuntimeError; end
6
+
7
+ def load_collection(resource)
8
+ class_for(resource).all
9
+ end
10
+
11
+ def build_object(resource, object_params)
12
+ class_for(resource).new(object_params)
13
+ end
14
+
15
+ def load_object(resource, id)
16
+ r = class_for(resource).find(:id => id)
17
+ raise ResourceNotFound if r.nil?
18
+ r
19
+ end
20
+
21
+ def update_object(object, params)
22
+ object.set params
23
+ end
24
+
25
+ def destroy_object(object)
26
+ object.destroy
27
+ end
28
+
29
+ error ResourceNotFound do
30
+ response.status = 404
31
+ end
32
+ end
33
+ end
34
+
35
+ include ClassyResources::Sequel
@@ -0,0 +1,19 @@
1
+ # stolen from active record
2
+ #
3
+ module ClassyResources
4
+ module SequelErrorsToXml
5
+ def to_xml(options={})
6
+ options[:root] ||= "errors"
7
+ options[:indent] ||= 2
8
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
9
+
10
+ options[:builder].instruct! unless options.delete(:skip_instruct)
11
+ options[:builder].errors do |e|
12
+ full_messages.each { |msg| e.error(msg) }
13
+ end
14
+ end
15
+ end
16
+
17
+ ::Sequel::Model::Validation::Errors.send(:include, SequelErrorsToXml)
18
+ end
19
+
@@ -0,0 +1,166 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'sinatra'
3
+ require 'sinatra/test/unit'
4
+ require File.dirname(__FILE__) + '/fixtures/active_record_test_app'
5
+
6
+ class ActiveRecordTest < Test::Unit::TestCase
7
+ context "on GET to /posts with xml" do
8
+ setup do
9
+ 2.times { create_post }
10
+ get '/posts.xml'
11
+ end
12
+
13
+ expect { assert_equal 200, @response.status }
14
+ expect { assert_equal Post.all.to_xml, @response.body }
15
+ expect { assert_equal "application/xml", @response.content_type }
16
+ end
17
+
18
+ context "on GET to /posts with json" do
19
+ setup do
20
+ 2.times { create_post }
21
+ get '/posts.json'
22
+ end
23
+
24
+ expect { assert_equal 200, @response.status }
25
+ expect { assert_equal Post.all.to_json, @response.body }
26
+ expect { assert_equal "application/json", @response.content_type }
27
+ end
28
+
29
+ context "on POST to /posts" do
30
+ setup do
31
+ Post.destroy_all
32
+ post '/posts.xml', :post => {:title => "whatever"}
33
+ end
34
+
35
+ expect { assert_equal 201, @response.status }
36
+ expect { assert_equal "/posts/#{Post.first.id}.xml", @response.location }
37
+ expect { assert_equal "whatever", Post.first.title }
38
+ expect { assert_equal "application/xml", @response.content_type }
39
+ expect { assert_equal Post.first.to_xml, @response.body }
40
+ end
41
+
42
+ context "on POST to /posts with invalid params" do
43
+ setup do
44
+ Post.destroy_all
45
+ post '/posts.xml', :post => {}
46
+ end
47
+
48
+ expect { assert_equal 422, @response.status }
49
+ expect { assert_equal "application/xml", @response.content_type }
50
+ expect { assert_equal Post.create.errors.to_xml, @response.body }
51
+ expect { assert_equal 0, Post.count }
52
+ end
53
+
54
+ context "on GET to /posts/id" do
55
+ setup do
56
+ @post = create_post
57
+ get "/posts/#{@post.id}.xml"
58
+ end
59
+
60
+ expect { assert_equal 200, @response.status }
61
+ expect { assert_equal @post.to_xml, @response.body }
62
+ expect { assert_equal "application/xml", @response.content_type }
63
+ end
64
+
65
+ context "on GET to /posts/id with a missing post" do
66
+ setup do
67
+ get "/posts/doesntexist.xml"
68
+ end
69
+
70
+ expect { assert_equal 404, @response.status }
71
+ expect { assert @response.body.empty? }
72
+ expect { assert_equal "application/xml", @response.content_type }
73
+ end
74
+
75
+ context "on PUT to /posts/id" do
76
+ setup do
77
+ @post = create_post
78
+ put "/posts/#{@post.id}.xml", :post => {:title => "Changed!"}
79
+ end
80
+
81
+ expect { assert_equal 200, @response.status }
82
+ expect { assert_equal @post.reload.to_xml, @response.body }
83
+ expect { assert_equal "application/xml", @response.content_type }
84
+
85
+ should "update the post" do
86
+ assert_equal "Changed!", @post.reload.title
87
+ end
88
+ end
89
+
90
+ context "on PUT to /posts/id with invalid params" do
91
+ setup do
92
+ @post = create_post
93
+ put "/posts/#{@post.id}.xml", :post => {:title => ""}
94
+ end
95
+
96
+ expect { assert_equal 422, @response.status }
97
+ expect { assert_equal "application/xml", @response.content_type }
98
+ expect { assert_equal Post.create.errors.to_xml, @response.body }
99
+
100
+ should "not update the post" do
101
+ assert_not_equal "", @post.reload.title
102
+ end
103
+ end
104
+
105
+ context "on PUT to /posts/id with a missing post" do
106
+ setup do
107
+ put "/posts/missing.xml", :post => {:title => "Changed!"}
108
+ end
109
+
110
+ expect { assert_equal 404, @response.status }
111
+ expect { assert @response.body.empty? }
112
+ expect { assert_equal "application/xml", @response.content_type }
113
+ end
114
+
115
+ context "on DELETE to /posts/id" do
116
+ setup do
117
+ @post = create_post
118
+ delete "/posts/#{@post.id}.xml"
119
+ end
120
+
121
+ expect { assert_equal 200, @response.status }
122
+ expect { assert_equal "application/xml", @response.content_type }
123
+
124
+ should "destroy the post" do
125
+ assert_nil Post.find_by_id(@post)
126
+ end
127
+ end
128
+
129
+ context "on DELETE to /posts/id with a missing post" do
130
+ setup do
131
+ delete "/posts/missing.xml"
132
+ end
133
+
134
+ expect { assert_equal 404, @response.status }
135
+ expect { assert_equal "application/xml", @response.content_type }
136
+ expect { assert @response.body.empty? }
137
+ end
138
+
139
+ context "on POST to /comments with a JSON post body" do
140
+ setup do
141
+ Comment.destroy_all
142
+ post "/comments.xml", {:comment => hash_for_comment(:author => 'james')}.to_json,
143
+ :content_type => 'application/json'
144
+ end
145
+
146
+ expect { assert_equal 201, @response.status }
147
+ expect { assert_equal "application/xml", @response.content_type }
148
+ expect { assert_equal "/comments/#{Comment.first.id}.xml", @response.location }
149
+ expect { assert_equal 1, Comment.count }
150
+ expect { assert_equal 'james', Comment.first.author }
151
+ end
152
+
153
+ context "on POST to /posts/id/comments with a XML post body" do
154
+ setup do
155
+ Comment.destroy_all
156
+ post "/comments.xml", Comment.new(:author => 'james').to_xml,
157
+ :content_type => 'application/xml'
158
+ end
159
+
160
+ expect { assert_equal 201, @response.status }
161
+ expect { assert_equal "application/xml", @response.content_type }
162
+ expect { assert_equal "/comments/#{Comment.first.id}.xml", @response.location }
163
+ expect { assert_equal 1, Comment.count }
164
+ expect { assert_equal 'james', Comment.first.author }
165
+ end
166
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ gem 'activerecord', '2.2.2'
3
+ require 'activerecord'
4
+ require 'sinatra'
5
+ require 'classy_resources/active_record'
6
+
7
+ ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3',
8
+ :database => ':memory:'}}
9
+ ActiveRecord::Base.establish_connection('sqlite3')
10
+
11
+ ActiveRecord::Base.logger = Logger.new(STDERR)
12
+ ActiveRecord::Base.logger.level = Logger::WARN
13
+
14
+ ActiveRecord::Schema.define(:version => 0) do
15
+ create_table :posts do |t|
16
+ t.string :title
17
+ end
18
+
19
+ create_table :comments do |t|
20
+ t.integer :post_id
21
+ t.string :author
22
+ end
23
+ end
24
+
25
+ class Post < ActiveRecord::Base
26
+ has_many :comments
27
+ validates_presence_of :title
28
+ end
29
+
30
+ class Comment < ActiveRecord::Base
31
+ belongs_to :post
32
+ end
33
+
34
+ set :raise_errors, false
35
+
36
+ define_resource :posts, :collection => [:get, :post],
37
+ :member => [:get, :put, :delete],
38
+ :formats => [:xml, :json]
39
+
40
+ define_resource :comments, :collection => [:get, :post]
41
+
42
+ use ClassyResources::PostBodyParams
43
+
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+ require 'sequel'
4
+ require 'classy_resources/sequel'
5
+
6
+ Sequel::Model.db = Sequel.sqlite
7
+
8
+ Sequel::Model.db.instance_eval do
9
+ create_table! :users do
10
+ primary_key :id
11
+ varchar :name
12
+ end
13
+
14
+ create_table! :subscriptions do
15
+ primary_key :id
16
+ int :user_id
17
+ varchar :name
18
+ end
19
+ end
20
+
21
+ class User < Sequel::Model(:users)
22
+ one_to_many :subscriptions
23
+ validates_presence_of :name
24
+ end
25
+
26
+ class Subscription < Sequel::Model(:subscriptions)
27
+ many_to_one :users
28
+ validates_presence_of :user_id
29
+ end
30
+
31
+ set :raise_errors, false
32
+
33
+ define_resource :users, :collection => [:get, :post],
34
+ :member => [:put, :delete, :get]
35
+
36
+ define_resource :subscriptions, :collection => [:get, :post]
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'sinatra'
3
+ require 'sinatra/test/unit'
4
+ require File.dirname(__FILE__) + '/fixtures/sequel_test_app'
5
+ require 'activesupport'
6
+
7
+ class SequelErrorsToXmlTest < Test::Unit::TestCase
8
+ context "serializing a sequel errors object" do
9
+ before do
10
+ @subscription = Subscription.new
11
+ @subscription.valid?
12
+ end
13
+
14
+ should "serialize in a format that is active resource compatible" do
15
+ str =<<-__END__
16
+ <?xml version="1.0" encoding="UTF-8"?>
17
+ <errors>
18
+ <error>user_id is not present</error>
19
+ </errors>
20
+ __END__
21
+ assert_equal str, @subscription.errors.to_xml
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,137 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+ require 'sinatra'
3
+ require 'sinatra/test/unit'
4
+ require File.dirname(__FILE__) + '/fixtures/sequel_test_app'
5
+ require 'activesupport'
6
+
7
+ class Sequel::Model
8
+ def to_xml(opts={})
9
+ values.to_xml(opts)
10
+ end
11
+ end
12
+
13
+ class SequelTest < Test::Unit::TestCase
14
+ context "on GET to /users with xml" do
15
+ setup do
16
+ 2.times { create_user }
17
+ get '/users.xml'
18
+ end
19
+
20
+ expect { assert_equal 200, @response.status }
21
+ expect { assert_equal User.all.to_xml, @response.body }
22
+ expect { assert_equal "application/xml", @response.content_type }
23
+ end
24
+
25
+ context "on POST to /users" do
26
+ setup do
27
+ User.destroy_all
28
+ post '/users.xml', :user => {:name => "whatever"}
29
+ end
30
+
31
+ expect { assert_equal 201, @response.status }
32
+ expect { assert_equal "/users/#{User.first.id}.xml", @response.location }
33
+ expect { assert_equal "whatever", User.first.name }
34
+ expect { assert_equal "application/xml", @response.content_type }
35
+ end
36
+
37
+ context "on GET to /users/id" do
38
+ setup do
39
+ @user = create_user
40
+ get "/users/#{@user.id}.xml"
41
+ end
42
+
43
+ expect { assert_equal 200, @response.status }
44
+ expect { assert_equal @user.to_xml, @response.body }
45
+ expect { assert_equal "application/xml", @response.content_type }
46
+ end
47
+
48
+ context "on GET to /users/id with a missing user" do
49
+ setup do
50
+ get "/users/missing.xml"
51
+ end
52
+
53
+ expect { assert_equal 404, @response.status }
54
+ expect { assert @response.body.empty? }
55
+ expect { assert_equal "application/xml", @response.content_type }
56
+ end
57
+
58
+ context "on PUT to /users/id" do
59
+ setup do
60
+ @user = create_user
61
+ put "/users/#{@user.id}.xml", :user => {:name => "Changed!"}
62
+ end
63
+
64
+ expect { assert_equal 200, @response.status }
65
+ expect { assert_equal @user.reload.to_xml, @response.body }
66
+ expect { assert_equal "application/xml", @response.content_type }
67
+
68
+ should "update the user" do
69
+ assert_equal "Changed!", @user.reload.name
70
+ end
71
+ end
72
+
73
+ context "on PUT to /users/id with invalid params" do
74
+ setup do
75
+ @user = create_user
76
+ put "/users/#{@user.id}.xml", :user => {:name => ""}
77
+ @invalid_user = User.new
78
+ @invalid_user.valid?
79
+ end
80
+
81
+ expect { assert_equal 422, @response.status }
82
+ expect { assert_equal @invalid_user.errors.to_xml, @response.body }
83
+ expect { assert_equal "application/xml", @response.content_type }
84
+
85
+ should "not update the user" do
86
+ assert_not_equal "Changed!", @user.reload.name
87
+ end
88
+ end
89
+
90
+ context "on PUT to /users/id with a missing user" do
91
+ setup do
92
+ put "/users/missing.xml", :user => {:name => "Changed!"}
93
+ end
94
+
95
+ expect { assert_equal 404, @response.status }
96
+ expect { assert @response.body.empty? }
97
+ expect { assert_equal "application/xml", @response.content_type }
98
+ end
99
+
100
+ context "on DELETE to /users/id" do
101
+ setup do
102
+ @user = create_user
103
+ delete "/users/#{@user.id}.xml"
104
+ end
105
+
106
+ expect { assert_equal 200, @response.status }
107
+ expect { assert_equal "application/xml", @response.content_type }
108
+
109
+ should "destroy the user" do
110
+ assert_nil User.find(:id => @user.id)
111
+ end
112
+ end
113
+
114
+ context "on DELETE to /users/id with a missing user" do
115
+ setup do
116
+ delete "/users/missing.xml"
117
+ end
118
+
119
+ expect { assert_equal 404, @response.status }
120
+ expect { assert_equal "application/xml", @response.content_type }
121
+ expect { assert @response.body.empty? }
122
+ end
123
+
124
+ context "on POST to /subscriptions with invalid params" do
125
+ setup do
126
+ Subscription.destroy_all
127
+ @subscription = Subscription.new
128
+ @subscription.valid?
129
+ post "/subscriptions.xml", :subscription => {}
130
+ end
131
+
132
+ expect { assert_equal 422, @response.status }
133
+ expect { assert_equal "application/xml", @response.content_type }
134
+ expect { assert_equal 0, Subscription.count }
135
+ expect { assert_equal @subscription.errors.to_xml, @response.body }
136
+ end
137
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../lib/classy_resources'
2
+ require 'rubygems'
3
+ require 'test/unit'
4
+ require 'context'
5
+ require 'zebra'
6
+ require 'mocha'
7
+
8
+ class Test::Unit::TestCase
9
+ protected
10
+ def create_post(opts = {})
11
+ Post.create!({:title => 'awesome'}.merge(opts))
12
+ end
13
+
14
+ def hash_for_comment(opts = {})
15
+ {}.merge(opts)
16
+ end
17
+
18
+ def create_comment(opts = {})
19
+ Comment.create!(hash_for_comment(opts))
20
+ end
21
+
22
+ def create_user(opts = {})
23
+ u = User.new({:name => 'james'}.merge(opts))
24
+ u.save
25
+ u
26
+ end
27
+
28
+ def hash_for_subscription(opts = {})
29
+ {:name => "emptiness is depressing"}.merge(opts)
30
+ end
31
+
32
+ def create_subscription(opts = {})
33
+ s = Subscription.new(hash_for_subscription(opts))
34
+ s.save
35
+ s
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: classy_resources
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - James Golick
8
+ - Justin Lynn
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-11-16 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - "="
23
+ - !ruby/object:Gem::Version
24
+ version: 2.2.2
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: sinatra
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.9.0.4
35
+ version:
36
+ description: Instant ActiveResource compatible resources for sinatra.
37
+ email: justinlynn@gmail.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - README.rdoc
46
+ - VERSION.yml
47
+ - lib/classy_resources/active_record.rb
48
+ - lib/classy_resources/mime_type.rb
49
+ - lib/classy_resources/post_body_param_parsing.rb
50
+ - lib/classy_resources/post_body_params.rb
51
+ - lib/classy_resources/sequel.rb
52
+ - lib/classy_resources/sequel_errors_to_xml.rb
53
+ - lib/classy_resources.rb
54
+ - test/active_record_test.rb
55
+ - test/fixtures/active_record_test_app.rb
56
+ - test/fixtures/sequel_test_app.rb
57
+ - test/sequel_errors_to_xml_test.rb
58
+ - test/sequel_test.rb
59
+ - test/test_helper.rb
60
+ has_rdoc: true
61
+ homepage: http://github.com/justinlynn/classy_resources
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.5
85
+ signing_key:
86
+ specification_version: 2
87
+ summary: Instant ActiveResource compatible resources. Think resource_controller, for sinatra. Now modified for gemcutter.org awesomeness.
88
+ test_files: []
89
+