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.
@@ -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
- @dst = dst
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__),
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module GoClient
3
- VERSION = '0.29.9'.freeze
3
+ VERSION = '0.30.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'haveapi', path: '../../../servers/ruby'
4
+ gem 'haveapi-go-client', path: '..'
5
+ gem 'haveapi-client', path: '../../ruby'
6
+ gem 'webrick'