haveapi-go-client 0.29.9 → 0.30.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +5 -0
- data/docs/documentation.md +171 -0
- data/haveapi-go-client.gemspec +1 -1
- data/lib/haveapi/go_client/documentation.rb +516 -0
- data/lib/haveapi/go_client/generator.rb +16 -3
- data/lib/haveapi/go_client/version.rb +1 -1
- data/spec/documentation.Gemfile +6 -0
- data/spec/documentation_acceptance.rb +271 -0
- data/spec/documentation_build_check.rb +31 -0
- data/spec/documentation_wire_check.rb +94 -0
- data/spec/support/documentation_fixture.rb +70 -0
- data/spec/support/test_server.rb +1 -1
- data/spec/unit/documentation_spec.rb +286 -0
- metadata +11 -3
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'haveapi/go_client'
|
|
4
|
+
require 'haveapi/go_client/utils'
|
|
5
|
+
|
|
6
|
+
module HaveAPI::GoClient
|
|
7
|
+
# Pure adapter over the complete schema used to generate a published SDK.
|
|
8
|
+
# Never reconstruct names from a user's reduced documentation description.
|
|
9
|
+
class Documentation
|
|
10
|
+
include Utils
|
|
11
|
+
|
|
12
|
+
Resolved = Struct.new(:resource, :action, :description)
|
|
13
|
+
INPUT_LAYOUTS = %w[hash object].freeze
|
|
14
|
+
OUTPUT_LAYOUTS = %w[hash object hash_list object_list].freeze
|
|
15
|
+
PARAMETER_CONTRACT = %i[type nullable resource value_id value_label].freeze
|
|
16
|
+
INPUT_MEMBERS = %w[SelectParameters UnselectParameters AnySelected].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(description:, import_path:, module_version:)
|
|
19
|
+
unless import_path.is_a?(String) && import_path.match?(%r{\A[A-Za-z0-9][A-Za-z0-9._~/-]*\z}) \
|
|
20
|
+
&& import_path.split('/').none? { |part| ['', '.', '..'].include?(part) } \
|
|
21
|
+
&& module_version.is_a?(String) && module_version.match?(/\Av\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/)
|
|
22
|
+
raise ArgumentError, 'invalid Go SDK import path or module version'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@import_path = import_path.dup.freeze
|
|
26
|
+
@module_version = module_version.dup.freeze
|
|
27
|
+
@description = copy(description)
|
|
28
|
+
validate_description
|
|
29
|
+
@api = ApiVersion.new(@description)
|
|
30
|
+
freeze_data(@description)
|
|
31
|
+
freeze
|
|
32
|
+
rescue KeyError, NoMethodError, TypeError
|
|
33
|
+
raise ArgumentError, 'invalid saved Go SDK description'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve(resource_path:, action_name:)
|
|
37
|
+
parent = @api
|
|
38
|
+
desc = @description
|
|
39
|
+
resource_path.each do |name|
|
|
40
|
+
parent = parent.resources.find { |resource| resource.name == name.to_s }
|
|
41
|
+
return nil unless parent
|
|
42
|
+
|
|
43
|
+
desc = lookup(desc[:resources], name)
|
|
44
|
+
end
|
|
45
|
+
return nil unless parent.is_a?(Resource)
|
|
46
|
+
|
|
47
|
+
action = parent.actions.find { |item| item.name == action_name.to_s }
|
|
48
|
+
unless action
|
|
49
|
+
aliases = parent.actions.select { |item| item.aliases.map(&:to_s).include?(action_name.to_s) }
|
|
50
|
+
action = aliases.first if aliases.one?
|
|
51
|
+
end
|
|
52
|
+
return unless action
|
|
53
|
+
|
|
54
|
+
Resolved.new(resource: parent, action:, description: lookup(desc[:actions], action.name)).freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render(kind:, context:, mode:, auth_method: nil, auth_description: nil)
|
|
58
|
+
return limitation('This Go SDK example mode is unsupported.') unless mode == :sync
|
|
59
|
+
|
|
60
|
+
case kind
|
|
61
|
+
when :init
|
|
62
|
+
code(program(context, [], ['_ = api']))
|
|
63
|
+
when :auth
|
|
64
|
+
render_auth(context, auth_method, auth_description)
|
|
65
|
+
when :example
|
|
66
|
+
render_example(context)
|
|
67
|
+
else
|
|
68
|
+
limitation('This Go SDK example is unavailable.')
|
|
69
|
+
end
|
|
70
|
+
rescue Unsupported => e
|
|
71
|
+
limitation(e.message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
protected
|
|
75
|
+
|
|
76
|
+
class Unsupported < StandardError; end
|
|
77
|
+
|
|
78
|
+
def validate_description
|
|
79
|
+
unless @description.is_a?(Hash) && @description[:authentication].is_a?(Hash) \
|
|
80
|
+
&& @description[:meta].is_a?(Hash) && [String, Symbol].any? { |type| @description[:meta][:namespace].is_a?(type) }
|
|
81
|
+
raise ArgumentError, 'invalid saved Go SDK description'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
validate_resources(@description[:resources])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_resources(resources)
|
|
88
|
+
raise ArgumentError, 'invalid saved Go SDK resources' unless resources.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
resources.each_value do |resource|
|
|
91
|
+
unless resource.is_a?(Hash) && resource[:actions].is_a?(Hash)
|
|
92
|
+
raise ArgumentError, 'invalid saved Go SDK resources'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
validate_resources(resource[:resources])
|
|
96
|
+
resource[:actions].each_value do |action|
|
|
97
|
+
unless action.is_a?(Hash) && action[:aliases].is_a?(Array) \
|
|
98
|
+
&& action[:aliases].all?(String) \
|
|
99
|
+
&& action[:method].is_a?(String) && %w[GET POST PUT PATCH DELETE HEAD OPTIONS].include?(action[:method]) \
|
|
100
|
+
&& action[:path].is_a?(String) && action[:path].start_with?('/') && !action[:path].start_with?('//') \
|
|
101
|
+
&& action[:meta].is_a?(Hash)
|
|
102
|
+
raise ArgumentError, 'invalid saved Go SDK action'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
%i[input output].each do |direction|
|
|
106
|
+
io = action[direction]
|
|
107
|
+
unless io.nil? || (io.is_a?(Hash) && io[:parameters].is_a?(Hash))
|
|
108
|
+
raise ArgumentError, 'invalid saved Go SDK parameters'
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_example(context)
|
|
116
|
+
resolved = resolve(resource_path: context.resource_path, action_name: context.action_name)
|
|
117
|
+
live = visible_action(context.description, context.resource_path, context.action_name)
|
|
118
|
+
unsupported!('This action is unavailable in the configured Go SDK.') unless resolved && live && context.action
|
|
119
|
+
saved = resolved.description
|
|
120
|
+
check_action(saved, live)
|
|
121
|
+
check_action(saved, context.action)
|
|
122
|
+
sample = context.sample
|
|
123
|
+
unless sample.is_a?(Hash) && [true, false].include?(sample[:status])
|
|
124
|
+
unsupported!('This sample does not describe a structured request and response.')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
action = resolved.action
|
|
128
|
+
check_input(action, saved[:input])
|
|
129
|
+
check_output(action, saved[:output], live[:output], sample, context)
|
|
130
|
+
members = resolved.resource.resource_path.map(&:go_member_name) + [action.go_member_name]
|
|
131
|
+
lines = ["invocation := api.#{members.join('.')}.Prepare()"]
|
|
132
|
+
lines.concat(path_lines(action, sample))
|
|
133
|
+
lines.concat(input_lines(action, saved[:input], live[:input], sample))
|
|
134
|
+
lines.concat(metadata_lines(action, saved, live))
|
|
135
|
+
lines.concat(call_lines(action, live, sample, context))
|
|
136
|
+
code(program(context, ['fmt'], lines))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_action(saved, live)
|
|
140
|
+
unless saved[:method] == live[:method] && saved[:path] == live[:path] \
|
|
141
|
+
&& (saved[:blocking] == true) == (live[:blocking] == true)
|
|
142
|
+
unsupported!('The configured Go SDK does not match this action route.')
|
|
143
|
+
end
|
|
144
|
+
%i[input output].each do |direction|
|
|
145
|
+
left = saved[direction] || {}
|
|
146
|
+
right = live[direction] || {}
|
|
147
|
+
unless left[:layout].to_s == right[:layout].to_s && left[:namespace].to_s == right[:namespace].to_s
|
|
148
|
+
unsupported!('The configured Go SDK does not match this action layout.')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
return unless [saved, live].any? { |description| required_metadata?(description) }
|
|
152
|
+
|
|
153
|
+
unsupported!('This action requires metadata that is absent from the sample.')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def required_metadata?(description)
|
|
157
|
+
%i[global object].any? do |role|
|
|
158
|
+
parameters = description.dig(:meta, role, :input, :parameters) || {}
|
|
159
|
+
parameters.any? { |_name, parameter| parameter[:required] == true }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def check_input(action, desc)
|
|
164
|
+
return if !desc || (desc[:parameters] || {}).empty?
|
|
165
|
+
|
|
166
|
+
unless INPUT_LAYOUTS.include?(desc[:layout].to_s)
|
|
167
|
+
unsupported!('The Go generator does not support list input in documentation examples.')
|
|
168
|
+
end
|
|
169
|
+
check_parameters(action.input, desc, input: true)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def check_output(action, desc, live, sample, context)
|
|
173
|
+
if !desc || (desc[:parameters] || {}).empty?
|
|
174
|
+
if sample[:status] && sample[:response] && ![{}, []].include?(sample[:response])
|
|
175
|
+
unsupported!('This response is unavailable in the configured Go SDK.')
|
|
176
|
+
end
|
|
177
|
+
return
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
unless OUTPUT_LAYOUTS.include?(desc[:layout].to_s)
|
|
181
|
+
unsupported!('The Go generator does not support this output layout.')
|
|
182
|
+
end
|
|
183
|
+
check_parameters(action.output, desc, input: false)
|
|
184
|
+
return unless sample[:status] && sample.has_key?(:response) && !sample[:response].nil?
|
|
185
|
+
|
|
186
|
+
values = desc[:layout].to_s.end_with?('_list') ? sample[:response] : [sample[:response]]
|
|
187
|
+
unsupported!('This sample does not match the Go SDK output layout.') unless values.is_a?(Array)
|
|
188
|
+
values.each { |value| check_output_value(action.output, desc, live, value, context) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def check_parameters(io, desc, input:)
|
|
192
|
+
names = io ? io.parameters.map(&:name) : []
|
|
193
|
+
unless (desc[:parameters] || {}).keys.all? { |name| names.include?(name.to_s) }
|
|
194
|
+
unsupported!('The Go generator does not support Custom fields in this sample.')
|
|
195
|
+
end
|
|
196
|
+
members = io.parameters.map(&:go_name)
|
|
197
|
+
reserved = input ? INPUT_MEMBERS + members.map { |name| "Set#{name}" } + members.map { |name| "Set#{name}Nil" } : []
|
|
198
|
+
return unless members.uniq != members || members.intersect?(reserved)
|
|
199
|
+
|
|
200
|
+
unsupported!('The Go generator cannot represent these parameter name collisions.')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def check_output_value(io, desc, live, value, context)
|
|
204
|
+
unsupported!('This sample does not match the Go SDK output layout.') unless value.is_a?(Hash)
|
|
205
|
+
value.each do |name, item|
|
|
206
|
+
parameter = io.parameters.find { |candidate| candidate.name == name.to_s }
|
|
207
|
+
unsupported!('This response field is unavailable in the configured Go SDK.') unless parameter
|
|
208
|
+
left = lookup(desc[:parameters], name)
|
|
209
|
+
right = lookup(live && live[:parameters], name)
|
|
210
|
+
unless left && right && parameter_contract(left) == parameter_contract(right)
|
|
211
|
+
unsupported!('This response field does not match the configured Go SDK.')
|
|
212
|
+
end
|
|
213
|
+
if item.nil? && !parameter.respond_to?(:association)
|
|
214
|
+
unsupported!('The Go SDK cannot distinguish scalar null output from its zero value.')
|
|
215
|
+
end
|
|
216
|
+
literal(parameter.go_out_type, item) unless parameter.respond_to?(:association)
|
|
217
|
+
next unless parameter.respond_to?(:association)
|
|
218
|
+
next if item.nil?
|
|
219
|
+
|
|
220
|
+
unless item.is_a?(Hash)
|
|
221
|
+
unsupported!('The Go SDK requires an object for an associated response field.')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
associated = parameter.association.resource.actions.find { |candidate| candidate.name == 'show' }
|
|
225
|
+
path = parameter.association.resource.resource_path.map(&:name)
|
|
226
|
+
saved_association = resolve(resource_path: path, action_name: 'show')
|
|
227
|
+
live_association = visible_action(context.description, path, 'show')
|
|
228
|
+
unless saved_association && live_association
|
|
229
|
+
unsupported!('This associated response is unavailable in the visible Go SDK documentation.')
|
|
230
|
+
end
|
|
231
|
+
check_parameters(associated.output, saved_association.description[:output], input: false)
|
|
232
|
+
check_output_value(associated.output, saved_association.description[:output], live_association[:output], item, context)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def path_lines(action, sample)
|
|
237
|
+
names = action.path.scan(/\{([a-zA-Z0-9\-_]+)\}/).flatten
|
|
238
|
+
values = sample[:path_params] || []
|
|
239
|
+
unless values.is_a?(Array) && names.length == values.length
|
|
240
|
+
unsupported!('This sample does not supply all Go SDK path parameters.')
|
|
241
|
+
end
|
|
242
|
+
names.zip(values).map do |name, value|
|
|
243
|
+
case value
|
|
244
|
+
when Integer
|
|
245
|
+
unsupported!('This path value is outside the Go int64 range.') unless int64?(value)
|
|
246
|
+
"invocation.SetPathParamInt(#{go_string_literal(name)}, #{value})"
|
|
247
|
+
when String
|
|
248
|
+
"invocation.SetPathParamString(#{go_string_literal(name)}, #{go_string_literal(value)})"
|
|
249
|
+
else
|
|
250
|
+
unsupported!('This Go SDK path parameter must be an integer or string.')
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def input_lines(action, saved, live, sample)
|
|
256
|
+
return [] unless sample.has_key?(:request) && !sample[:request].nil?
|
|
257
|
+
|
|
258
|
+
request = sample[:request]
|
|
259
|
+
unsupported!('The Go generator supports object input only.') unless request.is_a?(Hash)
|
|
260
|
+
unless action.has_input?
|
|
261
|
+
unsupported!('This input is unavailable in the configured Go SDK.') unless request.empty?
|
|
262
|
+
return []
|
|
263
|
+
end
|
|
264
|
+
lines = ["#{'input := ' unless request.empty?}invocation.NewInput().SelectParameters()"]
|
|
265
|
+
request.each do |name, value|
|
|
266
|
+
parameter = action.input.parameters.find { |candidate| candidate.name == name.to_s }
|
|
267
|
+
saved_parameter = lookup(saved && saved[:parameters], name)
|
|
268
|
+
live_parameter = lookup(live && live[:parameters], name)
|
|
269
|
+
unless parameter && saved_parameter && live_parameter && parameter_contract(saved_parameter) == parameter_contract(live_parameter)
|
|
270
|
+
unsupported!('This input field does not match the configured Go SDK.')
|
|
271
|
+
end
|
|
272
|
+
if value.nil?
|
|
273
|
+
unsupported!('This Go SDK input field does not accept null.') unless parameter.nillable?
|
|
274
|
+
lines << "input.Set#{parameter.go_name}Nil(true)"
|
|
275
|
+
else
|
|
276
|
+
lines << "input.Set#{parameter.go_name}(#{literal(parameter.go_in_type, value)})"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
lines
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def metadata_lines(action, saved, live)
|
|
283
|
+
io = action.metadata && action.metadata.global && action.metadata.global.input
|
|
284
|
+
return [] unless io && io.parameters.any?
|
|
285
|
+
|
|
286
|
+
saved_meta = saved.dig(:meta, :global, :input, :parameters) || {}
|
|
287
|
+
live_meta = live.dig(:meta, :global, :input, :parameters) || {}
|
|
288
|
+
return [] unless io.parameters.all? do |parameter|
|
|
289
|
+
left = lookup(saved_meta, parameter.name)
|
|
290
|
+
right = lookup(live_meta, parameter.name)
|
|
291
|
+
left && right && parameter_contract(left) == parameter_contract(right)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
['// To supply advertised global metadata, select only the fields you set:',
|
|
295
|
+
'// metadata := invocation.NewMetaInput().SelectParameters()'] +
|
|
296
|
+
io.parameters.map { |parameter| "// metadata.Set#{parameter.go_name}(value) // value has type #{parameter.go_in_type}" }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def call_lines(action, live, sample, context)
|
|
300
|
+
lines = [
|
|
301
|
+
'response, err := invocation.Call()',
|
|
302
|
+
'if err != nil {',
|
|
303
|
+
' fmt.Println(err)',
|
|
304
|
+
' return',
|
|
305
|
+
'}',
|
|
306
|
+
'if !response.Status {',
|
|
307
|
+
' fmt.Println(response.Message, response.Errors)',
|
|
308
|
+
' return',
|
|
309
|
+
'}'
|
|
310
|
+
]
|
|
311
|
+
return lines + ['// This sample demonstrates the failure branch above.'] unless sample[:status]
|
|
312
|
+
|
|
313
|
+
lines.concat(output_lines(action, sample))
|
|
314
|
+
if operation_visible?(action, live, context)
|
|
315
|
+
lines.push(
|
|
316
|
+
'if response.IsBlocking() {',
|
|
317
|
+
' state, err := response.OperationStatus()',
|
|
318
|
+
' if err != nil {',
|
|
319
|
+
' fmt.Println(err)',
|
|
320
|
+
' return',
|
|
321
|
+
' }',
|
|
322
|
+
' if !state.Status {',
|
|
323
|
+
' fmt.Println(state.Message, state.Errors)',
|
|
324
|
+
' return',
|
|
325
|
+
' }',
|
|
326
|
+
' // Inspect the operation status using its documented output fields.',
|
|
327
|
+
'}',
|
|
328
|
+
'// Request success does not establish operation completion.'
|
|
329
|
+
)
|
|
330
|
+
elsif action.blocking?
|
|
331
|
+
lines << '// Request success does not establish operation completion.'
|
|
332
|
+
end
|
|
333
|
+
lines
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def output_lines(action, sample)
|
|
337
|
+
return [] unless action.has_output? && sample.has_key?(:response) && !sample[:response].nil?
|
|
338
|
+
|
|
339
|
+
value = sample[:response]
|
|
340
|
+
if action.output.layout.to_s.end_with?('_list')
|
|
341
|
+
return ['fmt.Println(len(response.Output))'] if value.empty?
|
|
342
|
+
|
|
343
|
+
# Choose fields present in every documented row, without fabricating a row.
|
|
344
|
+
common = common_output_fields(action.output, value)
|
|
345
|
+
return ['fmt.Println(len(response.Output))'] if common.empty?
|
|
346
|
+
|
|
347
|
+
['for _, item := range response.Output {', ' if item != nil {'] +
|
|
348
|
+
output_fields(action.output, common, 'item').map { |line| " #{line}" } + [' }', '}']
|
|
349
|
+
else
|
|
350
|
+
['if response.Output != nil {'] + output_fields(action.output, value, 'response.Output').map { |line| " #{line}" } + ['}']
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def common_output_fields(io, rows)
|
|
355
|
+
rows.first.each_with_object({}) do |(name, value), common|
|
|
356
|
+
next unless rows.all? { |row| row.has_key?(name) }
|
|
357
|
+
|
|
358
|
+
parameter = io.parameters.find { |candidate| candidate.name == name.to_s }
|
|
359
|
+
if parameter.respond_to?(:association)
|
|
360
|
+
values = rows.map { |row| row[name] }
|
|
361
|
+
if values.all?(Hash)
|
|
362
|
+
associated = parameter.association.resource.actions.find { |candidate| candidate.name == 'show' }
|
|
363
|
+
common[name] = common_output_fields(associated.output, values)
|
|
364
|
+
elsif values.all?(&:nil?)
|
|
365
|
+
common[name] = nil
|
|
366
|
+
end
|
|
367
|
+
else
|
|
368
|
+
common[name] = value
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def output_fields(io, sample, variable)
|
|
374
|
+
sample.flat_map do |name, value|
|
|
375
|
+
parameter = io.parameters.find { |candidate| candidate.name == name.to_s }
|
|
376
|
+
field = "#{variable}.#{parameter.go_name}"
|
|
377
|
+
if parameter.respond_to?(:association) && value.is_a?(Hash)
|
|
378
|
+
associated = parameter.association.resource.actions.find { |candidate| candidate.name == 'show' }
|
|
379
|
+
["if #{field} != nil {"] + output_fields(associated.output, value, field).map { |line| " #{line}" } + ['}']
|
|
380
|
+
else
|
|
381
|
+
["fmt.Println(#{field}#{' == nil' if value.nil?})"]
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def operation_visible?(action, live, context)
|
|
387
|
+
return false unless action.blocking?
|
|
388
|
+
|
|
389
|
+
visible_id = live.dig(:meta, :global, :output, :parameters, :action_state_id)
|
|
390
|
+
metadata = action.metadata && action.metadata.global && action.metadata.global.output
|
|
391
|
+
saved_id = metadata && metadata.parameters.find { |parameter| parameter.name == 'action_state_id' }
|
|
392
|
+
return false unless visible_id && visible_id[:type] == 'Integer' && saved_id && saved_id.go_out_type == 'int64'
|
|
393
|
+
|
|
394
|
+
# Generated operation helpers currently use these exact members/types.
|
|
395
|
+
state = resolve(resource_path: ['action_state'], action_name: 'show')
|
|
396
|
+
visible_state = visible_action(context.description, ['action_state'], 'show')
|
|
397
|
+
state && visible_state && state.resource.go_member_name == 'ActionState' && state.action.go_member_name == 'Show' \
|
|
398
|
+
&& state.action.go_response_type == 'ActionActionStateShowResponse' \
|
|
399
|
+
&& state.action.path.scan(/\{([a-zA-Z0-9\-_]+)\}/).flatten == ['action_state_id'] \
|
|
400
|
+
&& !state.action.has_input? \
|
|
401
|
+
&& state.description[:path] == visible_state[:path] && state.description[:method] == visible_state[:method] \
|
|
402
|
+
&& @description.dig(:meta, :namespace).to_s == context.description.dig(:meta, :namespace).to_s
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def render_auth(context, method, desc)
|
|
406
|
+
method = method.to_sym
|
|
407
|
+
visible = lookup(context.description[:authentication], method)
|
|
408
|
+
saved = lookup(@description[:authentication], method)
|
|
409
|
+
unsupported!('This authentication method is unavailable in the configured Go SDK.') unless visible && saved && desc
|
|
410
|
+
lines = case method
|
|
411
|
+
when :basic
|
|
412
|
+
['api.SetBasicAuthentication(os.Getenv("HAVEAPI_USER"), os.Getenv("HAVEAPI_PASSWORD"))']
|
|
413
|
+
when :token
|
|
414
|
+
unless saved[:http_header] == visible[:http_header] && saved[:query_parameter] == visible[:query_parameter]
|
|
415
|
+
unsupported!('This authentication method does not match the configured Go SDK.')
|
|
416
|
+
end
|
|
417
|
+
['api.SetExistingTokenAuth(os.Getenv("HAVEAPI_TOKEN"))',
|
|
418
|
+
'// Use a token obtained through the advertised authentication provider.']
|
|
419
|
+
when :oauth2
|
|
420
|
+
unless saved[:http_header] == visible[:http_header]
|
|
421
|
+
unsupported!('This authentication method does not match the configured Go SDK.')
|
|
422
|
+
end
|
|
423
|
+
['api.SetExistingOAuth2Auth(os.Getenv("HAVEAPI_ACCESS_TOKEN"))',
|
|
424
|
+
'// Obtain the access token through the OAuth2 provider before running this program.']
|
|
425
|
+
else
|
|
426
|
+
unsupported!('This authentication method is unsupported by the Go generator.')
|
|
427
|
+
end
|
|
428
|
+
code(program(context, ['os'], lines))
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def program(context, imports, lines)
|
|
432
|
+
# Saved server routes already include the mount root and /vVERSION.
|
|
433
|
+
origin = URI.parse(context.base_url)
|
|
434
|
+
origin.path = ''
|
|
435
|
+
origin.query = origin.fragment = nil
|
|
436
|
+
header = ["// go get #{@import_path}@#{@module_version}", 'package main', '', 'import (']
|
|
437
|
+
imports.each { |name| header << " #{go_string_literal(name)}" }
|
|
438
|
+
header << " client #{go_string_literal(@import_path)}"
|
|
439
|
+
header.push(')', '', 'func main() {', " api := client.New(#{go_string_literal(origin.to_s)})")
|
|
440
|
+
(header + lines.map { |line| " #{line}" } + ['}', '']).join("\n")
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def literal(type, value)
|
|
444
|
+
valid = case type
|
|
445
|
+
when 'string' then value.is_a?(String)
|
|
446
|
+
when 'int64' then int64?(value)
|
|
447
|
+
when 'float64' then (value.is_a?(Float) || int64?(value)) && value.to_f.finite?
|
|
448
|
+
when 'bool' then [true, false].include?(value)
|
|
449
|
+
else false
|
|
450
|
+
end
|
|
451
|
+
unsupported!('This sample value cannot be represented by the Go SDK field type.') unless valid
|
|
452
|
+
type == 'string' ? go_string_literal(value) : value.to_s
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def int64?(value)
|
|
456
|
+
value.is_a?(Integer) && value.between?(-(2**63), (2**63) - 1)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def parameter_contract(desc)
|
|
460
|
+
PARAMETER_CONTRACT.map { |key| desc[key] }
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def visible_action(description, path, name)
|
|
464
|
+
desc = description
|
|
465
|
+
path.each do |component|
|
|
466
|
+
desc = lookup(desc[:resources], component)
|
|
467
|
+
return nil unless desc
|
|
468
|
+
end
|
|
469
|
+
lookup(desc[:actions], name)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def lookup(hash, key)
|
|
473
|
+
hash && (hash[key] || hash[key.to_s] || hash[key.to_s.to_sym])
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def copy(value)
|
|
477
|
+
case value
|
|
478
|
+
when Hash
|
|
479
|
+
value.each_with_object({}) do |(key, item), result|
|
|
480
|
+
unless (key.is_a?(String) || key.is_a?(Symbol)) && !result.has_key?(key.to_sym)
|
|
481
|
+
raise ArgumentError, 'invalid saved Go SDK description'
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
result[key.to_sym] = copy(item)
|
|
485
|
+
end
|
|
486
|
+
when Array then value.map { |item| copy(item) }
|
|
487
|
+
when String then value.dup
|
|
488
|
+
when NilClass, Numeric, TrueClass, FalseClass, Symbol then value
|
|
489
|
+
else raise ArgumentError, 'invalid saved Go SDK description'
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def freeze_data(value)
|
|
494
|
+
case value
|
|
495
|
+
when Hash then value.each do |key, item|
|
|
496
|
+
freeze_data(key)
|
|
497
|
+
freeze_data(item)
|
|
498
|
+
end
|
|
499
|
+
when Array then value.each { |item| freeze_data(item) }
|
|
500
|
+
end
|
|
501
|
+
value.freeze
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def unsupported!(reason)
|
|
505
|
+
raise Unsupported, reason
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def code(source)
|
|
509
|
+
{ status: :code, code: source.freeze }.freeze
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def limitation(reason)
|
|
513
|
+
{ status: :limitation, reason: reason.freeze }.freeze
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
@@ -19,6 +19,15 @@ module HaveAPI::GoClient
|
|
|
19
19
|
# @return [String]
|
|
20
20
|
attr_reader :package
|
|
21
21
|
|
|
22
|
+
# Construct from the exact saved API description without contacting a server.
|
|
23
|
+
# Generation and formatting remain explicit caller operations.
|
|
24
|
+
def self.from_description(description, dst, opts)
|
|
25
|
+
generator = allocate
|
|
26
|
+
generator.send(:configure, dst, opts)
|
|
27
|
+
generator.instance_variable_set(:@api, ApiVersion.new(description))
|
|
28
|
+
generator
|
|
29
|
+
end
|
|
30
|
+
|
|
22
31
|
# @param url [String] API URL
|
|
23
32
|
# @param dst [String] destination directory
|
|
24
33
|
# @param opts [Hash]
|
|
@@ -26,9 +35,7 @@ module HaveAPI::GoClient
|
|
|
26
35
|
# @option opts [String] :module
|
|
27
36
|
# @option opts [String] :package
|
|
28
37
|
def initialize(url, dst, opts)
|
|
29
|
-
|
|
30
|
-
@module = opts[:module] && go_module_path(opts[:module])
|
|
31
|
-
@package = go_package_name(opts[:package])
|
|
38
|
+
configure(dst, opts)
|
|
32
39
|
|
|
33
40
|
conn = HaveAPI::Client::Communicator.new(url)
|
|
34
41
|
if opts[:basic_user] && opts[:basic_password]
|
|
@@ -77,6 +84,12 @@ module HaveAPI::GoClient
|
|
|
77
84
|
|
|
78
85
|
attr_reader :api
|
|
79
86
|
|
|
87
|
+
def configure(dst, opts)
|
|
88
|
+
@dst = dst
|
|
89
|
+
@module = opts[:module] && go_module_path(opts[:module])
|
|
90
|
+
@package = go_package_name(opts[:package])
|
|
91
|
+
end
|
|
92
|
+
|
|
80
93
|
def i18n_messages
|
|
81
94
|
@i18n_messages ||= YAML.safe_load_file(
|
|
82
95
|
File.expand_path('i18n_messages.yml', __dir__),
|