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 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 to it according to the definition in the models, which is
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 installed a controller with ready routing
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
- fields :name, :number, :coconuts, :apple
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
- git clone git@github.com:robokopp/toast.git
125
- gem install jeweler
126
- cd toast
127
- rake install
125
+ With bundler
128
126
 
129
- or with bundler (Gemfile)
127
+ gem "toast"
130
128
 
131
- gem "toast", :git => "https://github.com/robokopp/toast.git"
129
+ the latest Git:
132
130
 
133
- Note: There is a Gem on RubyGems.org too, but I will not keep it up-to-date for now.
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
  ==========
@@ -10,8 +10,9 @@ module Toast
10
10
 
11
11
  @model = model
12
12
  @record = model.find(id) rescue raise(ResourceNotFound)
13
- @collection = subresource_name
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(@collection)
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
- raise MethodNotAllowed
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
- if payload.keys.to_set != (@associate_model.toast_config.exposed_attributes.to_set - @associate_model.toast_config.auto_fields.to_set)
49
- raise PayloadInvalid
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
- record = @record.send(@collection).create payload
89
+ begin
90
+ record = @record.send(@assoc).create! payload
57
91
 
58
- {
59
- :json => record.exposed_attributes,
60
- :location => record.uri,
61
- :status => :created
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
@@ -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
- @fields = []
12
- @auto_fields = []
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
- @fields.select{|f| !assocs.include?(f)}
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
- @fields.select{|f| assocs.include?(f)}
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
- def auto_fields= *arg
41
- @auto_fields.push *ConfigDSL.sanitize(arg,"auto fields")
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 auto_fields *arg
45
- return(@auto_fields) if arg.empty?
46
- self.auto_fields = *arg
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= *arg
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 = *arg
67
+ self.disallow_methods = arg
56
68
  end
57
69
 
58
- def pass_params_to= *arg
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 = *arg
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 = *arg
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 = *arg
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
- @fields = base_config.fields
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 fields= *fields
110
- @fields = ConfigDSL.sanitize(fields,"fields")
120
+ def readables= readables
121
+ @writables = [] # forget inherited writables
122
+ @readables = ConfigDSL.sanitize(readables,"readables") << "uri"
123
+ end
111
124
 
112
- # @exposed_attributes = []
113
- # @exposed_associations = []
125
+ def readables *arg
126
+ return(@readables) if arg.empty?
127
+ self.readables = arg
128
+ end
114
129
 
115
- # @fields.each do |attr_or_assoc|
116
- # if @model.new.attributes.keys.include? attr_or_assoc
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 fields *arg
125
- return(@fields) if arg.empty?
126
- self.fields = *arg
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
- @fields.select{|f| !assocs.include?(f)}
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
- @fields.select{|f| assocs.include?(f)}
147
+ (@readables + @writables).uniq.select{|f| assocs.include?(f)}
137
148
  end
138
149
 
139
- def disallow_methods= *arg
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 = *arg
156
+ self.disallow_methods = arg
146
157
  end
147
158
 
148
- # attr_reader :exposed_attributes, :exposed_associations
149
159
  end
150
160
 
151
161
 
@@ -25,10 +25,17 @@ module Toast
25
25
  raise PayloadFormatError
26
26
  end
27
27
 
28
- if payload.keys.to_set != (@model.toast_config.exposed_attributes.to_set - @model.toast_config.auto_fields.to_set)
29
- raise PayloadInvalid
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,
@@ -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, AssociateCollection, Attribute
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
- if payload.keys.to_set != (@model.toast_config.exposed_attributes.to_set - @model.toast_config.auto_fields.to_set)
69
- raise PayloadInvalid
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
- - 3
8
- - 5
9
- version: 0.3.5
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-02-29 00:00:00 +01:00
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
@@ -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