sinatra_resource 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/VERSION +1 -1
  2. data/examples/datacatalog/lib/resource.rb +2 -2
  3. data/examples/datacatalog/lib/roles.rb +1 -1
  4. data/examples/datacatalog/resources/categories_sources.rb +43 -0
  5. data/examples/datacatalog/test/helpers/lib/request_helpers.rb +10 -2
  6. data/examples/datacatalog/test/helpers/resource_test_helper.rb +1 -1
  7. data/examples/datacatalog/test/helpers/shared/api_keys.rb +4 -4
  8. data/examples/datacatalog/test/helpers/shared/model_counts.rb +81 -0
  9. data/examples/datacatalog/test/helpers/shared/status_codes.rb +7 -3
  10. data/examples/datacatalog/test/helpers/test_helper.rb +2 -9
  11. data/examples/datacatalog/test/resources/categories/categories_delete_test.rb +5 -17
  12. data/examples/datacatalog/test/resources/categories/categories_get_many_test.rb +5 -2
  13. data/examples/datacatalog/test/resources/categories/categories_get_one_test.rb +4 -3
  14. data/examples/datacatalog/test/resources/categories/categories_post_test.rb +27 -43
  15. data/examples/datacatalog/test/resources/categories/categories_put_test.rb +9 -15
  16. data/examples/datacatalog/test/resources/categories_sources/categories_sources_delete_test.rb +148 -0
  17. data/examples/datacatalog/test/resources/categories_sources/categories_sources_get_many_test.rb +92 -0
  18. data/examples/datacatalog/test/resources/categories_sources/categories_sources_get_one_test.rb +95 -0
  19. data/examples/datacatalog/test/resources/categories_sources/categories_sources_post_test.rb +187 -0
  20. data/examples/datacatalog/test/resources/categories_sources/categories_sources_put_test.rb +323 -0
  21. data/examples/datacatalog/test/resources/sources/sources_delete_test.rb +5 -17
  22. data/examples/datacatalog/test/resources/sources/sources_get_many_test.rb +5 -2
  23. data/examples/datacatalog/test/resources/sources/sources_get_one_test.rb +4 -3
  24. data/examples/datacatalog/test/resources/sources/sources_post_test.rb +22 -35
  25. data/examples/datacatalog/test/resources/sources/sources_put_test.rb +12 -18
  26. data/examples/datacatalog/test/resources/users/users_delete_test.rb +10 -22
  27. data/examples/datacatalog/test/resources/users/users_get_many_test.rb +5 -2
  28. data/examples/datacatalog/test/resources/users/users_get_one_test.rb +4 -3
  29. data/examples/datacatalog/test/resources/users/users_post_test.rb +15 -32
  30. data/examples/datacatalog/test/resources/users/users_put_test.rb +15 -23
  31. data/lib/builder/action_definitions.rb +60 -0
  32. data/lib/builder/helpers.rb +53 -26
  33. data/lib/builder/mongo_helpers.rb +61 -10
  34. data/lib/builder.rb +136 -38
  35. data/lib/resource.rb +99 -16
  36. data/lib/sinatra_resource.rb +6 -6
  37. data/notes/permissions.mdown +6 -6
  38. data/sinatra_resource.gemspec +17 -2
  39. metadata +17 -2
@@ -11,11 +11,13 @@ module SinatraResource
11
11
  #
12
12
  # @param [MongoMapper::Document] document
13
13
  #
14
+ # @param [Hash] resource_config
15
+ #
14
16
  # @return [Hash<String => Object>]
15
- def build_resource(role, document)
17
+ def build_resource(role, document, resource_config)
16
18
  resource = {}
17
- config[:properties].each_pair do |property, hash|
18
- if authorized?(:read, role, property)
19
+ resource_config[:properties].each_pair do |property, hash|
20
+ if authorized?(:read, role, resource_config, property)
19
21
  resource[property.to_s] = value(property, document, hash)
20
22
  end
21
23
  end
@@ -29,10 +31,12 @@ module SinatraResource
29
31
  #
30
32
  # @param [String] api_key
31
33
  #
34
+ # @param [Hash] resource_config
35
+ #
32
36
  # @return [Array<Hash<String => Object>>]
33
- def build_resources(documents)
37
+ def build_resources(documents, resource_config)
34
38
  documents.map do |document|
35
- build_resource(lookup_role(document), document)
39
+ build_resource(lookup_role(document), document, resource_config)
36
40
  end
37
41
  end
38
42
 
@@ -44,10 +48,17 @@ module SinatraResource
44
48
  # @param [Symbol] role
