giraffesoft-classy_resources 0.0.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.
- data/README.rdoc +30 -0
- data/VERSION.yml +4 -0
- data/lib/classy_resources/active_record.rb +26 -0
- data/lib/classy_resources/mime_type.rb +216 -0
- data/lib/classy_resources/sequel.rb +25 -0
- data/lib/classy_resources.rb +103 -0
- data/test/active_record_test.rb +80 -0
- data/test/fixtures/active_record_test_app.rb +25 -0
- data/test/fixtures/sequel_test_app.rb +19 -0
- data/test/sequel_test.rb +76 -0
- data/test/test_helper.rb +19 -0
- metadata +65 -0
data/README.rdoc
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
= Classy Resources
|
|
2
|
+
|
|
3
|
+
Think resource_controller, except for Sinatra.
|
|
4
|
+
|
|
5
|
+
= Usage
|
|
6
|
+
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
require 'sinatra'
|
|
9
|
+
require 'classy_resources'
|
|
10
|
+
require 'classy_resources/active_record'
|
|
11
|
+
# ... or require 'classy_resources/sequel'
|
|
12
|
+
# more ORMs coming (it's also easy to implement your own)...
|
|
13
|
+
|
|
14
|
+
define_resource :posts, :member => [:get, :put, :delete],
|
|
15
|
+
:collection => [:get, :post],
|
|
16
|
+
:formats => [:xml, :json, :yaml]
|
|
17
|
+
|
|
18
|
+
The above declaration will create the five actions specified, each responding to all of the formats listed.
|
|
19
|
+
|
|
20
|
+
- GET /resources.format # => index
|
|
21
|
+
- POST /resources.format # => create
|
|
22
|
+
- GET /resources/1.format # => show
|
|
23
|
+
- PUT /resources/1.format # => update
|
|
24
|
+
- DELETE /resources/1.format # => destroy
|
|
25
|
+
|
|
26
|
+
Since ClassyResources was designed to be active resource compatible, the params formats and return values are what AR expects.
|
|
27
|
+
|
|
28
|
+
== Copyright
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2008 James Golick, Daniel Haran, GiraffeSoft Inc.. See LICENSE for details.
|
data/VERSION.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ClassyResources
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
def load_collection(resource)
|
|
4
|
+
class_for(resource).all
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create_object(resource, params)
|
|
8
|
+
class_for(resource).create!(params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find_object(resource, id)
|
|
12
|
+
class_for(resource).find(id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update_object(object, params)
|
|
16
|
+
object.update_attributes(params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy_object(object)
|
|
20
|
+
object.destroy
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
include ClassyResources::ActiveRecord
|
|
26
|
+
|
|
@@ -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,25 @@
|
|
|
1
|
+
module ClassyResources
|
|
2
|
+
module Sequel
|
|
3
|
+
def load_collection(resources)
|
|
4
|
+
class_for(resources).all
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def create_object(resource, params)
|
|
8
|
+
class_for(resource).create(params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find_object(resource, id)
|
|
12
|
+
class_for(resource).find(:id => id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update_object(object, params)
|
|
16
|
+
object.update(params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy_object(object)
|
|
20
|
+
object.destroy
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
include ClassyResources::Sequel
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
|
2
|
+
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
|
|
3
|
+
require 'classy_resources/mime_type'
|
|
4
|
+
|
|
5
|
+
module ClassyResources
|
|
6
|
+
def class_for(resource)
|
|
7
|
+
resource.to_s.singularize.classify.constantize
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def define_resource(*options)
|
|
11
|
+
ResourceBuilder.new(self, *options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def collection_url_for(resource, format)
|
|
15
|
+
"/#{resource}.#{format}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def object_route_url(resource, format)
|
|
19
|
+
"/#{resource}/:id.#{format}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def object_url_for(resource, format, object)
|
|
23
|
+
"/#{resource}/#{object.id}.#{format}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def set_content_type(format)
|
|
27
|
+
content_type Mime.const_get(format.to_s.upcase).to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def serialize(object, format)
|
|
31
|
+
object.send(:"to_#{format}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ResourceBuilder
|
|
35
|
+
attr_reader :resources, :options, :main, :formats
|
|
36
|
+
|
|
37
|
+
def initialize(main, *args)
|
|
38
|
+
@main = main
|
|
39
|
+
@options = args.pop if args.last.is_a?(Hash)
|
|
40
|
+
@resources = args
|
|
41
|
+
@formats = options[:formats] || :xml
|
|
42
|
+
|
|
43
|
+
build!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build!
|
|
47
|
+
resources.each do |r|
|
|
48
|
+
[*formats].each do |f|
|
|
49
|
+
[:member, :collection].each do |t|
|
|
50
|
+
[*options[t]].each do |v|
|
|
51
|
+
send(:"define_#{t}_#{v}", r, f) unless v.nil?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
protected
|
|
59
|
+
def define_collection_get(resource, format)
|
|
60
|
+
get collection_url_for(resource, format) do
|
|
61
|
+
set_content_type(format)
|
|
62
|
+
serialize(load_collection(resource), format)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def define_collection_post(resource, format)
|
|
67
|
+
post collection_url_for(resource, format) do
|
|
68
|
+
set_content_type(format)
|
|
69
|
+
object = create_object(resource, params[resource.to_s.singularize])
|
|
70
|
+
redirect object_url_for(resource, format, object)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def define_member_get(resource, format)
|
|
75
|
+
get object_route_url(resource, format) do
|
|
76
|
+
set_content_type(format)
|
|
77
|
+
object = find_object(resource, params[:id])
|
|
78
|
+
serialize(object, format)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def define_member_put(resource, format)
|
|
83
|
+
put object_route_url(resource, format) do
|
|
84
|
+
set_content_type(format)
|
|
85
|
+
object = find_object(resource, params[:id])
|
|
86
|
+
update_object(object, params[resource.to_s.singularize])
|
|
87
|
+
serialize(object, format)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def define_member_delete(resource, format)
|
|
92
|
+
delete object_route_url(resource, format) do
|
|
93
|
+
set_content_type(format)
|
|
94
|
+
object = find_object(resource, params[:id])
|
|
95
|
+
destroy_object(object)
|
|
96
|
+
""
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
include ClassyResources
|
|
103
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
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 302, @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
|
+
end
|
|
40
|
+
|
|
41
|
+
context "on GET to /posts/id" do
|
|
42
|
+
setup do
|
|
43
|
+
@post = create_post
|
|
44
|
+
get "/posts/#{@post.id}.xml"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
expect { assert_equal 200, @response.status }
|
|
48
|
+
expect { assert_equal @post.to_xml, @response.body }
|
|
49
|
+
expect { assert_equal "application/xml", @response.content_type }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
context "on PUT to /posts/id" do
|
|
53
|
+
setup do
|
|
54
|
+
@post = create_post
|
|
55
|
+
put "/posts/#{@post.id}.xml", :post => {:title => "Changed!"}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
expect { assert_equal 200, @response.status }
|
|
59
|
+
expect { assert_equal @post.reload.to_xml, @response.body }
|
|
60
|
+
expect { assert_equal "application/xml", @response.content_type }
|
|
61
|
+
|
|
62
|
+
should "update the post" do
|
|
63
|
+
assert_equal "Changed!", @post.reload.title
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context "on DELETE to /posts/id" do
|
|
68
|
+
setup do
|
|
69
|
+
@post = create_post
|
|
70
|
+
delete "/posts/#{@post.id}.xml"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
expect { assert_equal 200, @response.status }
|
|
74
|
+
expect { assert_equal "application/xml", @response.content_type }
|
|
75
|
+
|
|
76
|
+
should "destroy the post" do
|
|
77
|
+
assert_nil Post.find_by_id(@post)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'sinatra'
|
|
3
|
+
require 'classy_resources/active_record'
|
|
4
|
+
require 'activerecord'
|
|
5
|
+
|
|
6
|
+
ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3',
|
|
7
|
+
:database => ':memory:'}}
|
|
8
|
+
ActiveRecord::Base.establish_connection('sqlite3')
|
|
9
|
+
|
|
10
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
|
11
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Schema.define(:version => 0) do
|
|
14
|
+
create_table :posts do |t|
|
|
15
|
+
t.string :title
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Post < ActiveRecord::Base
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
define_resource :posts, :collection => [:get, :post],
|
|
24
|
+
:member => [:get, :put, :delete],
|
|
25
|
+
:formats => [:xml, :json]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'sinatra'
|
|
3
|
+
require 'classy_resources/sequel'
|
|
4
|
+
require '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
|
+
end
|
|
14
|
+
|
|
15
|
+
class User < Sequel::Model(:users)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
define_resource :users, :collection => [:get, :post],
|
|
19
|
+
:member => [:put, :delete, :get]
|
data/test/sequel_test.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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 302, @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 PUT to /users/id" do
|
|
49
|
+
setup do
|
|
50
|
+
@user = create_user
|
|
51
|
+
put "/users/#{@user.id}.xml", :user => {:name => "Changed!"}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
expect { assert_equal 200, @response.status }
|
|
55
|
+
expect { assert_equal @user.reload.to_xml, @response.body }
|
|
56
|
+
expect { assert_equal "application/xml", @response.content_type }
|
|
57
|
+
|
|
58
|
+
should "update the user" do
|
|
59
|
+
assert_equal "Changed!", @user.reload.name
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context "on DELETE to /users/id" do
|
|
64
|
+
setup do
|
|
65
|
+
@user = create_user
|
|
66
|
+
delete "/users/#{@user.id}.xml"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
expect { assert_equal 200, @response.status }
|
|
70
|
+
expect { assert_equal "application/xml", @response.content_type }
|
|
71
|
+
|
|
72
|
+
should "destroy the user" do
|
|
73
|
+
assert_nil User.find(:id => @user.id)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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!({}.merge(opts))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_user(opts = {})
|
|
15
|
+
u = User.new({}.merge(opts))
|
|
16
|
+
u.save
|
|
17
|
+
u
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: giraffesoft-classy_resources
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- James Golick
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-01-27 00:00:00 -08:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: TODO
|
|
17
|
+
email: james@giraffesoft.ca
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
|
|
24
|
+
files:
|
|
25
|
+
- README.rdoc
|
|
26
|
+
- VERSION.yml
|
|
27
|
+
- lib/classy_resources
|
|
28
|
+
- lib/classy_resources/active_record.rb
|
|
29
|
+
- lib/classy_resources/mime_type.rb
|
|
30
|
+
- lib/classy_resources/sequel.rb
|
|
31
|
+
- lib/classy_resources.rb
|
|
32
|
+
- test/active_record_test.rb
|
|
33
|
+
- test/fixtures
|
|
34
|
+
- test/fixtures/active_record_test_app.rb
|
|
35
|
+
- test/fixtures/sequel_test_app.rb
|
|
36
|
+
- test/sequel_test.rb
|
|
37
|
+
- test/test_helper.rb
|
|
38
|
+
has_rdoc: false
|
|
39
|
+
homepage: http://github.com/giraffesoft/classy_resources
|
|
40
|
+
post_install_message:
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: "0"
|
|
50
|
+
version:
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: "0"
|
|
56
|
+
version:
|
|
57
|
+
requirements: []
|
|
58
|
+
|
|
59
|
+
rubyforge_project:
|
|
60
|
+
rubygems_version: 1.2.0
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 2
|
|
63
|
+
summary: TODO
|
|
64
|
+
test_files: []
|
|
65
|
+
|