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
@@ -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