haveapi 0.3.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 (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