komoju 0.0.0 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -14
- data/.gitmodules +3 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +1 -1
- data/README.md +14 -2
- data/Rakefile +9 -0
- data/bin/generate-client +11 -0
- data/komoju.gemspec +15 -6
- data/lib/komoju/client.rb +460 -0
- data/lib/komoju/version.rb +1 -1
- data/lib/komoju.rb +10 -2
- data/test/client_test.rb +8 -0
- data/test/helper.rb +1 -0
- data/vendor/heroics/lib/heroics/cli.rb +88 -0
- data/vendor/heroics/lib/heroics/client.rb +109 -0
- data/vendor/heroics/lib/heroics/client_generator.rb +99 -0
- data/vendor/heroics/lib/heroics/command.rb +67 -0
- data/vendor/heroics/lib/heroics/errors.rb +6 -0
- data/vendor/heroics/lib/heroics/link.rb +120 -0
- data/vendor/heroics/lib/heroics/naming.rb +19 -0
- data/vendor/heroics/lib/heroics/resource.rb +30 -0
- data/vendor/heroics/lib/heroics/schema.rb +444 -0
- data/vendor/heroics/lib/heroics/version.rb +3 -0
- data/vendor/heroics/lib/heroics.rb +22 -0
- data/vendor/heroics/test/cli_test.rb +236 -0
- data/vendor/heroics/test/client_generator_test.rb +34 -0
- data/vendor/heroics/test/client_test.rb +215 -0
- data/vendor/heroics/test/command_test.rb +214 -0
- data/vendor/heroics/test/helper.rb +204 -0
- data/vendor/heroics/test/link_test.rb +398 -0
- data/vendor/heroics/test/naming_test.rb +45 -0
- data/vendor/heroics/test/resource_test.rb +35 -0
- data/vendor/heroics/test/schema_test.rb +287 -0
- data/vendor/heroics/test/version_test.rb +9 -0
- data/vendor/heroics/test.rb +42 -0
- metadata +139 -8
@@ -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,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'
|