komoju 0.0.0 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,444 @@
1
+ module Heroics
2
+ # A wrapper around a bare JSON schema to make it easier to use.
3
+ class Schema
4
+ attr_reader :schema
5
+
6
+ # Instantiate a schema.
7
+ #
8
+ # @param schema [Hash] The bare JSON schema to wrap.
9
+ def initialize(schema)
10
+ @schema = schema
11
+ @resources = {}
12
+ @schema['properties'].each do |key, value|
13
+ @resources[key] = ResourceSchema.new(@schema, key)
14
+ end
15
+ end
16
+
17
+ # A description of the API.
18
+ def description
19
+ @schema['description']
20
+ end
21
+
22
+ # Get a schema for a named resource.
23
+ #
24
+ # @param name [String] The name of the resource.
25
+ # @raise [SchemaError] Raised if an unknown resource name is provided.
26
+ def resource(name)
27
+ if @schema['definitions'].has_key?(name)
28
+ ResourceSchema.new(@schema, name)
29
+ else
30
+ raise SchemaError.new("Unknown resource '#{name}'.")
31
+ end
32
+ end
33
+
34
+ # The resource schema children that are part of this schema.
35
+ #
36
+ # @return [Array<ResourceSchema>] The resource schema children.
37
+ def resources
38
+ @resources.values
39
+ end
40
+
41
+ # Get a simple human-readable representation of this client instance.
42
+ def inspect
43
+ "#<Heroics::Schema description=\"#{@schema['description']}\">"
44
+ end
45
+ alias to_s inspect
46
+ end
47
+
48
+ # A wrapper around a bare resource element in a JSON schema to make it
49
+ # easier to use.
50
+ class ResourceSchema
51
+ attr_reader :name
52
+
53
+ # Instantiate a resource schema.
54
+ #
55
+ # @param schema [Hash] The bare JSON schema to wrap.
56
+ # @param name [String] The name of the resource to identify in the schema.
57
+ def initialize(schema, name)
58
+ @schema = schema
59
+ @name = name
60
+ link_schema = schema['definitions'][name]['links'] || []
61
+
62
+ @links = Hash[link_schema.each_with_index.map do |link, link_index|
63
+ link_name = Heroics.ruby_name(link['title'])
64
+ [link_name, LinkSchema.new(schema, name, link_index)]
65
+ end]
66
+ end
67
+
68
+ # A description of the resource.
69
+ def description
70
+ @schema['definitions'][name]['description']
71
+ end
72
+
73
+ # Get a schema for a named link.
74
+ #
75
+ # @param name [String] The name of the link.
76
+ # @raise [SchemaError] Raised if an unknown link name is provided.
77
+ def link(name)
78
+ schema = @links[name]
79
+ raise SchemaError.new("Unknown link '#{name}'.") unless schema
80
+ schema
81
+ end
82
+
83
+ # The link schema children that are part of this resource schema.
84
+ #
85
+ # @return [Array<LinkSchema>] The link schema children.
86
+ def links
87
+ @links.values
88
+ end
89
+ end
90
+
91
+ # A wrapper around a bare link element for a resource in a JSON schema to
92
+ # make it easier to use.
93
+ class LinkSchema
94
+ attr_reader :name, :resource_name, :description
95
+
96
+ # Instantiate a link schema.
97
+ #
98
+ # @param schema [Hash] The bare JSON schema to wrap.
99
+ # @param resource_name [String] The name of the resource to identify in
100
+ # the schema.
101
+ # @param link_index [Fixnum] The index of the link in the resource schema.
102
+ def initialize(schema, resource_name, link_index)
103
+ @schema = schema
104
+ @resource_name = resource_name
105
+ @link_index = link_index
106
+ @name = Heroics.ruby_name(link_schema['title'])
107
+ @description = link_schema['description']
108
+ end
109
+
110
+ # Get the resource name in pretty form.
111
+ #
112
+ # @return [String] The pretty resource name.
113
+ def pretty_resource_name
114
+ Heroics.pretty_name(resource_name)
115
+ end
116
+
117
+ # Get the link name in pretty form.
118
+ #
119
+ # @return [String] The pretty link name.
120
+ def pretty_name
121
+ Heroics.pretty_name(name)
122
+ end
123
+
124
+ # Get the HTTP method for this link.
125
+ #
126
+ # @return [Symbol] The HTTP method.
127
+ def method
128
+ link_schema['method'].downcase.to_sym
129
+ end
130
+
131
+ # Get the Content-Type for this link.
132
+ #
133
+ # @return [String] The Content-Type value
134
+ def content_type
135
+ link_schema['encType'] || 'application/json'
136
+ end
137
+
138
+ def encode(body)
139
+ case content_type
140
+ when 'application/x-www-form-urlencoded'
141
+ URI.encode_www_form(body)
142
+ when /application\/.*json/
143
+ MultiJson.dump(body)
144
+ end
145
+ end
146
+
147
+ # Get the names of the parameters this link expects.
148
+ #
149
+ # @return [Array<String>] The parameters.
150
+ def parameters
151
+ parameter_names = link_schema['href'].scan(PARAMETER_REGEX)
152
+ resolve_parameters(parameter_names)
153
+ end
154
+
155
+ # Get the names and descriptions of the parameters this link expects.
156
+ #
157
+ # @return [Hash<String, String>] A list of hashes with `name` and
158
+ # `description` key/value pairs describing parameters.
159
+ def parameter_details
160
+ parameter_names = link_schema['href'].scan(PARAMETER_REGEX)
161
+ parameters = resolve_parameter_details(parameter_names)
162
+ parameters << CollectionOptions.new if requires_collection_options?
163
+ parameters << BodyParameter.new if requires_request_body?
164
+ parameters
165
+ end
166
+
167
+ # Get an example request body.
168
+ #
169
+ # @return [Hash] A sample request body.
170
+ def example_body
171
+ if body_schema = link_schema['schema']
172
+ definitions = @schema['definitions'][@resource_name]['definitions']
173
+ Hash[body_schema['properties'].keys.map do |property|
174
+ # FIXME This is wrong! -jkakar
175
+ if definitions.has_key?(property)
176
+ example = definitions[property]['example']
177
+ else
178
+ example = ''
179
+ end
180
+ [property, example]
181
+ end]
182
+ end
183
+ end
184
+
185
+ # Inject parameters into the link href and return the body, if it exists.
186
+ #
187
+ # @param parameters [Array] The list of parameters to inject into the
188
+ # path.
189
+ # @raise [ArgumentError] Raised if either too many or too few parameters
190
+ # were provided.
191
+ # @return [String,Object] A path and request body pair. The body value is
192
+ # nil if a payload wasn't included in the list of parameters.
193
+ def format_path(parameters)
194
+ path = link_schema['href']
195
+ parameter_size = path.scan(PARAMETER_REGEX).size
196
+ too_few_parameters = parameter_size > parameters.size
197
+ # FIXME We should use the schema to detect when a request body is
198
+ # permitted and do the calculation correctly here. -jkakar
199
+ too_many_parameters = parameter_size < (parameters.size - 1)
200
+ if too_few_parameters || too_many_parameters
201
+ raise ArgumentError.new("wrong number of arguments " +
202
+ "(#{parameters.size} for #{parameter_size})")
203
+ end
204
+
205
+ (0..parameter_size).each do |i|
206
+ path = path.sub(PARAMETER_REGEX, format_parameter(parameters[i]))
207
+ end
208
+ body = parameters.slice(parameter_size)
209
+ return path, body
210
+ end
211
+
212
+ private
213
+
214
+ # Match parameters in definition strings.
215
+ PARAMETER_REGEX = /\{\([%\/a-zA-Z0-9_-]*\)\}/
216
+
217
+ # Get the raw link schema.
218
+ #
219
+ # @param [Hash] The raw link schema.
220
+ def link_schema
221
+ @schema['definitions'][@resource_name]['links'][@link_index]
222
+ end
223
+
224
+ def requires_collection_options?
225
+ return link_schema['rel'] == 'instances'
226
+ end
227
+
228
+ def requires_request_body?
229
+ return link_schema.has_key?('schema')
230
+ end
231
+
232
+ # Get the names of the parameters this link expects.
233
+ #
234
+ # @param parameters [Array] The names of the parameter definitions to
235
+ # convert to parameter names.
236
+ # @return [Array<String>] The parameters.
237
+ def resolve_parameters(parameters)
238
+ properties = @schema['definitions'][@resource_name]['properties']
239
+ return [''] if properties.nil?
240
+ definitions = Hash[properties.each_pair.map do |key, value|
241
+ [value['$ref'], key]
242
+ end]
243
+ parameters.map do |parameter|
244
+ definition_name = URI.unescape(parameter[2..-3])
245
+ if definitions.has_key?(definition_name)
246
+ definitions[definition_name]
247
+ else
248
+ definition_name = definition_name.split('/')[-1]
249
+ resource_definitions = @schema[
250
+ 'definitions'][@resource_name]['definitions'][definition_name]
251
+ if resource_definitions.has_key?('anyOf')
252
+ resource_definitions['anyOf'].map do |property|
253
+ definitions[property['$ref']]
254
+ end.join('|')
255
+ else
256
+ resource_definitions['oneOf'].map do |property|
257
+ definitions[property['$ref']]
258
+ end.join('|')
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Get the parameters this link expects.
265
+ #
266
+ # @param parameters [Array] The names of the parameter definitions to
267
+ # convert to parameter names.
268
+ # @return [Array<Parameter|ParameterChoice>] A list of parameter instances
269
+ # that represent parameters to be injected into the link URL.
270
+ def resolve_parameter_details(parameters)
271
+ parameters.map do |parameter|
272
+ # URI decode parameters and strip the leading '{(' and trailing ')}'.
273
+ parameter = URI.unescape(parameter[2..-3])
274
+
275
+ # Split the path into components and discard the leading '#' that
276
+ # represents the root of the schema.
277
+ path = parameter.split('/')[1..-1]
278
+ info = lookup_parameter(path, @schema)
279
+ # The reference can be one of several values.
280
+ resource_name = path[1].gsub('-', '_')
281
+ if info.has_key?('anyOf')
282
+ ParameterChoice.new(resource_name,
283
+ unpack_multiple_parameters(info['anyOf']))
284
+ elsif info.has_key?('oneOf')
285
+ ParameterChoice.new(resource_name,
286
+ unpack_multiple_parameters(info['oneOf']))
287
+ else
288
+ name = path[-1]
289
+ Parameter.new(resource_name, name, info['description'])
290
+ end
291
+ end
292
+ end
293
+
294
+ # Unpack an 'anyOf' or 'oneOf' multi-parameter blob.
295
+ #
296
+ # @param parameters [Array<Hash>] An array of hashes containing '$ref'
297
+ # keys and definition values.
298
+ # @return [Array<Parameter>] An array of parameters extracted from the
299
+ # blob.
300
+ def unpack_multiple_parameters(parameters)
301
+ parameters.map do |info|
302
+ parameter = info['$ref']
303
+ path = parameter.split('/')[1..-1]
304
+ info = lookup_parameter(path, @schema)
305
+ resource_name = path.size > 2 ? path[1].gsub('-', '_') : nil
306
+ name = path[-1]
307
+ Parameter.new(resource_name, name, info['description'])
308
+ end
309
+ end
310
+
311
+ # Recursively walk the object hierarchy in the schema to resolve a given
312
+ # path. This is used to find property information related to definitions
313
+ # in link hrefs.
314
+ #
315
+ # @param path [Array<String>] An array of paths to walk, such as
316
+ # ['definitions', 'resource', 'definitions', 'property'].
317
+ # @param schema [Hash] The schema to walk.
318
+ def lookup_parameter(path, schema)
319
+ key = path[0]
320
+ remaining = path[1..-1]
321
+ if remaining.empty?
322
+ return schema[key]
323
+ else
324
+ lookup_parameter(remaining, schema[key])
325
+ end
326
+ end
327
+
328
+ # Convert a path parameter to a format suitable for use in a path.
329
+ #
330
+ # @param [Fixnum,String,TrueClass,FalseClass,Time] The parameter to format.
331
+ # @return [String] The formatted parameter.
332
+ def format_parameter(parameter)
333
+ formatted_parameter = parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
334
+ URI.escape formatted_parameter
335
+ end
336
+
337
+ # Convert a time to an ISO 8601 combined data and time format.
338
+ #
339
+ # @param time [Time] The time to convert to ISO 8601 format.
340
+ # @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
341
+ def iso_format(time)
342
+ time.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
343
+ end
344
+ end
345
+
346
+ # Download a JSON schema from a URL.
347
+ #
348
+ # @param url [String] The URL for the schema.
349
+ # @param options [Hash] Configuration for links. Possible keys include:
350
+ # - default_headers: Optionally, a set of headers to include in every
351
+ # request made by the client. Default is no custom headers.
352
+ # @return [Schema] The downloaded JSON schema.
353
+ def self.download_schema(url, options={})
354
+ default_headers = options.fetch(:default_headers, {})
355
+ response = Excon.get(url, headers: default_headers, expects: [200, 201])
356
+ Schema.new(MultiJson.load(response.body))
357
+ end
358
+
359
+ # The base parameter class
360
+ class BaseParameter
361
+ attr_reader :resource_name, :name, :description
362
+
363
+ # This is the used for generating the function signature
364
+ def signature
365
+ [@resource_name, @name].compact.join("_")
366
+ end
367
+
368
+ # A pretty representation of a parameter instance
369
+ def inspect
370
+ "#{self.class}(name=#{@name}, description=#{@description})"
371
+ end
372
+ end
373
+
374
+ # A representation of a parameter.
375
+ class Parameter < BaseParameter
376
+ def initialize(resource_name, name, description)
377
+ @resource_name = resource_name
378
+ @name = name
379
+ @description = description
380
+ end
381
+
382
+ # The name of the parameter, with the resource included, suitable for use
383
+ # in a function signature.
384
+ def name
385
+ [@resource_name, @name].compact.join("_")
386
+ end
387
+ end
388
+
389
+ # A representation of a body parameter.
390
+ class BodyParameter < BaseParameter
391
+ def initialize
392
+ @name = 'body'
393
+ @description = 'the object to pass as the request payload'
394
+ end
395
+ end
396
+
397
+ # Additional options to pass with a request
398
+ class CollectionOptions < BaseParameter
399
+ def initialize
400
+ @name = 'collection_options'
401
+ @description = 'additional collection options to pass with the request'
402
+ end
403
+
404
+ def signature
405
+ "#{@name} = {}"
406
+ end
407
+ end
408
+
409
+ # A representation of a set of parameters.
410
+ class ParameterChoice < BaseParameter
411
+ attr_reader :parameters
412
+
413
+ def initialize(resource_name, parameters)
414
+ @resource_name = resource_name
415
+ @parameters = parameters
416
+ end
417
+
418
+ # A name created by merging individual parameter descriptions, suitable
419
+ # for use in a function signature.
420
+ def name
421
+ @parameters.map do |parameter|
422
+ if parameter.resource_name
423
+ parameter.name
424
+ else
425
+ "#{@resource_name}_#{parameter.name}"
426
+ end
427
+ end.join('_or_')
428
+ end
429
+
430
+ def signature
431
+ name
432
+ end
433
+
434
+ # A description created by merging individual parameter descriptions.
435
+ def description
436
+ @parameters.map { |parameter| parameter.description }.join(' or ')
437
+ end
438
+
439
+ # A pretty representation of this instance.
440
+ def inspect
441
+ "ParameterChoice(parameters=#{@parameters})"
442
+ end
443
+ end
444
+ end
@@ -0,0 +1,3 @@
1
+ module Heroics
2
+ VERSION = '0.0.14'
3
+ end
@@ -0,0 +1,22 @@
1
+ require 'base64'
2
+ require 'erubis'
3
+ require 'excon'
4
+ require 'moneta'
5
+ require 'multi_json'
6
+ require 'uri'
7
+ require 'zlib'
8
+
9
+ # Heroics is an HTTP client for an API described by a JSON schema.
10
+ module Heroics
11
+ end
12
+
13
+ require 'heroics/version'
14
+ require 'heroics/errors'
15
+ require 'heroics/naming'
16
+ require 'heroics/link'
17
+ require 'heroics/resource'
18
+ require 'heroics/client'
19
+ require 'heroics/schema'
20
+ require 'heroics/command'
21
+ require 'heroics/cli'
22
+ require 'heroics/client_generator'