45
49
  # a role (such as :anonymous, :basic, or :admin)
46
50
  #
51
+ # @param [Hash] resource_config
52
+ #
53
+ # @param [Boolean] leaf
54
+ # If a simple resource, should be true.
55
+ # If a nested resource, are we at the 'end' (the leaf)?
56
+ #
47
57
  # @return [undefined]
48
- def check_params(action, role)
58
+ def check_params(action, role, resource_config, leaf)
59
+ return unless leaf
49
60
  params_check_action(action)
50
- params_check_action_and_role(action, role)
61
+ params_check_action_and_role(action, role, resource_config)
51
62
  end
52
63
 
53
64
  # Halt unless the current role has permission to carry out +action+
@@ -58,10 +69,12 @@ module SinatraResource
58
69
  # @param [Symbol] role
59
70
  # a role (such as :anonymous, :basic, or :admin)
60
71
  #
72
+ # @param [Hash] resource_config
73
+ #
61
74
  # @return [undefined]
62
- def check_permission(action, role)
63
- before_authorization(action, role)
64
- unless authorized?(action, role)
75
+ def check_permission(action, role, resource_config)
76
+ before_authorization(action, role, resource_config)
77
+ unless authorized?(action, role, resource_config)
65
78
  error 401, convert(body_for(:unauthorized))
66
79
  end
67
80
  end
@@ -85,12 +98,12 @@ module SinatraResource
85
98
  # @param [Object] object
86
99
  #
87
100
  # @return [String]
88
- def display(action, object)
101
+ def display(action, object, resource_config)
89
102
  case action
90
103
  when :read
91
104
  when :create
92
105
  response.status = 201
