sinatra_resource 0.1.0 → 0.2.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.
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