giraffesoft-classy_resources 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 0
@@ -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]
@@ -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
@@ -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
+