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
@@ -0,0 +1,352 @@
1
+ module HaveAPI::ModelAdapters
2
+ # Adapter for ActiveRecord models.
3
+ class ActiveRecord < ::HaveAPI::ModelAdapter
4
+ register
5
+
6
+ def self.handle?(layout, klass)
7
+ klass < ::ActiveRecord::Base && %i(object object_list).include?(layout)
8
+ end
9
+
10
+ def self.load_validators(model, params)
11
+ tr = ValidatorTranslator.new(params.params)
12
+
13
+ model.validators.each do |validator|
14
+ tr.translate(validator)
15
+ end
16
+ end
17
+
18
+ module Action
19
+ module InstanceMethods
20
+ # Helper method that sets correct ActiveRecord includes
21
+ # according to the meta includes sent by the user.
22
+ # +q+ is the model or partial AR query. If not set,
23
+ # action's model class is used instead.
24
+ def with_includes(q = nil)
25
+ q ||= self.class.model
26
+ includes = meta && meta[:includes]
27
+ args = includes.nil? ? [] : ar_parse_includes(includes)
28
+
29
+ # Resulting includes may still contain duplicities in form of nested
30
+ # includes. ar_default_includes returns a flat array where as
31
+ # ar_parse_includes may contain hashes. But since ActiveRecord is taking
32
+ # it well, it is not necessary to fix.
33
+ args.concat(ar_default_includes).uniq
34
+
35
+ if !args.empty?
36
+ q.includes(*args)
37
+ else
38
+ q
39
+ end
40
+ end
41
+
42
+ # Parse includes sent by the user and return them
43
+ # in an array of symbols and hashes.
44
+ def ar_parse_includes(raw)
45
+ return @ar_parsed_includes if @ar_parsed_includes
46
+ @ar_parsed_includes = ar_inner_includes(raw)
47
+ end
48
+
49
+ # Called by ar_parse_includes for recursion purposes.
50
+ def ar_inner_includes(includes)
51
+ args = []
52
+
53
+ includes.each do |assoc|
54
+ if assoc.index('__')
55
+ tmp = {}
56
+ parts = assoc.split('__')
57
+ tmp[parts.first.to_sym] = ar_inner_includes(parts[1..-1])
58
+
59
+ args << tmp
60
+ else
61
+ args << assoc.to_sym
62
+ end
63
+ end
64
+
65
+ args
66
+ end
67
+
68
+ # Default includes contain all associated resources specified
69
+ # inaction output parameters. They are fetched from the database
70
+ # anyway, to return the label for even unresolved association.
71
+ def ar_default_includes
72
+ ret = []
73
+
74
+ self.class.output.params.each do |p|
75
+ if p.is_a?(HaveAPI::Parameters::Resource) && self.class.model.reflections[p.name.to_sym]
76
+ ret << p.name.to_sym
77
+ end
78
+ end
79
+
80
+ ret
81
+ end
82
+ end
83
+ end
84
+
85
+ class Input < ::HaveAPI::ModelAdapter::Input
86
+ def self.clean(model, raw, extra)
87
+ return if (raw.is_a?(String) && raw.empty?) || (!raw.is_a?(String) && !raw)
88
+
89
+ if extra[:fetch]
90
+ model.instance_exec(raw, &extra[:fetch])
91
+ else
92
+ model.find(raw)
93
+ end
94
+ end
95
+ end
96
+
97
+ class Output < ::HaveAPI::ModelAdapter::Output
98
+ def self.used_by(action)
99
+ action.meta(:object) do
100
+ output do
101
+ custom :url_params, label: 'URL parameters',
102
+ desc: 'An array of parameters needed to resolve URL to this object'
103
+ bool :resolved, label: 'Resolved', desc: 'True if the association is resolved'
104
+ end
105
+ end
106
+
107
+ if %i(object object_list).include?(action.input.layout)
108
+ clean = Proc.new do |raw|
109
+ if raw.is_a?(String)
110
+ raw.strip.split(',')
111
+ elsif raw.is_a?(Array)
112
+ raw
113
+ else
114
+ nil
115
+ end
116
+ end
117
+
118
+ desc = <<END
119
+ A list of names of associated resources separated by a comma.
120
+ Nested associations are declared with '__' between resource names.
121
+ For example, 'user,node' will resolve the two associations.
122
+ To resolve further associations of node, use e.g. 'user,node__location',
123
+ to go even deeper, use e.g. 'user,node__location__environment'.
124
+ END
125
+
126
+ action.meta(:global) do
127
+ input do
128
+ custom :includes, label: 'Included associations',
129
+ desc: desc, &clean
130
+ end
131
+ end
132
+
133
+ action.send(:include, Action::InstanceMethods)
134
+ end
135
+ end
136
+
137
+ def has_param?(name)
138
+ param = @context.action.output[name]
139
+ param && @object.respond_to?(param.db_name)
140
+ end
141
+
142
+ def [](name)
143
+ param = @context.action.output[name]
144
+ v = @object.send(param.db_name)
145
+
146
+ if v.is_a?(::ActiveRecord::Base)
147
+ resourcify(param, v)
148
+ else
149
+ v
150
+ end
151
+ end
152
+
153
+ def meta
154
+ params = @context.action.resolve.call(@object)
155
+
156
+ {
157
+ url_params: params.is_a?(Array) ? params : [params],
158
+ resolved: true
159
+ }
160
+ end
161
+
162
+ protected
163
+ # Return representation of an associated resource +param+
164
+ # with its instance in +val+.
165
+ #
166
+ # By default, it returns an unresolved resource, which contains
167
+ # only object id and label. Resource will be resolved
168
+ # if it is set in meta includes field.
169
+ def resourcify(param, val)
170
+ res_show = param.show_action
171
+ res_output = res_show.output
172
+
173
+ args = res_show.resolve.call(val)
174
+
175
+ if includes_include?(param.name)
176
+ push_cls = @context.action
177
+ push_ins = @context.action_instance
178
+
179
+ pass_includes = includes_pass_on_to(param.name)
180
+
181
+ show = res_show.new(
182
+ push_ins.request,
183
+ push_ins.version,
184
+ {},
185
+ nil,
186
+ @context
187
+ )
188
+ show.meta[:includes] = pass_includes
189
+
190
+ # This flag is used to tell the action that it is being used
191
+ # as a nested association, that it wasn't called directly by the user.
192
+ show.flags[:inner_assoc] = true
193
+
194
+ show.authorized?(push_ins.current_user) # FIXME: handle false
195
+
196
+ ret = show.safe_output(val)
197
+
198
+ @context.action_instance = push_ins
199
+ @context.action = push_cls
200
+
201
+ fail "#{res_show.to_s} resolve failed" unless ret[0]
202
+
203
+ ret[1][res_show.output.namespace].update({
204
+ _meta: ret[1][:_meta].update(resolved: true)
205
+ })
206
+
207
+ else
208
+ {
209
+ param.value_id => val.send(res_output[param.value_id].db_name),
210
+ param.value_label => val.send(res_output[param.value_label].db_name),
211
+ _meta: {
212
+ :url_params => args.is_a?(Array) ? args : [args],
213
+ :resolved => false
214
+ }
215
+ }
216
+ end
217
+ end
218
+
219
+ # Should an association with +name+ be resolved?
220
+ def includes_include?(name)
221
+ includes = @context.action_instance.meta[:includes]
222
+ return unless includes
223
+
224
+ name = name.to_sym
225
+
226
+ if @context.action_instance.flags[:inner_assoc]
227
+ # This action is called as an association of parent resource.
228
+ # Meta includes are already parsed and can be accessed directly.
229
+ includes.each do |v|
230
+ if v.is_a?(::Hash)
231
+ return true if v.has_key?(name)
232
+ else
233
+ return true if v == name
234
+ end
235
+ end
236
+
237
+ false
238
+
239
+ else
240
+ # This action is the one that was called by the user.
241
+ # Meta includes contains an array of strings as was sent
242
+ # by the user. The parsed includes must be fetched from
243
+ # the action itself.
244
+ includes = @context.action_instance.ar_parse_includes([])
245
+
246
+ includes.each do |v|
247
+ if v.is_a?(::Hash)
248
+ return true if v.has_key?(name)
249
+ else
250
+ return true if v == name
251
+ end
252
+ end
253
+
254
+ false
255
+ end
256
+ end
257
+
258
+ # Create an array of includes that is passed to child association.
259
+ def includes_pass_on_to(assoc)
260
+ parsed = if @context.action_instance.flags[:inner_assoc]
261
+ @context.action_instance.meta[:includes]
262
+ else
263
+ @context.action_instance.ar_parse_includes([])
264
+ end
265
+
266
+ ret = []
267
+
268
+ parsed.each do |v|
269
+ if v.is_a?(::Hash)
270
+ v.each { |k, v| ret << v if k == assoc }
271
+ end
272
+ end
273
+
274
+ ret.flatten(1)
275
+ end
276
+ end
277
+
278
+ class ValidatorTranslator
279
+ class << self
280
+ attr_reader :handlers
281
+
282
+ def handle(validator, &block)
283
+ @handlers ||= {}
284
+ @handlers[validator] = block
285
+ end
286
+ end
287
+
288
+ handle ::ActiveRecord::Validations::PresenceValidator do |v|
289
+ validator({present: true})
290
+ end
291
+
292
+ handle ::ActiveModel::Validations::AbsenceValidator do |v|
293
+ validator({absent: true})
294
+ end
295
+
296
+ handle ::ActiveModel::Validations::ExclusionValidator do |v|
297
+ validator(v.options)
298
+ end
299
+
300
+ handle ::ActiveModel::Validations::FormatValidator do |v|
301
+ validator({format: {with_source: v.options[:with].source}.update(v.options)})
302
+ end
303
+
304
+ handle ::ActiveModel::Validations::InclusionValidator do |v|
305
+ validator(v.options)
306
+ end
307
+
308
+ handle ::ActiveModel::Validations::LengthValidator do |v|
309
+ validator(v.options)
310
+ end
311
+
312
+ handle ::ActiveModel::Validations::NumericalityValidator do |v|
313
+ validator(v.options)
314
+ end
315
+
316
+ handle ::ActiveRecord::Validations::UniquenessValidator do |v|
317
+ validator(v.options)
318
+ end
319
+
320
+ def initialize(params)
321
+ @params = params
322
+ end
323
+
324
+ def validator_for(param, v)
325
+ @params.each do |p|
326
+ next unless p.is_a?(::HaveAPI::Parameters::Param)
327
+
328
+ if p.db_name == param
329
+ p.add_validator(v)
330
+ break
331
+ end
332
+ end
333
+ end
334
+
335
+ def validator(v)
336
+ validator_for(@attr, v)
337
+ end
338
+
339
+ def translate(v)
340
+ self.class.handlers.each do |klass, translator|
341
+ if v.is_a?(klass)
342
+ v.attributes.each do |attr|
343
+ @attr = attr
344
+ instance_exec(v, &translator)
345
+ end
346
+ break
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,27 @@
1
+ module HaveAPI::ModelAdapters
2
+ # Simple hash adapter. Model is just a hash of parameters
3
+ # and their values.
4
+ class Hash < ::HaveAPI::ModelAdapter
5
+ register
6
+
7
+ def self.handle?(layout, klass)
8
+ klass.is_a?(::Hash)
9
+ end
10
+
11
+ class Input < ::HaveAPI::ModelAdapter::Input
12
+ def self.clean(model, raw)
13
+ raw
14
+ end
15
+ end
16
+
17
+ class Output < ::HaveAPI::ModelAdapter::Output
18
+ def has_param?(name)
19
+ @object.has_key?(name)
20
+ end
21
+
22
+ def [](name)
23
+ @object[name]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module HaveAPI
2
+ module OutputFormatters
3
+
4
+ end
5
+
6
+ class OutputFormatter
7
+ class << self
8
+ attr_reader :formatters
9
+
10
+ def register(klass)
11
+ @formatters ||= []
12
+ @formatters << klass
13
+ end
14
+ end
15
+
16
+ def supports?(types)
17
+ @formatter = nil
18
+
19
+ if types.empty?
20
+ return @formatter = self.class.formatters.first.new
21
+ end
22
+
23
+ types.each do |type|
24
+ self.class.formatters.each do |f|
25
+ if f.handle?(type)
26
+ @formatter = f.new
27
+ break
28
+ end
29
+ end
30
+ end
31
+
32
+ @formatter.nil? ? false : true
33
+ end
34
+
35
+ def format(status, response, message = nil, errors = nil)
36
+ @formatter.format(header(status, response, message, errors))
37
+ end
38
+
39
+ def error(msg)
40
+ @formatter.format(header(false, nil, msg))
41
+ end
42
+
43
+ def content_type
44
+ @formatter.content_type
45
+ end
46
+
47
+ protected
48
+ def header(status, response, message = nil, errors = nil)
49
+ {
50
+ status: status,
51
+ response: response,
52
+ message: message,
53
+ errors: errors
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ module HaveAPI::OutputFormatters
2
+ class BaseFormatter
3
+ class << self
4
+ attr_reader :types
5
+
6
+ def handle(*args)
7
+ @types ||= []
8
+ @types += args
9
+
10
+ HaveAPI::OutputFormatter.register(Kernel.const_get(self.to_s)) unless @registered
11
+ @registered = true
12
+ end
13
+
14
+ def handle?(type)
15
+ @types.detect do |t|
16
+ File.fnmatch(type, t)
17
+ end
18
+ end
19
+ end
20
+
21
+ def content_type
22
+ self.class.types.first
23
+ end
24
+
25
+ def format(response)
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module HaveAPI::OutputFormatters
2
+ class Json < BaseFormatter
3
+ handle 'application/json'
4
+
5
+ def format(response)
6
+ JSON.pretty_generate(response)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ module HaveAPI::Parameters
2
+ class Param
3
+ attr_reader :name, :label, :desc, :type, :default
4
+
5
+ def initialize(name, required: nil, label: nil, desc: nil, type: nil,
6
+ choices: nil, db_name: nil, default: :_nil, fill: false,
7
+ clean: nil)
8
+ @required = required
9
+ @name = name
10
+ @label = label || name.to_s.capitalize
11
+ @desc = desc
12
+ @type = type
13
+ @choices = choices
14
+ @db_name = db_name
15
+ @default = default
16
+ @fill = fill
17
+ @layout = :custom
18
+ @validators = {}
19
+ @clean = clean
20
+ end
21
+
22
+ def db_name
23
+ @db_name || @name
24
+ end
25
+
26
+ def required?
27
+ @required
28
+ end
29
+
30
+ def optional?
31
+ !@required
32
+ end
33
+
34
+ def fill?
35
+ @fill
36
+ end
37
+
38
+ def add_validator(v)
39
+ @validators.update(v)
40
+ end
41
+
42
+ def validators
43
+ @validators
44
+ end
45
+
46
+ def describe(context)
47
+ {
48
+ required: required?,
49
+ label: @label,
50
+ description: @desc,
51
+ type: @type ? @type.to_s : String.to_s,
52
+ choices: @choices,
53
+ validators: @validators,
54
+ default: @default
55
+ }
56
+ end
57
+
58
+ def patch(attrs)
59
+ attrs.each { |k, v| instance_variable_set("@#{k}", v) }
60
+ end
61
+
62
+ def clean(raw)
63
+ return instance_exec(raw, &@clean) if @clean
64
+
65
+ val = if raw.nil?
66
+ @default
67
+
68
+ elsif @type.nil?
69
+ nil
70
+
71
+ elsif @type == Integer
72
+ raw.to_i
73
+
74
+ elsif @type == Boolean
75
+ Boolean.to_b(raw)
76
+
77
+ elsif @type == ::Datetime
78
+ begin
79
+ Time.iso8601(raw)
80
+
81
+ rescue ArgumentError
82
+ raise HaveAPI::ValidationError.new("not in ISO 8601 format '#{raw}'")
83
+ end
84
+
85
+ else
86
+ raw
87
+ end
88
+
89
+ if @choices
90
+ if @choices.is_a?(Array)
91
+ unless @choices.include?(val) || @choices.include?(val.to_s.to_sym)
92
+ raise HaveAPI::ValidationError.new("invalid choice '#{raw}'")
93
+ end
94
+
95
+ elsif @choices.is_a?(Hash)
96
+ unless @choices.has_key?(val) || @choices.has_key?(val.to_s.to_sym)
97
+ raise HaveAPI::ValidationError.new("invalid choice '#{raw}'")
98
+ end
99
+ end
100
+ end
101
+
102
+ val
103
+ end
104
+
105
+ def format_output(v)
106
+ if @type == ::Datetime && v.is_a?(Time)
107
+ v.iso8601
108
+
109
+ else
110
+ v
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,109 @@
1
+ module HaveAPI::Parameters
2
+ class Resource
3
+ attr_reader :name, :resource, :label, :desc, :type, :value_id, :value_label,
4
+ :choices, :value_params
5
+
6
+ def initialize(resource, name: nil, label: nil, desc: nil,
7
+ choices: nil, value_id: :id, value_label: :label, required: nil,
8
+ db_name: nil, fetch: nil)
9
+ @resource = resource
10
+ @resource_path = build_resource_path(resource)
11
+ @name = name || resource.to_s.demodulize.underscore.to_sym
12
+ @label = label || (name && name.to_s.capitalize) || resource.to_s.demodulize
13
+ @desc = desc
14
+ @choices = choices || @resource::Index
15
+ @value_id = value_id
16
+ @value_label = value_label
17
+ @required = required
18
+ @db_name = db_name
19
+ @extra = {
20
+ fetch: fetch
21
+ }
22
+ end
23
+
24
+ def db_name
25
+ @db_name || @name
26
+ end
27
+
28
+ def required?
29
+ @required
30
+ end
31
+
32
+ def optional?
33
+ !@required
34
+ end
35
+
36
+ def show_action
37
+ @resource::Show
38
+ end
39
+
40
+ def describe(context)
41
+ val_url = context.url_for(
42
+ @resource::Show,
43
+ context.endpoint && context.action_prepare && context.layout == :object && context.call_url_params(context.action, context.action_prepare)
44
+ )
45
+ val_method = @resource::Index.http_method.to_s.upcase
46
+
47
+ choices_url = context.url_for(
48
+ @choices,
49
+ context.endpoint && context.layout == :object && context.call_url_params(context.action, context.action_prepare)
50
+ )
51
+ choices_method = @choices.http_method.to_s.upcase
52
+
53
+ {
54
+ required: required?,
55
+ label: @label,
56
+ description: @desc,
57
+ type: 'Resource',
58
+ resource: @resource_path,
59
+ value_id: @value_id,
60
+ value_label: @value_label,
61
+ value: context.action_prepare && {
62
+ url: val_url,
63
+ method: val_method,
64
+ help: "#{val_url}?method=#{val_method}",
65
+ },
66
+ choices: {
67
+ url: choices_url,
68
+ method: choices_method,
69
+ help: "#{choices_url}?method=#{choices_method}"
70
+ }
71
+ }
72
+ end
73
+
74
+ def patch(attrs)
75
+ attrs.each { |k, v| instance_variable_set("@#{k}", v) }
76
+ end
77
+
78
+ def clean(raw)
79
+ ::HaveAPI::ModelAdapter.for(
80
+ show_action.input.layout, @resource.model
81
+ ).input_clean(@resource.model, raw, @extra)
82
+ end
83
+
84
+ def format_output(v)
85
+ v
86
+ end
87
+
88
+ private
89
+ def build_resource_path(r)
90
+ path = []
91
+ top_module = Kernel
92
+
93
+ r.to_s.split('::').each do |name|
94
+ top_module = top_module.const_get(name)
95
+
96
+ begin
97
+ top_module.obj_type
98
+
99
+ rescue NoMethodError
100
+ next
101
+ end
102
+
103
+ path << name.underscore
104
+ end
105
+
106
+ path
107
+ end
108
+ end
109
+ end