toast 0.3.5 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +9 -12
- data/lib/toast/association.rb +51 -12
- data/lib/toast/config_dsl.rb +57 -47
- data/lib/toast/record.rb +9 -2
- data/lib/toast/resource.rb +1 -3
- data/lib/toast/root_collection.rb +5 -4
- metadata +4 -5
- data/lib/toast/attribute.rb +0 -42
data/README.md
CHANGED
@@ -11,7 +11,7 @@ and what attributes and associations are to be exposed. That's it. No
|
|
11
11
|
controller boiler plate code for every model, no routing setup.
|
12
12
|
|
13
13
|
Toast is a Rails engine that runs one generic controller and a sets up
|
14
|
-
the routing
|
14
|
+
the routing according to the definition in the models, which is
|
15
15
|
denoted using a block oriented DSL.
|
16
16
|
|
17
17
|
REST is more than some pretty URIs, the use of the HTTP verbs and
|
@@ -23,7 +23,7 @@ which ever suits the task best. That's why TOAST stands for:
|
|
23
23
|
> **TOast Ain't reST**
|
24
24
|
|
25
25
|
*Be careful*: This version is experimental and probably not bullet
|
26
|
-
proof. As soon as the gem is
|
26
|
+
proof. As soon as the gem is loaded a controller with ready routing
|
27
27
|
is enabled serving the annotated model's data records for reading,
|
28
28
|
updating and deleting. There are no measures to prevent XSS and CSFR
|
29
29
|
attacks.
|
@@ -49,8 +49,9 @@ and let a corresponding model class have a *resourceful_model* annotation:
|
|
49
49
|
|
50
50
|
resourceful_model do
|
51
51
|
# attributes or association names
|
52
|
-
|
53
|
-
|
52
|
+
readables :coconuts, :apple
|
53
|
+
writables :name, :number
|
54
|
+
|
54
55
|
# class methods of Banana returning an Array of Banana records
|
55
56
|
collections :find_some, :all
|
56
57
|
end
|
@@ -121,17 +122,13 @@ More details and configuration options are documented in the manual... (_comming
|
|
121
122
|
Installation
|
122
123
|
============
|
123
124
|
|
124
|
-
|
125
|
-
gem install jeweler
|
126
|
-
cd toast
|
127
|
-
rake install
|
125
|
+
With bundler
|
128
126
|
|
129
|
-
|
127
|
+
gem "toast"
|
130
128
|
|
131
|
-
|
129
|
+
the latest Git:
|
132
130
|
|
133
|
-
|
134
|
-
Please use the git (edge) version.
|
131
|
+
gem "toast", :git => "https://github.com/robokopp/toast.git"
|
135
132
|
|
136
133
|
Test Suite
|
137
134
|
==========
|
data/lib/toast/association.rb
CHANGED
@@ -10,8 +10,9 @@ module Toast
|
|
10
10
|
|
11
11
|
@model = model
|
12
12
|
@record = model.find(id) rescue raise(ResourceNotFound)
|
13
|
-
@
|
13
|
+
@assoc = subresource_name
|
14
14
|
@format = format
|
15
|
+
@is_collection = [:has_many, :has_and_belongs_to_many].include? @model.reflect_on_association(@assoc.to_sym).macro
|
15
16
|
|
16
17
|
@associate_model = Resource.get_class_by_resource_name subresource_name
|
17
18
|
@associate_model.uri_base = @model.uri_base
|
@@ -19,7 +20,7 @@ module Toast
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def get
|
22
|
-
result = @record.send(@
|
23
|
+
result = @record.send(@assoc)
|
23
24
|
|
24
25
|
if result.is_a? Array
|
25
26
|
{
|
@@ -35,8 +36,38 @@ module Toast
|
|
35
36
|
|
36
37
|
end
|
37
38
|
|
38
|
-
def put
|
39
|
-
|
39
|
+
def put payload
|
40
|
+
# only for has_one/belongs_to assocs
|
41
|
+
raise MethodNotAllowed if @is_collection
|
42
|
+
|
43
|
+
# update see record
|
44
|
+
if self.media_type != @associate_model.toast_config.media_type
|
45
|
+
raise UnsupportedMediaType
|
46
|
+
end
|
47
|
+
|
48
|
+
unless payload.is_a? Hash
|
49
|
+
raise PayloadFormatError
|
50
|
+
end
|
51
|
+
|
52
|
+
# silently ignore all exposed readable, but not writable fields
|
53
|
+
(@associate_model.toast_config.readables - @associate_model.toast_config.writables).each do |rof|
|
54
|
+
payload.delete(rof)
|
55
|
+
end
|
56
|
+
|
57
|
+
record = @record.send(@assoc)
|
58
|
+
|
59
|
+
# set the virtual attributes
|
60
|
+
(payload.keys.to_set - record.attribute_names.to_set).each do |vattr|
|
61
|
+
record.send("#{vattr}=", payload.delete(vattr))
|
62
|
+
end
|
63
|
+
|
64
|
+
# mass-update for the rest
|
65
|
+
record.update_attributes payload
|
66
|
+
{
|
67
|
+
:json => record.exposed_attributes,
|
68
|
+
:status => :ok,
|
69
|
+
:location => record.uri
|
70
|
+
}
|
40
71
|
end
|
41
72
|
|
42
73
|
def post payload
|
@@ -45,21 +76,29 @@ module Toast
|
|
45
76
|
raise UnsupportedMediaType
|
46
77
|
end
|
47
78
|
|
48
|
-
|
49
|
-
|
79
|
+
|
80
|
+
# silently ignore all exposed readable, but not writable fields
|
81
|
+
(@associate_model.toast_config.readables - @associate_model.toast_config.writables).each do |rof|
|
82
|
+
payload.delete(rof)
|
50
83
|
end
|
51
84
|
|
52
85
|
unless payload.is_a? Hash
|
53
86
|
raise PayloadFormatError
|
54
87
|
end
|
55
88
|
|
56
|
-
|
89
|
+
begin
|
90
|
+
record = @record.send(@assoc).create! payload
|
57
91
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
92
|
+
{
|
93
|
+
:json => record.exposed_attributes,
|
94
|
+
:location => record.uri,
|
95
|
+
:status => :created
|
96
|
+
}
|
97
|
+
|
98
|
+
rescue ActiveRecord::RecordInvalid => e
|
99
|
+
# model validation failed
|
100
|
+
raise PayloadInvalid.new(e.message)
|
101
|
+
end
|
63
102
|
end
|
64
103
|
|
65
104
|
def delete
|
data/lib/toast/config_dsl.rb
CHANGED
@@ -4,12 +4,11 @@ module Toast
|
|
4
4
|
class Base
|
5
5
|
include Blockenspiel::DSL
|
6
6
|
dsl_attr_accessor :media_type, :has_many, :namespace
|
7
|
-
# attr_reader :exposed_attributes, :exposed_associations
|
8
7
|
|
9
8
|
def initialize model
|
10
9
|
@model = model
|
11
|
-
@
|
12
|
-
@
|
10
|
+
@readables = ["uri"]
|
11
|
+
@writables = []
|
13
12
|
@collections = []
|
14
13
|
@singles = []
|
15
14
|
@media_type = "application/json"
|
@@ -18,50 +17,63 @@ module Toast
|
|
18
17
|
@in_collection = ConfigDSL::InCollection.new model, self
|
19
18
|
end
|
20
19
|
|
21
|
-
def fields= *fields
|
22
|
-
@fields.push *ConfigDSL.sanitize(fields,"fields")
|
23
|
-
end
|
24
|
-
|
25
|
-
def fields *arg
|
26
|
-
return(@fields) if arg.empty?
|
27
|
-
self.fields = *arg
|
28
|
-
end
|
29
|
-
|
30
20
|
def exposed_attributes
|
31
21
|
assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
|
32
|
-
@
|
22
|
+
(@writables + @readables).uniq.select{|f| !assocs.include?(f)}
|
33
23
|
end
|
34
24
|
|
35
25
|
def exposed_associations
|
36
26
|
assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
|
37
|
-
@
|
27
|
+
(@writables + @readables).uniq.select{|f| assocs.include?(f)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def readables= arg
|
31
|
+
#if arg.first == :all
|
32
|
+
# @readables.push @model.attribute_names - ConfigDSL.sanitize(args.last[:except], "readables")
|
33
|
+
#else
|
34
|
+
@readables.push *ConfigDSL.sanitize(arg,"readables")
|
35
|
+
#end
|
38
36
|
end
|
39
37
|
|
40
|
-
|
41
|
-
|
38
|
+
# args: Array or :all, :except => Array
|
39
|
+
def readables *arg
|
40
|
+
return(@readables) if arg.empty?
|
41
|
+
self.readables = arg
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
44
|
+
def writables= arg
|
45
|
+
#if arg.first == :all
|
46
|
+
# @writables.push @model.attribute_names - ConfigDSL.sanitize(args.last[:except], "writables")
|
47
|
+
#else
|
48
|
+
# white list writables (protect the rest from mass-assignment)
|
49
|
+
@model.attr_accessible *arg
|
50
|
+
@writables.push *ConfigDSL.sanitize(arg,"writables")
|
51
|
+
#end
|
47
52
|
end
|
53
|
+
|
54
|
+
# args: Array or :all, :except => Array
|
55
|
+
def writables *arg
|
56
|
+
return(@writables) if arg.empty?
|
57
|
+
self.writables = arg
|
58
|
+
end
|
59
|
+
|
48
60
|
|
49
|
-
def disallow_methods=
|
61
|
+
def disallow_methods= arg
|
50
62
|
@disallow_methods.push *ConfigDSL.sanitize(arg,"disallow methods")
|
51
63
|
end
|
52
64
|
|
53
65
|
def disallow_methods *arg
|
54
66
|
return(@disallow_methods) if arg.empty?
|
55
|
-
self.disallow_methods =
|
67
|
+
self.disallow_methods = arg
|
56
68
|
end
|
57
69
|
|
58
|
-
def pass_params_to=
|
70
|
+
def pass_params_to= arg
|
59
71
|
@pass_params_to.push *ConfigDSL.sanitize(arg,"pass_params_to")
|
60
72
|
end
|
61
73
|
|
62
74
|
def pass_params_to *arg
|
63
75
|
return(@pass_params_to) if arg.empty?
|
64
|
-
self.pass_params_to =
|
76
|
+
self.pass_params_to = arg
|
65
77
|
end
|
66
78
|
|
67
79
|
def collections= collections=[]
|
@@ -70,7 +82,7 @@ module Toast
|
|
70
82
|
|
71
83
|
def collections *arg
|
72
84
|
return(@collections) if arg.empty?
|
73
|
-
self.collections =
|
85
|
+
self.collections = arg
|
74
86
|
end
|
75
87
|
|
76
88
|
def singles= singles=[]
|
@@ -79,7 +91,7 @@ module Toast
|
|
79
91
|
|
80
92
|
def singles *arg
|
81
93
|
return(@singles) if arg.empty?
|
82
|
-
self.singles =
|
94
|
+
self.singles = arg
|
83
95
|
end
|
84
96
|
|
85
97
|
def in_collection &block
|
@@ -99,53 +111,51 @@ module Toast
|
|
99
111
|
|
100
112
|
def initialize model, base_config
|
101
113
|
@model = model
|
102
|
-
@
|
114
|
+
@readables = base_config.readables # must assign a reference
|
115
|
+
@writables = base_config.writables # must assign a reference
|
103
116
|
@disallow_methods = []
|
104
|
-
# @exposed_attributes = base_config.exposed_attributes
|
105
|
-
# @exposed_associations = base_config.exposed_associations
|
106
117
|
@media_type = "application/json"
|
107
118
|
end
|
108
119
|
|
109
|
-
def
|
110
|
-
@
|
120
|
+
def readables= readables
|
121
|
+
@writables = [] # forget inherited writables
|
122
|
+
@readables = ConfigDSL.sanitize(readables,"readables") << "uri"
|
123
|
+
end
|
111
124
|
|
112
|
-
|
113
|
-
|
125
|
+
def readables *arg
|
126
|
+
return(@readables) if arg.empty?
|
127
|
+
self.readables = arg
|
128
|
+
end
|
114
129
|
|
115
|
-
|
116
|
-
|
117
|
-
# @exposed_attributes << attr_or_assoc
|
118
|
-
# else
|
119
|
-
# @exposed_associations << attr_or_assoc
|
120
|
-
# end
|
121
|
-
# end
|
130
|
+
def writables *arg
|
131
|
+
self.writables = 42
|
122
132
|
end
|
123
133
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
134
|
+
def writables= arg
|
135
|
+
puts
|
136
|
+
puts "Toast Config Warning (#{model.class}): Defining \"writables\" in collection definition has no effect."
|
137
|
+
puts
|
127
138
|
end
|
128
139
|
|
129
140
|
def exposed_attributes
|
130
141
|
assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
|
131
|
-
@
|
142
|
+
(@readables + @writables).uniq.select{|f| !assocs.include?(f)}
|
132
143
|
end
|
133
144
|
|
134
145
|
def exposed_associations
|
135
146
|
assocs = @model.reflect_on_all_associations.map{|a| a.name.to_s}
|
136
|
-
@
|
147
|
+
(@readables + @writables).uniq.select{|f| assocs.include?(f)}
|
137
148
|
end
|
138
149
|
|
139
|
-
def disallow_methods=
|
150
|
+
def disallow_methods= arg
|
140
151
|
@disallow_methods.push *ConfigDSL.sanitize(arg,"disallow methods")
|
141
152
|
end
|
142
153
|
|
143
154
|
def disallow_methods *arg
|
144
155
|
return(@disallow_methods) if arg.empty?
|
145
|
-
self.disallow_methods =
|
156
|
+
self.disallow_methods = arg
|
146
157
|
end
|
147
158
|
|
148
|
-
# attr_reader :exposed_attributes, :exposed_associations
|
149
159
|
end
|
150
160
|
|
151
161
|
|
data/lib/toast/record.rb
CHANGED
@@ -25,10 +25,17 @@ module Toast
|
|
25
25
|
raise PayloadFormatError
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
# silently ignore all exposed readable, but not writable fields
|
29
|
+
(@model.toast_config.readables - @model.toast_config.writables).each do |rof|
|
30
|
+
payload.delete(rof)
|
30
31
|
end
|
31
32
|
|
33
|
+
# set the virtual attributes
|
34
|
+
(payload.keys.to_set - @record.attribute_names.to_set).each do |vattr|
|
35
|
+
@record.send("#{vattr}=", payload.delete(vattr))
|
36
|
+
end
|
37
|
+
|
38
|
+
# mass-update for the rest
|
32
39
|
@record.update_attributes payload
|
33
40
|
{
|
34
41
|
:json => @record.exposed_attributes,
|
data/lib/toast/resource.rb
CHANGED
@@ -7,7 +7,7 @@ module Toast
|
|
7
7
|
class UnsupportedMediaType < Exception; end
|
8
8
|
|
9
9
|
# Represents a resource. There are following resource types as sub classes:
|
10
|
-
# Record, RootCollection,
|
10
|
+
# Record, RootCollection, Association, Single
|
11
11
|
class Resource
|
12
12
|
|
13
13
|
attr_accessor :media_type
|
@@ -39,8 +39,6 @@ module Toast
|
|
39
39
|
Toast::Record.new(model, id, format)
|
40
40
|
elsif model.toast_config.exposed_associations.include? subresource_name
|
41
41
|
Toast::Association.new(model, id, subresource_name, format)
|
42
|
-
elsif model.toast_config.exposed_attributes.include? subresource_name
|
43
|
-
Toast::Attribute.new(model, id, subresource_name, format)
|
44
42
|
else
|
45
43
|
raise ResourceNotFound
|
46
44
|
end
|
@@ -65,11 +65,11 @@ module Toast
|
|
65
65
|
raise PayloadFormatError
|
66
66
|
end
|
67
67
|
|
68
|
-
|
69
|
-
|
68
|
+
# silently ignore all exposed readable, but not writable fields
|
69
|
+
(@model.toast_config.readables - @model.toast_config.writables).each do |rof|
|
70
|
+
payload.delete(rof)
|
70
71
|
end
|
71
72
|
|
72
|
-
|
73
73
|
begin
|
74
74
|
record = @model.create! payload
|
75
75
|
|
@@ -78,8 +78,9 @@ module Toast
|
|
78
78
|
:location => record.uri,
|
79
79
|
:status => :created
|
80
80
|
}
|
81
|
-
|
81
|
+
|
82
82
|
rescue ActiveRecord::RecordInvalid => e
|
83
|
+
# model validation failed
|
83
84
|
raise PayloadInvalid.new(e.message)
|
84
85
|
end
|
85
86
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 4
|
8
|
+
- 0
|
9
|
+
version: 0.4.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Robert Annies
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2012-
|
17
|
+
date: 2012-03-13 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -48,7 +48,6 @@ files:
|
|
48
48
|
- lib/toast.rb
|
49
49
|
- lib/toast/active_record_extensions.rb
|
50
50
|
- lib/toast/association.rb
|
51
|
-
- lib/toast/attribute.rb
|
52
51
|
- lib/toast/config_dsl.rb
|
53
52
|
- lib/toast/engine.rb
|
54
53
|
- lib/toast/record.rb
|
data/lib/toast/attribute.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
module Toast
|
2
|
-
class Attribute < Resource
|
3
|
-
|
4
|
-
attr_reader :model
|
5
|
-
|
6
|
-
def initialize model, id, attribute_name, format
|
7
|
-
unless model.toast_config.exposed_attributes.include? attribute_name
|
8
|
-
raise ResourceNotFound
|
9
|
-
end
|
10
|
-
|
11
|
-
@model = model
|
12
|
-
@record = model.find(id) rescue raise(ResourceNotFound)
|
13
|
-
@attribute_name = attribute_name
|
14
|
-
@format = format
|
15
|
-
end
|
16
|
-
|
17
|
-
def get
|
18
|
-
{
|
19
|
-
:json => @record[@attribute_name],
|
20
|
-
:status => :ok
|
21
|
-
}
|
22
|
-
end
|
23
|
-
|
24
|
-
def put payload
|
25
|
-
|
26
|
-
@record.update_attribute(@attribute_name, payload)
|
27
|
-
{
|
28
|
-
:json => @record[@attribute_name],
|
29
|
-
:stauts => :ok,
|
30
|
-
:loaction => @record.uri
|
31
|
-
}
|
32
|
-
end
|
33
|
-
|
34
|
-
def post payload
|
35
|
-
raise MethodNotAllowed
|
36
|
-
end
|
37
|
-
|
38
|
-
def delete
|
39
|
-
raise MethodNotAllowed
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|