haveapi 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/lib/haveapi/action.rb +521 -0
  3. data/lib/haveapi/actions/default.rb +55 -0
  4. data/lib/haveapi/actions/paginable.rb +12 -0
  5. data/lib/haveapi/api.rb +66 -0
  6. data/lib/haveapi/authentication/base.rb +37 -0
  7. data/lib/haveapi/authentication/basic/provider.rb +37 -0
  8. data/lib/haveapi/authentication/chain.rb +110 -0
  9. data/lib/haveapi/authentication/token/provider.rb +166 -0
  10. data/lib/haveapi/authentication/token/resources.rb +107 -0
  11. data/lib/haveapi/authorization.rb +108 -0
  12. data/lib/haveapi/common.rb +38 -0
  13. data/lib/haveapi/context.rb +78 -0
  14. data/lib/haveapi/example.rb +36 -0
  15. data/lib/haveapi/extensions/action_exceptions.rb +25 -0
  16. data/lib/haveapi/extensions/base.rb +9 -0
  17. data/lib/haveapi/extensions/resource_prefetch.rb +7 -0
  18. data/lib/haveapi/hooks.rb +190 -0
  19. data/lib/haveapi/metadata.rb +56 -0
  20. data/lib/haveapi/model_adapter.rb +119 -0
  21. data/lib/haveapi/model_adapters/active_record.rb +352 -0
  22. data/lib/haveapi/model_adapters/hash.rb +27 -0
  23. data/lib/haveapi/output_formatter.rb +57 -0
  24. data/lib/haveapi/output_formatters/base.rb +29 -0
  25. data/lib/haveapi/output_formatters/json.rb +9 -0
  26. data/lib/haveapi/params/param.rb +114 -0
  27. data/lib/haveapi/params/resource.rb +109 -0
  28. data/lib/haveapi/params.rb +314 -0
  29. data/lib/haveapi/public/css/bootstrap-theme.min.css +7 -0
  30. data/lib/haveapi/public/css/bootstrap.min.css +7 -0
  31. data/lib/haveapi/public/js/bootstrap.min.js +6 -0
  32. data/lib/haveapi/public/js/jquery-1.11.1.min.js +4 -0
  33. data/lib/haveapi/resource.rb +120 -0
  34. data/lib/haveapi/route.rb +22 -0
  35. data/lib/haveapi/server.rb +440 -0
  36. data/lib/haveapi/spec/helpers.rb +103 -0
  37. data/lib/haveapi/types.rb +24 -0
  38. data/lib/haveapi/version.rb +3 -0
  39. data/lib/haveapi/views/doc_layout.erb +27 -0
  40. data/lib/haveapi/views/doc_sidebars/create-client.erb +20 -0
  41. data/lib/haveapi/views/doc_sidebars/protocol.erb +42 -0
  42. data/lib/haveapi/views/index.erb +12 -0
  43. data/lib/haveapi/views/main_layout.erb +50 -0
  44. data/lib/haveapi/views/version_page.erb +195 -0
  45. data/lib/haveapi/views/version_sidebar.erb +42 -0
  46. data/lib/haveapi.rb +22 -0
  47. metadata +242 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3eed7a7a97050e343134cb1d9cfd614099a613c6
