toast 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +154 -0
- data/app/controller/toast_controller.rb +47 -0
- data/config/routes.rb +17 -0
- data/lib/toast.rb +1 -0
- data/lib/toast/active_record_extensions.rb +76 -0
- data/lib/toast/associate_collection.rb +58 -0
- data/lib/toast/attribute.rb +38 -0
- data/lib/toast/config_dsl.rb +112 -0
- data/lib/toast/engine.rb +28 -0
- data/lib/toast/record.rb +51 -0
- data/lib/toast/resource.rb +86 -0
- data/lib/toast/root_collection.rb +58 -0
- metadata +72 -0
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
Summary
|
2
|
+
=======
|
3
|
+
|
4
|
+
Toast is an extension to Ruby on Rails that lets you expose any
|
5
|
+
ActiveRecord model as a resource according to the REST paradigm. The
|
6
|
+
representation format is JSON.
|
7
|
+
|
8
|
+
In contrast to other plugins, gems and Rails' inbuilt REST features
|
9
|
+
toast takes a data centric approach: Tell the model to be a resource
|
10
|
+
and what attributes and associations are to be exposed. That's it. No
|
11
|
+
controller boiler plate code for every model, no routing setup.
|
12
|
+
|
13
|
+
Toast is a Rails engine that runs one generic controller and a sets up
|
14
|
+
the routing to it according to the definition in the models, which is
|
15
|
+
denoted using a block oriented DSL.
|
16
|
+
|
17
|
+
REST is more than some pretty URIs, the use of the HTTP verbs and
|
18
|
+
response codes. It's on the toast user to invent media types that
|
19
|
+
control the application's state and introduce semantics. With toast you
|
20
|
+
can build REST services or tightly coupled server-client applications,
|
21
|
+
which ever suits the task best. That's why TOAST stands for:
|
22
|
+
|
23
|
+
> **TOast Ain't reST**
|
24
|
+
|
25
|
+
*Be careful*: This version is experimental and probably not bullet
|
26
|
+
proof. As soon as the gem is installed a controller with ready routing
|
27
|
+
is enabled serving the annotated model's data records for reading,
|
28
|
+
updating and deleting. There are no measures to prevent XSS and CSFR
|
29
|
+
attacks.
|
30
|
+
|
31
|
+
Example
|
32
|
+
=======
|
33
|
+
|
34
|
+
Let the table `bananas` have the following schema:
|
35
|
+
|
36
|
+
create_table "bananas", :force => true do |t|
|
37
|
+
t.string "name"
|
38
|
+
t.integer "number"
|
39
|
+
t.string "color"
|
40
|
+
t.integer "apple_id"
|
41
|
+
end
|
42
|
+
|
43
|
+
and let a corresponding model class have a *resourceful_model* annotation:
|
44
|
+
|
45
|
+
class Banana < ActiveRecord::Base
|
46
|
+
belongs_to :apple
|
47
|
+
has_many :coconuts
|
48
|
+
scope :find_some, where("number < 100")
|
49
|
+
|
50
|
+
resourceful_model do
|
51
|
+
# attributes or association names
|
52
|
+
fields :name, :number, :coconuts, :apple
|
53
|
+
|
54
|
+
# class methods of Banana returning an Array of Banana records
|
55
|
+
collections :find_some, :all
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
The above definition inside the `resourceful_model` block exposes the
|
60
|
+
records of the model Banana automatically via a generic controller to
|
61
|
+
the outside world, accepting and delivering JSON representations of
|
62
|
+
the records. Let the associated models Apple and Coconut be
|
63
|
+
exposed as a resource, too.
|
64
|
+
|
65
|
+
### Get a collection
|
66
|
+
GET /bananas
|
67
|
+
--> 200, '[{"uri":"http://www.example.com/bananas/23",
|
68
|
+
"name": "Fred",
|
69
|
+
"number": 33,
|
70
|
+
"coconuts": "http://www.example.com/bananas/23/coconuts",
|
71
|
+
"apple":"http://www.example.com/bananas/23/apple,
|
72
|
+
{"uri":"http://www.example.com/bananas/24",
|
73
|
+
... }, ... ]
|
74
|
+
### Get a customized collection (filtered, paging, etc.)
|
75
|
+
|
76
|
+
GET /bananas/find_some
|
77
|
+
--> 200, '[SOME BANANAS]'
|
78
|
+
|
79
|
+
### Get a single resource representation:
|
80
|
+
GET /bananas/23
|
81
|
+
--> 200, '{"uri":"http://www.example.com/bananas/23"
|
82
|
+
"name": "Fred",
|
83
|
+
"number": 33,
|
84
|
+
"coconuts": "http://www.example.com/bananas/23/coconuts",
|
85
|
+
"apple": "http://www.example.com/bananas/23/apple" }'
|
86
|
+
|
87
|
+
### Get a associated collection
|
88
|
+
"GET" /bananas/23/coconuts
|
89
|
+
--> 200, '[{COCNUT},{COCONUT},...]',
|
90
|
+
|
91
|
+
### Update a single resource:
|
92
|
+
PUT /bananas/23, '{"uri":"http://www.example.com/bananas/23"
|
93
|
+
"name": "Barney",
|
94
|
+
"number": 44}'
|
95
|
+
--> 200, '{"uri":"http://www.example.com/bananas/23"
|
96
|
+
"name": "Barney",
|
97
|
+
"number": 44,
|
98
|
+
"coconuts": "http://www.example.com/bananas/23/coconuts",
|
99
|
+
"apple": "http://www.example.com/bananas/23/apple"}'
|
100
|
+
|
101
|
+
### Create a new record
|
102
|
+
"POST" /bananas, '{"name": "Johnny",
|
103
|
+
"number": 888}'
|
104
|
+
--> 201, {"uri":"http://www.example.com/bananas/102"
|
105
|
+
"name": "Johnny",
|
106
|
+
"number": 888,
|
107
|
+
"coconuts": "http://www.example.com/bananas/102/coconuts" ,
|
108
|
+
"apple": "http://www.example.com/bananas/102/apple }
|
109
|
+
|
110
|
+
### Create an associated record
|
111
|
+
"POST" /bananas/23/coconuts, '{COCONUT}'
|
112
|
+
--> 201, {"uri":"http://www.example.com/coconuts/432,
|
113
|
+
...}
|
114
|
+
|
115
|
+
### Delete records
|
116
|
+
DELETE /bananas/23
|
117
|
+
--> 200
|
118
|
+
|
119
|
+
More details and configuration options are documented in the manual... (_comming soon_)
|
120
|
+
|
121
|
+
Installation
|
122
|
+
============
|
123
|
+
|
124
|
+
git clone git@github.com:robokopp/toast.git
|
125
|
+
gem install jeweler
|
126
|
+
cd toast
|
127
|
+
rake install
|
128
|
+
|
129
|
+
Test Suite
|
130
|
+
==========
|
131
|
+
|
132
|
+
In `test/rails_app` you can find a rails application with tests. To run
|
133
|
+
the tests you need to
|
134
|
+
|
135
|
+
0. Install the *jeweler* gem:
|
136
|
+
|
137
|
+
gem install jeweler
|
138
|
+
|
139
|
+
1. install the toast gem from this git clone:
|
140
|
+
|
141
|
+
rake install
|
142
|
+
|
143
|
+
2. initialize the test application
|
144
|
+
|
145
|
+
cd test/rails_app
|
146
|
+
bundle install
|
147
|
+
|
148
|
+
3. Now you can run the test suite from within the test application
|
149
|
+
|
150
|
+
rake
|
151
|
+
|
152
|
+
Or you may call `rake test` from the root directory of the working
|
153
|
+
copy. This will reinstall the toast gem before running tests
|
154
|
+
automatically.
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class ToastController < ApplicationController
|
2
|
+
|
3
|
+
def catch_all
|
4
|
+
|
5
|
+
begin
|
6
|
+
resource = Toast::Resource.build( params, request )
|
7
|
+
render resource.apply(request.method, request.body.read)
|
8
|
+
|
9
|
+
rescue Toast::ResourceNotFound => e
|
10
|
+
return head(:not_found)
|
11
|
+
|
12
|
+
rescue Toast::PayloadInvalid => e
|
13
|
+
return head(:unprocessable_entity)
|
14
|
+
|
15
|
+
rescue Toast::PayloadFormatError => e
|
16
|
+
return head(:bad_request)
|
17
|
+
|
18
|
+
rescue Toast::MethodNotAllowed => e
|
19
|
+
return head(:method_not_allowed)
|
20
|
+
|
21
|
+
rescue Toast::UnsupportedMediaType => e
|
22
|
+
return head(:unsupported_media_type)
|
23
|
+
|
24
|
+
rescue Exception => e
|
25
|
+
log_exception e
|
26
|
+
raise e if Rails.env == "test"
|
27
|
+
return head(:internal_server_error)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_found
|
33
|
+
return head(:not_found)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
if Rails.env == "test"
|
38
|
+
def log_exception e
|
39
|
+
puts "#{e.class}: '#{e.message}'\n\n#{e.backtrace[0..14].join("\n")}\n\n"
|
40
|
+
end
|
41
|
+
else
|
42
|
+
def log_exception e
|
43
|
+
logger.error("#{e.class}: '#{e.message}'\n\n#{e.backtrace.join("\n")}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Rails.application.routes.draw do
|
2
|
+
|
3
|
+
ActiveRecord::Base.descendants.each do |model|
|
4
|
+
next unless model.is_resourceful_model?
|
5
|
+
|
6
|
+
resource_name = model.to_s.pluralize.underscore
|
7
|
+
|
8
|
+
match "#{model.toast_config.namespace}/#{resource_name}(/:id(/:subresource))" => 'toast#catch_all', :constraints => { :id => /\d+/ }, :resource => resource_name
|
9
|
+
match "#{model.toast_config.namespace}/#{resource_name}/:subresource" => 'toast#catch_all', :resource => resource_name
|
10
|
+
end
|
11
|
+
|
12
|
+
match ":resource(/:id(/:subresource))" => 'toast#not_found', :constraints => { :id => /\d+/ }
|
13
|
+
match ":resource/:subresource" => 'toast#not_found'
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
|
data/lib/toast.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'toast/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'blockenspiel'
|
2
|
+
require 'toast/config_dsl'
|
3
|
+
|
4
|
+
module Toast
|
5
|
+
module ActiveRecordExtensions
|
6
|
+
|
7
|
+
# Configuration DSL
|
8
|
+
def resourceful_model &block
|
9
|
+
@toast_config = Toast::ConfigDSL::Base.new(self)
|
10
|
+
Blockenspiel.invoke( block, @toast_config)
|
11
|
+
|
12
|
+
# add class methods
|
13
|
+
self.instance_eval do
|
14
|
+
|
15
|
+
cattr_accessor :uri_base
|
16
|
+
|
17
|
+
def is_resourceful_model?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def toast_config
|
22
|
+
@toast_config
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# add instance methods
|
27
|
+
self.class_eval do
|
28
|
+
# Return toast's standard uri for a record
|
29
|
+
def uri
|
30
|
+
"#{self.class.uri_base}/#{self.class.to_s.pluralize.underscore}/#{self.id}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns a Hash with all exposed attributes
|
34
|
+
def exposed_attributes options = {}
|
35
|
+
options.reverse_merge! :in_collection => false,
|
36
|
+
:with_uri => true
|
37
|
+
|
38
|
+
# attributes
|
39
|
+
exposed_attr =
|
40
|
+
options[:in_collection] ? self.class.toast_config.in_collection.exposed_attributes :
|
41
|
+
self.class.toast_config.exposed_attributes
|
42
|
+
|
43
|
+
out = exposed_attr.inject({}) do |acc, attr|
|
44
|
+
acc[attr] = self[attr]
|
45
|
+
acc
|
46
|
+
end
|
47
|
+
|
48
|
+
# association URIs
|
49
|
+
exposed_assoc =
|
50
|
+
options[:in_collection] ? self.class.toast_config.in_collection.exposed_associations :
|
51
|
+
self.class.toast_config.exposed_associations
|
52
|
+
|
53
|
+
exposed_assoc.each do |assoc|
|
54
|
+
out[assoc] = self.uri + "/" + assoc
|
55
|
+
end
|
56
|
+
|
57
|
+
out["uri"] = self.uri if options[:with_uri]
|
58
|
+
|
59
|
+
out
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# defaults for non resourceful-models
|
65
|
+
def is_resourceful_model?
|
66
|
+
false
|
67
|
+
end
|
68
|
+
def resourceful_model_options
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
def resource_names
|
72
|
+
@@all_resourceful_resource_names
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Toast
|
2
|
+
class AssociateCollection < Resource
|
3
|
+
|
4
|
+
def initialize model, id, subresource_name
|
5
|
+
unless model.toast_config.exposed_associations.include? subresource_name
|
6
|
+
raise ResourceNotFound
|
7
|
+
end
|
8
|
+
|
9
|
+
@model = model
|
10
|
+
@record = model.find(id) rescue raise(ResourceNotFound)
|
11
|
+
@collection = subresource_name
|
12
|
+
|
13
|
+
@associate_model = Resource.get_class_by_resource_name subresource_name
|
14
|
+
@associate_model.uri_base = @model.uri_base
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def get
|
19
|
+
records = @record.send(@collection)
|
20
|
+
{
|
21
|
+
:json => records.map{|r| r.exposed_attributes(:in_collection => true)},
|
22
|
+
:status => :ok
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def put
|
27
|
+
raise MethodNotAllowed
|
28
|
+
end
|
29
|
+
|
30
|
+
def post payload
|
31
|
+
|
32
|
+
|
33
|
+
if self.media_type != @associate_model.toast_config.media_type
|
34
|
+
raise UnsupportedMediaType
|
35
|
+
end
|
36
|
+
|
37
|
+
if payload.keys.to_set != @associate_model.toast_config.exposed_attributes.to_set
|
38
|
+
raise PayloadInvalid
|
39
|
+
end
|
40
|
+
|
41
|
+
unless payload.is_a? Hash
|
42
|
+
raise PayloadFormatError
|
43
|
+
end
|
44
|
+
#debugger
|
45
|
+
record = @record.send(@collection).create payload
|
46
|
+
|
47
|
+
{
|
48
|
+
:json => record.exposed_attributes,
|
49
|
+
:location => record.uri,
|
50
|
+
:status => :created
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete
|
55
|
+
raise MethodNotAllowed
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Toast
|
2
|
+
class Attribute < Resource
|
3
|
+
|
4
|
+
def initialize model, id, attribute_name
|
5
|
+
unless model.toast_config.exposed_attributes.include? attribute_name
|
6
|
+
raise ResourceNotFound
|
7
|
+
end
|
8
|
+
|
9
|
+
@model = model
|
10
|
+
@record = model.find(id) rescue raise(ResourceNotFound)
|
11
|
+
@attribute_name = attribute_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def get
|
15
|
+
{
|
16
|
+
:json => @record[@attribute_name],
|
17
|
+
:status => :ok
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def put payload
|
22
|
+
@record.update_attribute(@attribute_name, payload)
|
23
|
+
{
|
24
|
+
:json => @record[@attribute_name],
|
25
|
+
:stauts => :ok,
|
26
|
+
:loaction => @record.uri
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def post payload
|
31
|
+
raise MethodNotAllowed
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete
|
35
|
+
raise MethodNotAllowed
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Toast
|
2
|
+
module ConfigDSL
|
3
|
+
|
4
|
+
class Base
|
5
|
+
include Blockenspiel::DSL
|
6
|
+
dsl_attr_accessor :media_type, :has_many, :namespace
|
7
|
+
|
8
|
+
def initialize model
|
9
|
+
@model = model
|
10
|
+
@fields = []
|
11
|
+
@collections = []
|
12
|
+
@media_type = "application/json"
|
13
|
+
@exposed_attributes = []
|
14
|
+
@exposed_associations = []
|
15
|
+
@in_collection = ConfigDSL::InCollection.new model, self
|
16
|
+
end
|
17
|
+
|
18
|
+
def fields= *fields
|
19
|
+
@fields.push *ConfigDSL.sanitize(fields,"fields")
|
20
|
+
@fields.each do |attr_or_assoc|
|
21
|
+
if @model.new.attributes.keys.include? attr_or_assoc
|
22
|
+
@exposed_attributes << attr_or_assoc
|
23
|
+
else
|
24
|
+
@exposed_associations << attr_or_assoc
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def fields *arg
|
31
|
+
return(@fields) if arg.empty?
|
32
|
+
self.fields = *arg
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :exposed_attributes, :exposed_associations
|
36
|
+
|
37
|
+
def collections= collections=[]
|
38
|
+
@collections = ConfigDSL.sanitize(collections, "collections")
|
39
|
+
end
|
40
|
+
|
41
|
+
def collections *arg
|
42
|
+
return(@collections) if arg.empty?
|
43
|
+
self.collections = *arg
|
44
|
+
end
|
45
|
+
|
46
|
+
def in_collection &block
|
47
|
+
if block_given?
|
48
|
+
Blockenspiel.invoke( block, @in_collection)
|
49
|
+
else
|
50
|
+
@in_collection
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# non DSL methods
|
55
|
+
dsl_methods false
|
56
|
+
end
|
57
|
+
|
58
|
+
class InCollection
|
59
|
+
include Blockenspiel::DSL
|
60
|
+
|
61
|
+
def initialize model, base_config
|
62
|
+
@model = model
|
63
|
+
@fields = base_config.fields
|
64
|
+
@exposed_attributes = base_config.exposed_attributes
|
65
|
+
@exposed_associations = base_config.exposed_associations
|
66
|
+
@media_type = "application/json"
|
67
|
+
end
|
68
|
+
|
69
|
+
def fields= *fields
|
70
|
+
@fields = ConfigDSL.sanitize(fields,"fields")
|
71
|
+
|
72
|
+
@exposed_attributes = []
|
73
|
+
@exposed_associations = []
|
74
|
+
|
75
|
+
@fields.each do |attr_or_assoc|
|
76
|
+
if @model.new.attributes.keys.include? attr_or_assoc
|
77
|
+
@exposed_attributes << attr_or_assoc
|
78
|
+
else
|
79
|
+
@exposed_associations << attr_or_assoc
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def fields *arg
|
85
|
+
return(@fields) if arg.empty?
|
86
|
+
self.fields = *arg
|
87
|
+
end
|
88
|
+
|
89
|
+
attr_reader :exposed_attributes, :exposed_associations
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Helper
|
94
|
+
|
95
|
+
# checks if list is made of symbols and strings
|
96
|
+
# converts a single value to an Array
|
97
|
+
# converts all symbols to strings
|
98
|
+
def self.sanitize list, name
|
99
|
+
list = [list].flatten
|
100
|
+
|
101
|
+
list.map do |x|
|
102
|
+
if (!x.is_a?(Symbol) && !x.is_a?(String))
|
103
|
+
raise "Toast Config Error: '#{name}' should be a list of Symbols or Strings"
|
104
|
+
else
|
105
|
+
x.to_s
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
data/lib/toast/engine.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'toast/active_record_extensions.rb'
|
2
|
+
require 'toast/resource.rb'
|
3
|
+
require 'toast/root_collection'
|
4
|
+
require 'toast/associate_collection'
|
5
|
+
require 'toast/attribute'
|
6
|
+
require 'toast/record'
|
7
|
+
require 'action_dispatch/http/request'
|
8
|
+
|
9
|
+
module Toast
|
10
|
+
class Engine < Rails::Engine
|
11
|
+
|
12
|
+
# configure our plugin on boot. other extension points such
|
13
|
+
# as configuration, rake tasks, etc, are also available
|
14
|
+
initializer "toast.initialize" do |app|
|
15
|
+
# Add 'restful_model' declaration to ActiveRecord::Base
|
16
|
+
ActiveRecord::Base.extend Toast::ActiveRecordExtensions
|
17
|
+
|
18
|
+
# Load all models in app/models early to setup routing
|
19
|
+
begin
|
20
|
+
Dir["#{Rails.root}/app/models/**/*.rb"].each{|m| require m }
|
21
|
+
|
22
|
+
rescue ActiveRecord::StatementInvalid
|
23
|
+
# raised when DB is not setup yet. (rake db:schema:load)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/toast/record.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Toast
|
2
|
+
class Record < Resource
|
3
|
+
|
4
|
+
def initialize model, id
|
5
|
+
@model = model
|
6
|
+
@record = model.find(id) rescue raise(ResourceNotFound.new)
|
7
|
+
end
|
8
|
+
|
9
|
+
def post payload
|
10
|
+
raise MethodNotAllowed
|
11
|
+
end
|
12
|
+
|
13
|
+
# get, put, delete, post return a Hash that can be used as
|
14
|
+
# argument for ActionController#render
|
15
|
+
|
16
|
+
def put payload
|
17
|
+
if self.media_type != @model.toast_config.media_type
|
18
|
+
raise UnsupportedMediaType
|
19
|
+
end
|
20
|
+
|
21
|
+
unless payload.is_a? Hash
|
22
|
+
raise PayloadFormatError
|
23
|
+
end
|
24
|
+
|
25
|
+
if payload.keys.to_set != @model.toast_config.exposed_attributes.to_set
|
26
|
+
raise PayloadInvalid
|
27
|
+
end
|
28
|
+
|
29
|
+
@record.update_attributes payload
|
30
|
+
{
|
31
|
+
:json => @record.exposed_attributes,
|
32
|
+
:status => :ok,
|
33
|
+
:location => @record.uri
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def get
|
38
|
+
{
|
39
|
+
:json => @record.exposed_attributes,
|
40
|
+
:status => :ok
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete
|
45
|
+
@record.destroy
|
46
|
+
{
|
47
|
+
:status => :ok
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Toast
|
2
|
+
|
3
|
+
class ResourceNotFound < Exception; end
|
4
|
+
class MethodNotAllowed < Exception; end
|
5
|
+
class PayloadInvalid < Exception; end
|
6
|
+
class PayloadFormatError < Exception; end
|
7
|
+
class UnsupportedMediaType < Exception; end
|
8
|
+
|
9
|
+
# Represents a resource. There are following resource types as sub classes:
|
10
|
+
# Record, RootCollection, AssociateCollection, Attribute
|
11
|
+
class Resource
|
12
|
+
|
13
|
+
attr_accessor :media_type
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
raise 'ToastResource#new: use #build to create an instance'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build params, request
|
20
|
+
|
21
|
+
resource_name = params[:resource]
|
22
|
+
id = params[:id]
|
23
|
+
subresource_name = params[:subresource]
|
24
|
+
|
25
|
+
uri_base = "#{request.protocol}#{request.host}"
|
26
|
+
unless (request.protocol == "http://" and request.port == 80) or
|
27
|
+
(request.protocol == "https://" and request.port == 443)
|
28
|
+
uri_base += ":#{request.port}"
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
|
33
|
+
model = get_class_by_resource_name resource_name
|
34
|
+
|
35
|
+
model.uri_base = uri_base
|
36
|
+
model.uri_base += "/#{model.toast_config.namespace}" if model.toast_config.namespace
|
37
|
+
|
38
|
+
# decide which sub type
|
39
|
+
rsc = if id.nil?
|
40
|
+
Toast::RootCollection.new(model, subresource_name)
|
41
|
+
elsif subresource_name.nil?
|
42
|
+
Toast::Record.new(model, id)
|
43
|
+
elsif model.toast_config.exposed_associations.include? subresource_name
|
44
|
+
Toast::AssociateCollection.new(model, id, subresource_name)
|
45
|
+
elsif model.toast_config.exposed_attributes.include? subresource_name
|
46
|
+
Toast::Attribute.new(model, id, subresource_name)
|
47
|
+
else
|
48
|
+
raise ResourceNotFound
|
49
|
+
end
|
50
|
+
|
51
|
+
rsc.media_type = request.media_type
|
52
|
+
|
53
|
+
rsc
|
54
|
+
rescue NameError
|
55
|
+
raise ResourceNotFound
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.get_class_by_resource_name name
|
60
|
+
begin
|
61
|
+
model = name.singularize.classify.constantize # raises NameError
|
62
|
+
|
63
|
+
unless ((model.superclass == ActiveRecord::Base) and model.is_resourceful_model?)
|
64
|
+
raise ResourceNotFound
|
65
|
+
end
|
66
|
+
|
67
|
+
model
|
68
|
+
|
69
|
+
rescue NameError
|
70
|
+
raise ResourceNotFound
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def apply method, payload
|
75
|
+
case method
|
76
|
+
when "PUT","POST"
|
77
|
+
self.send(method.downcase, ActiveSupport::JSON.decode(payload))
|
78
|
+
when "DELETE","GET"
|
79
|
+
self.send(method.downcase)
|
80
|
+
else
|
81
|
+
raise MethodNotAllowed
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Toast
|
2
|
+
class RootCollection < Resource
|
3
|
+
|
4
|
+
def initialize model, subresource_name
|
5
|
+
|
6
|
+
subresource_name ||= "all"
|
7
|
+
|
8
|
+
unless model.toast_config.collections.include? subresource_name
|
9
|
+
raise ResourceNotFound
|
10
|
+
end
|
11
|
+
|
12
|
+
@model = model
|
13
|
+
@collection = subresource_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def get
|
17
|
+
records = @model.send(@collection)
|
18
|
+
{
|
19
|
+
:json => records.map{|r| r.exposed_attributes(:in_collection => true)},
|
20
|
+
:status => :ok
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def put
|
25
|
+
raise MethodNotAllowed
|
26
|
+
end
|
27
|
+
|
28
|
+
def post payload
|
29
|
+
if @collection != "all"
|
30
|
+
raise MethodNotAllowed
|
31
|
+
end
|
32
|
+
|
33
|
+
if self.media_type != @model.toast_config.media_type
|
34
|
+
raise UnsupportedMediaType
|
35
|
+
end
|
36
|
+
|
37
|
+
unless payload.is_a? Hash
|
38
|
+
raise PayloadFormatError
|
39
|
+
end
|
40
|
+
|
41
|
+
if payload.keys.to_set != @model.toast_config.exposed_attributes.to_set
|
42
|
+
raise PayloadInvalid
|
43
|
+
end
|
44
|
+
|
45
|
+
record = @model.create payload
|
46
|
+
|
47
|
+
{
|
48
|
+
:json => record,
|
49
|
+
:location => record.uri,
|
50
|
+
:status => :created
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete
|
55
|
+
raise MethodNotAllowed
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: toast
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Annies
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-06-28 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: blockenspiel
|
16
|
+
requirement: &1015970 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.4.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *1015970
|
25
|
+
description: ! 'Toast is an extension to Ruby on Rails that lets you expose any
|
26
|
+
|
27
|
+
ActiveRecord model as a resource according to the REST paradigm. The
|
28
|
+
|
29
|
+
representation format is JSON.'
|
30
|
+
email: robokopp@fernwerk.net
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files:
|
34
|
+
- README.md
|
35
|
+
files:
|
36
|
+
- app/controller/toast_controller.rb
|
37
|
+
- config/routes.rb
|
38
|
+
- lib/toast.rb
|
39
|
+
- lib/toast/active_record_extensions.rb
|
40
|
+
- lib/toast/associate_collection.rb
|
41
|
+
- lib/toast/attribute.rb
|
42
|
+
- lib/toast/config_dsl.rb
|
43
|
+
- lib/toast/engine.rb
|
44
|
+
- lib/toast/record.rb
|
45
|
+
- lib/toast/resource.rb
|
46
|
+
- lib/toast/root_collection.rb
|
47
|
+
- README.md
|
48
|
+
homepage: https://github.com/robokopp/toast
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.8.5
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Toast adds a RESTful interface to ActiveRecord models in Ruby on Rails.
|
72
|
+
test_files: []
|