93
- path = config[:path] + %(/#{object["id"]})
106
+ path = resource_config[:path] + %(/#{object["id"]})
94
107
  response.headers['Location'] = full_uri(path)
95
108
  when :update
96
109
  when :delete
@@ -118,8 +131,8 @@ module SinatraResource
118
131
  # @param [String, nil] id
119
132
  #
120
133
  # @return [Symbol]
121
- def get_role(id=nil)
122
- lookup_role(id ? config[:model].find_by_id(id) : nil)
134
+ def get_role(model, id=nil)
135
+ lookup_role(id ? model.find_by_id(id) : nil)
123
136
  end
124
137
 
125
138
  # Return the minimum role required for +action+, and, if specified,
@@ -128,13 +141,17 @@ module SinatraResource
128
141
  # @param [Symbol] action
129
142
  # :read, :create, :update, or :delete
130
143
  #
144
+ # @param [Hash] resource_config
145
+ #
146
+ # @param [Symbol, nil] property
147
+ #
131
148
  # @return [Symbol]
132
149
  # a role (such as :anonymous, :basic, or :admin)
133
- def minimum_role(action, property=nil)
150
+ def minimum_role(action, resource_config, property=nil)
134
151
  if property.nil?
135
- config[:permission][to_read_or_modify(action)]
152
+ resource_config[:permission][to_read_or_modify(action)]
136
153
  else
137
- config[:properties][property][to_r_or_w(action)]
154
+ resource_config[:properties][property][to_r_or_w(action)]
138
155
  end || :anonymous
139
156
  end
140
157
 
@@ -148,18 +165,20 @@ module SinatraResource
148
165
  # @param [Symbol] action
149
166
  # :read, :create, :update, or :delete
150
167
  #
151
- # @param [Symbol] property
168
+ # @param [Hash] resource_config
169
+ #
170
+ # @param [Symbol, nil] property
152
171
  # a property of a resource
153
172
  #
154
173
  # @return [Boolean]
155
- def authorized?(action, role, property=nil)
156
- klass = config[:roles]
174
+ def authorized?(action, role, resource_config, property=nil)
175
+ klass = resource_config[:roles]
157
176
  klass.validate_role(role)
158
- klass.satisfies?(role, minimum_role(action, property))
177
+ klass.satisfies?(role, minimum_role(action, resource_config, property))
159
178
  end
160
179
 
161
180
  # Application-level hook that runs as part of +check_permission+,
162
- # before +authorized?(action, role)+ is called.
181
+ # before +authorized?(action, role, resource_config)+ is called.
163
182
  #
164
183
  # For example, an application might want to throw custom errors
165
184
  # in certain situations before +authorized?+ runs.
@@ -172,8 +191,10 @@ module SinatraResource
172
191
  # @param [Symbol] role
173
192
  # a role (such as :anonymous, :basic, or :admin)
174
193
  #
194
+ # @param [Hash] resource_config
195
+ #
175
196
  # @return [String]
176
- def before_authorization(action, role)
197
+ def before_authorization(action, role, resource_config)
177
198
  raise NotImplementedError
178
199
  end
179
200
 
@@ -201,7 +222,7 @@ module SinatraResource
201
222
  when :not_found
202
223
  ""
203
224
  when :unauthorized
204
- ""
225
+ { "errors" => "unauthorized_api_key" }
205
226
  end
206
227
  end
207
228
 
@@ -221,6 +242,10 @@ module SinatraResource
221
242
  # @param [Symbol] action
222
243
  # :read, :create, :update, or :delete
223
244
  #
245
+ # @param [Boolean] leaf
246
+ # If a simple resource, should be true.
247
+ # If a nested resource, are we at the 'end' (the leaf)?
248
+ #
224
249
  # @return [undefined]
225
250
  def params_check_action(action)
226
251
  case action
@@ -251,11 +276,13 @@ module SinatraResource
251
276
  # @param [Symbol] role
252
277
  # a role (such as :anonymous, :basic, or :admin)
253
278
  #
279
+ # @param [Hash] resource_config
280
+ #
254
281
  # @return [undefined]
255
- def params_check_action_and_role(action, role)
282
+ def params_check_action_and_role(action, role, resource_config)
256
283
  invalid = []
257
284
  params.each_pair do |property, value|
258
- invalid << property if !authorized?(action, role, property.intern)
285
+ invalid << property if !authorized?(action, role, resource_config, property.intern)
259
286
  end
260
287
  unless invalid.empty?
261
288
  error 400, convert(body_for(:invalid_params, invalid))
@@ -3,12 +3,29 @@ module SinatraResource
3
3
  class Builder
4
4
 
5
5
  module MongoHelpers
6
+
7
+
8
+ # Make sure that +parent+ document is related to the +child+ document
9
+ # by way of +association+. If not, return 404 Not Found.
10
+ #
11
+ # @param [MongoMapper::Document] parent
12
+ #
13
+ # @param [Symbol] association
14
+ #
15
+ # @param [String] child_id
16
+ #
17
+ # @return [MongoMapper::Document]
18
+ def check_related?(parent, association, child_id)
19
+ unless parent.send(association).find { |x| x.id == child_id }
20
+ error 404, convert(body_for(:not_found))
21
+ end
22
+ end
6
23
 
7
24
  # Create a document from params. If not valid, returns 400.
8
25
  #
9
26
  # @return [MongoMapper::Document]
10
- def create_document!
11
- document = config[:model].new(params)
27
+ def create_document!(model)
28
+ document = model.new(params)
12
29
  unless document.valid?
13
30
  error 400, convert(body_for(:invalid_document, document))
14
31
  end
@@ -23,8 +40,8 @@ module SinatraResource
23
40
  # @param [String] id
24
41
  #
25
42
  # @return [MongoMapper::Document]
26
- def delete_document!(id)
27
- document = find_document!(id)
43
+ def delete_document!(model, id)
44
+ document = find_document!(model, id)
28
45
  document.destroy
29
46
  document
30
47
  end
@@ -34,8 +51,8 @@ module SinatraResource
34
51
  # @param [String] id
35
52
  #
36
53
  # @return [MongoMapper::Document]
37
- def find_document!(id)
38
- document = config[:model].find_by_id(id)
54
+ def find_document!(model, id)
55
+ document = model.find_by_id(id)
39
56
  unless document
40
57
  error 404, convert(body_for(:not_found))
41
58
  end
@@ -48,15 +65,49 @@ module SinatraResource
48
65
  # a class that includes MongoMapper::Document
49
66
  #
50
67
  # @return [Array<MongoMapper::Document>]
51
- def find_documents!
52
- config[:model].find(:all)
68
+ def find_documents!(model)
69
+ model.find(:all)
70
+ end
71
+
72
+ # Delegates to application, who should use custom logic to related
73
+ # +parent+ and +child+.
74
+ #
75
+ # @param [MongoMapper::Document] parent
76
+ #
77
+ # @param [MongoMapper::Document] child
78
+ #
79
+ # @param [Hash] resource_config
80
+ #
81
+ # @return [MongoMapper::Document] child document
82
+ def make_related(parent, child, resource_config)
83
+ proc = resource_config[:relation][:create]
84
+ proc.call(parent, child) if proc
85
+ child
86
+ end
87
+
88
+ # Select only the +children+ that are related to the +parent+ by
89
+ # way of the +association+.
90
+ #
91
+ # @param [MongoMapper::Document] parent
92
+ #
93
+ # @param [Symbol] association
94
+ #
95
+ # @param [Array<MongoMapper::Document>] children
96
+ #
97
+ # @return [MongoMapper::Document]
98
+ def select_related(parent, association, children)
99
+ children.select do |child|
100
+ parent.send(association).find { |x| x.id == child.id }
101
+ end
102
+ # TODO: this has O^2 complexity because of the nesting.
103
+ # I think it is reducible to O.
53
104
  end
54
105
 
55
106
  # Update a document with +id+ from params. If not valid, returns 400.
56
107
  #
57
108
  # @return [MongoMapper::Document]
58
- def update_document!(id)
59
- document = config[:model].update(id, params)
109
+ def update_document!(model, id)
110
+ document = model.update(id, params)
60
111
  unless document.valid?
61
112
  error 400, convert(body_for(:invalid_document, document))
62
113
  end
data/lib/builder.rb CHANGED
@@ -3,7 +3,17 @@ module SinatraResource
3
3
  class Builder
4
4
 
5
5
  def initialize(klass)
6
- @klass = klass
6
+ @klass = klass
7
+
8
+ @resource_config = @klass.resource_config
9
+ @child_association = @resource_config[:child_association]
10
+ @model = @resource_config[:model]
11
+ @parent = @resource_config[:parent]
12
+ @path = @resource_config[:path]
13
+ if @parent
14
+ @parent_resource_config = @parent.resource_config
15
+ @parent_model = @parent_resource_config[:model]
16
+ end
7
17
  end
8
18
 
9
19
  def build
@@ -16,64 +26,152 @@ module SinatraResource
16
26
  end
17
27
 
18
28
  def build_get_one
19
- @klass.get '/:id/?' do
20
- id = params.delete("id")
21
- role = get_role(id)
22
- check_permission(:read, role)
23
- check_params(:read, role)
24
- document = find_document!(id)
25
- resource = build_resource(role, document)
26
- display(:read, resource)
29
+ model = @model
30
+ resource_config = @resource_config
31
+ if !@parent
32
+ @klass.get '/:id/?' do
33
+ id = params.delete("id")
34
+ role = get_role(model, id)
35
+ document = document_for_get_one(role, model, resource_config, true, id, nil, nil)
36
+ resource = build_resource(role, document, resource_config)
37
+ display(:read, resource, resource_config)
38
+ end
39
+ else
40
+ association = @child_association
41
+ parent_model = @parent_model
42
+ parent_resource_config = @parent_resource_config
43
+ path = @path
44
+ @parent.get "/:parent_id/#{path}/:id/?" do
45
+ id = params.delete("id")
46
+ parent_id = params.delete("parent_id")
47
+ parent_role = get_role(parent_model, parent_id)
48
+ parent_document = document_for_get_one(parent_role, parent_model, parent_resource_config, false, parent_id, nil, nil)
49
+ # ------
50
+ role = get_role(model, id)
51
+ document = document_for_get_one(role, model, resource_config, true, id, parent_document, association)
52
+ resource = build_resource(role, document, resource_config)
53
+ display(:read, resource, resource_config)
54
+ end
27
55
  end
28
56
  end
29
57
 
30
58
  def build_get_many
31
- @klass.get '/?' do
32
- role = get_role
33
- check_permission(:read, role)
34
- check_params(:read, role)
35
- documents = find_documents!
36
- resources = build_resources(documents)
37
- display(:read, resources)
59
+ model = @model
60
+ resource_config = @resource_config
61
+ if !@parent
62
+ @klass.get '/?' do
63
+ role = get_role(model)
64
+ documents = documents_for_get_many(role, model, resource_config, true, nil, nil)
65
+ resources = build_resources(documents, resource_config)
66
+ display(:read, resources, resource_config)
67
+ end
68
+ else
69
+ association = @child_association
70
+ parent_model = @parent_model
71
+ parent_resource_config = @parent_resource_config
72
+ path = @path
73
+ @parent.get "/:parent_id/#{path}/?" do
74
+ parent_id = params.delete("parent_id")
75
+ parent_role = get_role(parent_model, parent_id)
76
+ parent_document = document_for_get_one(parent_role, parent_model, parent_resource_config, false, parent_id, nil, nil)
77
+ # ------
78
+ role = get_role(model)
79
+ documents = documents_for_get_many(role, model, resource_config, true, parent_document, association)
80
+ resources = build_resources(documents, resource_config)
81
+ display(:read, resources, resource_config)
82
+ end
38
83
  end
39
84
  end
40
85
 
41
86
  def build_post
42
- @klass.post '/?' do
43
- role = get_role
44
- check_permission(:create, role)
45
- check_params(:create, role)
46
- document = create_document!
47
- resource = build_resource(role, document)
48
- display(:create, resource)
87
+ model = @model
88
+ resource_config = @resource_config
89
+ if !@parent
90
+ @klass.post '/?' do
91
+ role = get_role(model)
92
+ document = document_for_post(role, model, resource_config, true, nil, nil)
93
+ resource = build_resource(role, document, resource_config)
94
+ display(:create, resource, resource_config)
95
+ end
96
+ else
97
+ association = @child_association
98
+ parent_model = @parent_model
99
+ parent_resource_config = @parent_resource_config
100
+ path = @path
101
+ @parent.post "/:parent_id/#{path}/?" do
102
+ parent_id = params.delete("parent_id")
103
+ parent_role = get_role(parent_model, parent_id)
104
+ parent_document = document_for_get_one(parent_role, parent_model, parent_resource_config, false, parent_id, nil, nil)
105
+ # ------
106
+ role = get_role(model)
107
+ document = document_for_post(role, model, resource_config, true, parent_document, association)
108
+ resource = build_resource(role, document, resource_config)
109
+ display(:create, resource, resource_config)
110
+ end
49
111
  end
50
112
  end
51
113
 
52
114
  def build_put
53
- @klass.put '/:id/?' do
54
- id = params.delete("id")
55
- role = get_role(id)
56
- check_permission(:update, role)
57
- check_params(:update, role)
58
- document = update_document!(id)
59
- resource = build_resource(role, document)
60
- display(:update, resource)
115
+ model = @model
116
+ resource_config = @resource_config
117
+ if !@parent
118
+ @klass.put '/:id/?' do
119
+ id = params.delete("id")
120
+ role = get_role(model, id)
121
+ document = document_for_put(role, model, resource_config, true, id, nil, nil)
122
+ resource = build_resource(role, document, resource_config)
123
+ display(:update, resource, resource_config)
124
+ end
125
+ else
126
+ association = @child_association
127
+ parent_model = @parent_model
128
+ parent_resource_config = @parent_resource_config
129
+ path = @path
130
+ @parent.put "/:parent_id/#{path}/:id/?" do
131
+ id = params.delete("id")
132
+ parent_id = params.delete("parent_id")
133
+ parent_role = get_role(parent_model, parent_id)
134
+ parent_document = document_for_get_one(parent_role, parent_model, parent_resource_config, false, parent_id, id, id)
135
+ # ------
136
+ role = get_role(model, id)
137
+ document = document_for_put(role, model, resource_config, true, id, parent_document, association)
138
+ resource = build_resource(role, document, resource_config)
139
+ display(:update, resource, resource_config)
140
+ end
61
141
  end
62
142
  end
63
143
 
64
144
  def build_delete
65
- @klass.delete '/:id/?' do
66
- id = params.delete("id")
67
- role = get_role(id)
68
- check_permission(:delete, role)
69
- check_params(:delete, role)
70
- delete_document!(id)
71
- display(:delete, "")
145
+ model = @model
146
+ resource_config = @resource_config
147
+ if !@parent
148
+ @klass.delete '/:id/?' do
149
+ id = params.delete("id")
150
+ role = get_role(model, id)
151
+ document_for_delete(role, model, resource_config, true, id, nil, nil)
152
+ display(:delete, "", resource_config)
153
+ end
154
+ else
155
+ association = @child_association
156
+ parent_model = @parent_model
157
+ parent_resource_config = @parent_resource_config
158
+ path = @path
159
+ @parent.delete "/:parent_id/#{path}/:id/?" do
160
+ id = params.delete("id")
161
+ parent_id = params.delete("parent_id")
162
+ parent_role = get_role(parent_model, parent_id)
163
+ parent_document = document_for_get_one(parent_role, parent_model, parent_resource_config, false, parent_id, nil, nil)
164
+ # ------
165
+ role = get_role(model, id)
166
+ document_for_delete(role, model, resource_config, true, id, parent_document, association)
167
+ display(:delete, "", resource_config)
168
+ end
72
169
  end
73
170
  end
74
171
 
75
172
  def build_helpers
76
173
  @klass.helpers do
174
+ include ActionDefinitions
77
175
  include Helpers
78
176
  include MongoHelpers
79
177
  end