4
+ data.tar.gz: 09d4bc3e055b2d385b8e6bdf49140425852b4a6f
5
+ SHA512:
6
+ metadata.gz: ed8241e6a1e8d65b41fb56671fe421730eaab16a748d8373d4331c06a5b14990e2e4a7b535919524bf655ab8a0ca0f6fccdb4c83932d9ec78ece6caffb7e9397
7
+ data.tar.gz: 992ef4d404cdd0b07564b6bd9310c59280544a026382ec1a84b753586a8cab2482a293f72b3ea15b55f4dcd487181747b7abf14f2f04b880b057ea7b8bef7272
@@ -0,0 +1,521 @@
1
+ module HaveAPI
2
+ class Action < Common
3
+ obj_type :action
4
+ has_attr :version
5
+ has_attr :desc
6
+ has_attr :route
7
+ has_attr :resolve, ->(klass){ klass.respond_to?(:id) ? klass.id : nil }
8
+ has_attr :http_method, :get
9
+ has_attr :auth, true
10
+ has_attr :aliases, []
11
+
12
+ include Hookable
13
+
14
+ has_hook :exec_exception
15
+
16
+ attr_reader :message, :errors, :version
17
+ attr_accessor :flags
18
+
19
+ class << self
20
+ attr_reader :resource, :authorization, :examples
21
+
22
+ def inherited(subclass)
23
+ # puts "Action.inherited called #{subclass} from #{to_s}"
24
+
25
+ subclass.instance_variable_set(:@obj_type, obj_type)
26
+
27
+ if subclass.name
28
+ # not an anonymouse class
29
+ delayed_inherited(subclass)
30
+ end
31
+ end
32
+
33
+ def delayed_inherited(subclass)
34
+ resource = Kernel.const_get(subclass.to_s.deconstantize)
35
+
36
+ inherit_attrs(subclass)
37
+ inherit_attrs_from_resource(subclass, resource, [:auth])
38
+
39
+ i = @input.clone
40
+ i.action = subclass
41
+
42
+ o = @output.clone
43
+ o.action = subclass
44
+
45
+ m = {}
46
+
47
+ @meta.each do |k,v|
48
+ m[k] = v && v.clone
49
+ next unless v
50
+ m[k].action = subclass
51
+ end
52
+
53
+ subclass.instance_variable_set(:@input, i)
54
+ subclass.instance_variable_set(:@output, o)
55
+ subclass.instance_variable_set(:@meta, m)
56
+
57
+ begin
58
+ subclass.instance_variable_set(:@resource, resource)
59
+ subclass.instance_variable_set(:@model, resource.model)
60
+ rescue NoMethodError
61
+ return
62
+ end
63
+ end
64
+
65
+ def initialize
66
+ return if @initialized
67
+
68
+ input.exec
69
+ model_adapter(input.layout).load_validators(model, input) if model
70
+
71
+ output.exec
72
+
73
+ model_adapter(input.layout).used_by(:input, self)
74
+ model_adapter(output.layout).used_by(:output, self)
75
+
76
+ if @meta
77
+ @meta.each_value do |m|
78
+ next unless m
79
+ m.input && m.input.exec
80
+ m.output && m.output.exec
81
+ end
82
+ end
83
+
84
+ @initialized = true
85
+ end
86
+
87
+ def model_adapter(layout)
88
+ ModelAdapter.for(layout, resource.model)
89
+ end
90
+
91
+ def input(layout = nil, namespace: nil, &block)
92
+ if block
93
+ @input ||= Params.new(:input, self)
94
+ @input.layout = layout
95
+ @input.namespace = namespace
96
+ @input.add_block(block)
97
+ else
98
+ @input
99
+ end
100
+ end
101
+
102
+ def output(layout = nil, namespace: nil, &block)
103
+ if block
104
+ @output ||= Params.new(:output, self)
105
+ @output.layout = layout
106
+ @output.namespace = namespace
107
+ @output.add_block(block)
108
+ else
109
+ @output
110
+ end
111
+ end
112
+
113
+ def meta(type = :object, &block)
114
+ if block
115
+ @meta ||= {object: nil, global: nil}
116
+ @meta[type] ||= Metadata::ActionMetadata.new
117
+ @meta[type].action = self
118
+ @meta[type].instance_exec(&block)
119
+ else
120
+ @meta[type]
121
+ end
122
+ end
123
+
124
+ def authorize(&block)
125
+ @authorization = Authorization.new(&block)
126
+ end
127
+
128
+ def example(title = '', &block)
129
+ @examples ||= []
130
+ e = Example.new(title)
131
+ e.instance_eval(&block)
132
+ @examples << e
133
+ end
134
+
135
+ def build_route(prefix)
136
+ route = @route || to_s.demodulize.underscore
137
+
138
+ if !route.is_a?(String) && route.respond_to?(:call)
139
+ route = route.call(self.resource)
140
+ end
141
+
142
+ prefix + route % {resource: self.resource.to_s.demodulize.underscore}
143
+ end
144
+
145
+ def describe(context)
146
+ authorization = (@authorization && @authorization.clone) || Authorization.new
147
+
148
+ return false if (context.endpoint || context.current_user) && !authorization.authorized?(context.current_user)
149
+
150
+ route_method = context.action.http_method.to_s.upcase
151
+ context.authorization = authorization
152
+
153
+ if context.endpoint
154
+ context.action_instance = context.action.from_context(context)
155
+
156
+ ret = catch(:return) do
157
+ context.action_prepare = context.action_instance.prepare
158
+ end
159
+
160
+ return false if ret == false
161
+ end
162
+
163
+ {
164
+ auth: @auth,
165
+ description: @desc,
166
+ aliases: @aliases,
167
+ input: @input ? @input.describe(context) : {parameters: {}},
168
+ output: @output ? @output.describe(context) : {parameters: {}},
169
+ meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
170
+ examples: @examples ? @examples.map { |e| e.describe } : [],
171
+ url: context.resolved_url,
172
+ method: route_method,
173
+ help: "#{context.url}?method=#{route_method}"
174
+ }
175
+ end
176
+
177
+ # Inherit attributes from resource action is defined in.
178
+ def inherit_attrs_from_resource(action, r, attrs)
179
+ begin
180
+ return unless r.obj_type == :resource
181
+
182
+ rescue NoMethodError
183
+ return
184
+ end
185
+
186
+ attrs.each do |attr|
187
+ action.method(attr).call(r.method(attr).call)
188
+ end
189
+ end
190
+
191
+ def from_context(c)
192
+ ret = new(nil, c.version, c.params, nil, c)
193
+ ret.instance_exec do
194
+ @safe_params = @params.dup
195
+ @authorization = c.authorization
196
+ @current_user = c.current_user
197
+ end
198
+
199
+ ret
200
+ end
201
+ end
202
+
203
+ def initialize(request, version, params, body, context)
204
+ @request = request
205
+ @version = version
206
+ @params = params
207
+ @params.update(body) if body
208
+ @context = context
209
+ @context.action = self.class
210
+ @context.action_instance = self
211
+ @metadata = {}
212
+ @reply_meta = {object: {}, global: {}}
213
+ @flags = {}
214
+
215
+ class_auth = self.class.authorization
216
+
217
+ if class_auth
218
+ @authorization = class_auth.clone
219
+ else
220
+ @authorization = Authorization.new {}
221
+ end
222
+ end
223
+
224
+ def validate!
225
+ begin
226
+ @params = validate
227
+ rescue ValidationError => e
228
+ error(e.message, e.to_hash)
229
+ end
230
+ end
231
+
232
+ def authorized?(user)
233
+ @current_user = user
234
+ @authorization.authorized?(user)
235
+ end
236
+
237
+ def current_user
238
+ @current_user
239
+ end
240
+
241
+ def params
242
+ @safe_params
243
+ end
244
+
245
+ def input
246
+ @safe_params[ self.class.input.namespace ] if self.class.input
247
+ end
248
+
249
+ def request
250
+ @request
251
+ end
252
+
253
+ def meta
254
+ @metadata
255
+ end
256
+
257
+ def set_meta(hash)
258
+ @reply_meta[:global].update(hash)
259
+ end
260
+
261
+ # Prepare object, set instance variables from URL parameters.
262
+ # This method should return queried object. If the method is
263
+ # not implemented or returns nil, action description will not
264
+ # contain link to an associated resource.
265
+ # --
266
+ # FIXME: is this correct behaviour?
267
+ # ++
268
+ def prepare
269
+
270
+ end
271
+
272
+ def pre_exec
273
+
274
+ end
275
+
276
+ # This method must be reimplemented in every action.
277
+ # It must not be invoked directly, only via safe_exec, which restricts output.
278
+ def exec
279
+ ['not implemented']
280
+ end
281
+
282
+ # Calls exec while catching all exceptions and restricting output only
283
+ # to what user can see.
284
+ # Return array +[status, data|error, errors]+
285
+ def safe_exec
286
+ ret = catch(:return) do
287
+ begin
288
+ validate!
289
+ prepare
290
+ pre_exec
291
+ exec
292
+ rescue Exception => e
293
+ tmp = call_class_hooks_as_for(Action, :exec_exception, args: [self, e])
294
+
295
+ if tmp.empty?
296
+ p e.message
297
+ puts e.backtrace
298
+ error('Server error occurred')
299
+ end
300
+
301
+ error(tmp[:message]) unless tmp[:status]
302
+ end
303
+ end
304
+
305
+ safe_output(ret)
306
+ end
307
+
308
+ def v?(v)
309
+ @version == v
310
+ end
311
+
312
+ def safe_output(ret)
313
+ if ret
314
+ output = self.class.output
315
+
316
+ if output
317
+ safe_ret = nil
318
+ adapter = self.class.model_adapter(output.layout)
319
+ out_params = self.class.output.params
320
+
321
+ case output.layout
322
+ when :object
323
+ out = adapter.output(@context, ret)
324
+ safe_ret = @authorization.filter_output(
325
+ out_params,
326
+ out,
327
+ true
328
+ )
329
+ @reply_meta[:global].update(out.meta)
330
+
331
+ when :object_list
332
+ safe_ret = []
333
+
334
+ ret.each do |obj|
335
+ out = adapter.output(@context, obj)
336
+
337
+ safe_ret << @authorization.filter_output(
338
+ out_params,
339
+ out,
340
+ true
341
+ )
342
+ safe_ret.last.update({Metadata.namespace => out.meta}) unless meta[:no]
343
+ end
344
+
345
+ when :hash
346
+ safe_ret = @authorization.filter_output(
347
+ out_params,
348
+ adapter.output(@context, ret),
349
+ true
350
+ )
351
+
352
+ when :hash_list
353
+ safe_ret = ret
354
+ safe_ret.map! do |hash|
355
+ @authorization.filter_output(
356
+ out_params,
357
+ adapter.output(@context, hash),
358
+ true
359
+ )
360
+ end
361
+
362
+ else
363
+ safe_ret = ret
364
+ end
365
+
366
+ ns = {output.namespace => safe_ret}
367
+ ns[Metadata.namespace] = @reply_meta[:global] unless meta[:no]
368
+
369
+ [true, ns]
370
+
371
+ else
372
+ [true, {}]
373
+ end
374
+
375
+ else
376
+ [false, @message, @errors]
377
+ end
378
+ end
379
+
380
+ input {}
381
+ output {}
382
+ meta(:global) do
383
+ input do
384
+ bool :no, label: 'Disable metadata'
385
+ end
386
+ end
387
+
388
+ protected
389
+ def with_restricted(*args)
390
+ if args.empty?
391
+ @authorization.restrictions
392
+ else
393
+ args.first.update(@authorization.restrictions)
394
+ end
395
+ end
396
+
397
+ # Convert parameter names to corresponding DB names.
398
+ # By default, input parameters are used for the translation.
399
+ def to_db_names(hash, src=:input)
400
+ return {} unless hash
401
+
402
+ params = self.class.method(src).call.params
403
+ ret = {}
404
+
405
+ hash.each do |k, v|
406
+ k = k.to_sym
407
+ hit = false
408
+
409
+ params.each do |p|
410
+ if k == p.name
411
+ ret[p.db_name] = v
412
+ hit = true
413
+ break
414
+ end
415
+ end
416
+
417
+ ret[k] = v unless hit
418
+ end
419
+
420
+ ret
421
+ end
422
+
423
+ # Convert DB names to corresponding parameter names.
424
+ # By default, output parameters are used for the translation.
425
+ def to_param_names(hash, src=:output)
426
+ return {} unless hash
427
+
428
+ params = self.class.method(src).call.params
429
+ ret = {}
430
+
431
+ hash.each do |k, v|
432
+ k = k.to_sym
433
+ hit = false
434
+
435
+ params.each do |p|
436
+ if k == p.db_name
437
+ ret[p.name] = v
438
+ hit = true
439
+ break
440
+ end
441
+ end
442
+
443
+ ret[k] = v unless hit
444
+ end
445
+
446
+ ret
447
+ end
448
+
449
+ def ok(ret={})
450
+ throw(:return, ret)
451
+ end
452
+
453
+ def error(msg, errs={})
454
+ @message = msg
455
+ @errors = errs
456
+ throw(:return, false)
457
+ end
458
+
459
+ private
460
+ def validate
461
+ # Validate standard input
462
+ @safe_params = @params.dup
463
+ input = self.class.input
464
+
465
+ if input
466
+ # First check layout
467
+ input.check_layout(@safe_params)
468
+
469
+ # Then filter allowed params
470
+ case input.layout
471
+ when :object_list, :hash_list
472
+ @safe_params[input.namespace].map! do |obj|
473
+ @authorization.filter_input(
474
+ self.class.input.params,
475
+ self.class.model_adapter(self.class.input.layout).input(obj))
476
+ end
477
+
478
+ else
479
+ @safe_params[input.namespace] = @authorization.filter_input(
480
+ self.class.input.params,
481
+ self.class.model_adapter(self.class.input.layout).input(@safe_params[input.namespace]))
482
+ end
483
+
484
+ # Remove duplicit key
485
+ @safe_params.delete(input.namespace.to_s)
486
+
487
+ # Now check required params, convert types and set defaults
488
+ input.validate(@safe_params)
489
+ end
490
+
491
+ # Validate metadata input
492
+ auth = Authorization.new { allow }
493
+ @metadata = {}
494
+
495
+ return if input && %i(object_list hash_list).include?(input.layout)
496
+
497
+ [:object, :global].each do |v|
498
+ meta = self.class.meta(v)
499
+ next unless meta
500
+
501
+ raw_meta = nil
502
+
503
+ [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
504
+ params = v == :object ? (@params[input.namespace] && @params[input.namespace][ns]) : @params[ns]
505
+ next unless params
506
+
507
+ raw_meta = auth.filter_input(
508
+ meta.input.params,
509
+ self.class.model_adapter(meta.input.layout).input(params)
510
+ )
511
+
512
+ break if raw_meta
513
+ end
514
+
515
+ next unless raw_meta
516
+
517
+ @metadata.update(meta.input.validate(raw_meta))
518
+ end
519
+ end
520
+ end
521
+ end
@@ -0,0 +1,55 @@
1
+ module HaveAPI
2
+ module Actions
3
+ module Default
4
+ class Index < Action
5
+ route ''
6
+ http_method :get
7
+ aliases %i(list)
8
+
9
+ meta(:global) do
10
+ input do
11
+ bool :count, label: 'Return the count of all items', default: false
12
+ end
13
+
14
+ output do
15
+ integer :total_count, label: 'Total count of all items'
16
+ end
17
+ end
18
+
19
+ include HaveAPI::Actions::Paginable
20
+
21
+ def pre_exec
22
+ set_meta(total_count: count) if meta[:count]
23
+ end
24
+
25
+ # Return the total count of items.
26
+ def count
27
+
28
+ end
29
+ end
30
+
31
+ class Create < Action
32
+ route ''
33
+ http_method :post
34
+ aliases %i(new)
35
+ end
36
+
37
+ class Show < Action
38
+ route ->(r){ r.singular ? '' : ':%{resource}_id' }
39
+ http_method :get
40
+ aliases %i(find)
41
+ end
42
+
43
+ class Update < Action
44
+ route ->(r){ r.singular ? '' : ':%{resource}_id' }
45
+ http_method :put
46
+ end
47
+
48
+ class Delete < Action
49
+ route ->(r){ r.singular ? '' : ':%{resource}_id' }
50
+ http_method :delete
51
+ aliases %i(destroy)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ module HaveAPI::Actions
2
+ module Paginable
3
+ def self.included(action)
4
+ action.input do
5
+ integer :offset, label: 'Offset', desc: 'The offset of the first object',
6
+ default: 0
7
+ integer :limit, label: 'Limit', desc: 'The number of objects to retrieve',
8
+ default: 25
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,66 @@
1
+ module HaveAPI
2
+ # Return a list of all resources or yield them if block is given.
3
+ def self.resources(module_name) # yields: resource
4
+ ret = []
5
+
6
+ module_name.constants.select do |c|
7
+ obj = module_name.const_get(c)
8
+
9
+ if obj.obj_type == :resource
10
+ if block_given?
11
+ yield obj
12
+ else
13
+ ret << obj
14
+ end
15
+ end
16
+ end
17
+
18
+ ret
19
+ end
20
+
21
+ # Iterate through all resources and return those for which yielded block
22
+ # returned true.
23
+ def self.filter_resources(module_name)
24
+ ret = []
25
+
26
+ resources(module_name) do |r|
27
+ ret << r if yield(r)
28
+ end
29
+
30
+ ret
31
+ end
32
+
33
+ # Return list of resources for version +v+.
34
+ def self.get_version_resources(module_name, v)
35
+ filter_resources(module_name) do |r|
36
+ r.version.is_a?(Array) ? r.version.include?(v) : (r.version == v || r.version == :all)
37
+ end
38
+ end
39
+
40
+ # Return a list of all API versions.
41
+ def self.get_versions(module_name)
42
+ ret = []
43
+
44
+ resources(module_name) do |r|
45
+ ret << r.version unless ret.include?(r.version)
46
+ end
47
+
48
+ ret
49
+ end
50
+
51
+ def self.set_module_name(name)
52
+ @module_name = name
53
+ end
54
+
55
+ def self.module_name
56
+ @module_name
57
+ end
58
+
59
+ def self.set_default_authenticate(chain)
60
+ @default_auth = chain
61
+ end
62
+
63
+ def self.default_authenticate
64
+ @default_auth
65
+ end
66
+ end
@@ -0,0 +1,37 @@
1
+ module HaveAPI
2
+ module Authentication
3
+ # Base class for authentication providers.
4
+ class Base
5
+ attr_accessor :name, :resources
6
+
7
+ def initialize(server, v)
8
+ @server = server
9
+ @version = v
10
+ setup
11
+ end
12
+
13
+ # Reimplement this method in your authentication provider.
14
+ # +request+ is passed directly from Sinatra.
15
+ def authenticate(request)
16
+
17
+ end
18
+
19
+ # Reimplement to describe provider.
20
+ def describe
21
+ {}
22
+ end
23
+
24
+ protected
25
+ # Called during API mount.
26
+ def setup
27
+
28
+ end
29
+
30
+ # Immediately return from authentication chain.
31
+ # User is not allowed to authenticate.
32
+ def deny
33
+ throw(:return)
34
+ end
35
+ end
36
+ end
37
+ end