classy_resources 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+