toast 0.1.0
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.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: []
|