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 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
+