haveapi-go-client 0.29.12 → 0.30.1

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,122 @@
1
+ # Real mounted DSL for the pure documentation-profile check; no action is run.
2
+ module GoDocumentationProfileFixture
3
+ HOSTILE = "quote\"\\\n雪</script>".freeze
4
+
5
+ module Resources
6
+ class Owner < HaveAPI::Resource
7
+ version '7'
8
+ auth false
9
+
10
+ define_action(:Index) do
11
+ authorize { allow }
12
+ output(:hash_list) do
13
+ integer :id
14
+ string :label
15
+ end
16
+ end
17
+
18
+ define_action(:Show) do
19
+ route '{owner_id}'
20
+ authorize { allow }
21
+ output(:hash) do
22
+ integer :id
23
+ string :label
24
+ end
25
+ end
26
+ end
27
+
28
+ class SetTimeout < HaveAPI::Resource
29
+ version '7'
30
+ auth false
31
+
32
+ define_action(:Secret) do
33
+ authorize { |user| user == :privileged ? allow : deny }
34
+ output(:hash) { string :private_value }
35
+ end
36
+ end
37
+
38
+ class SetTimeoutResource < HaveAPI::Resource
39
+ version '7'
40
+ route 'parents'
41
+ auth false
42
+
43
+ define_resource(:Child) do
44
+ route '{parent_id}/children'
45
+ auth false
46
+ define_action(:Client) do
47
+ route '{child_id}'
48
+ http_method :post
49
+ aliases ['perform']
50
+ authorize do |user|
51
+ input blacklist: [:private_value] unless user == :privileged
52
+ output blacklist: [:private_value] unless user == :privileged
53
+ allow
54
+ end
55
+
56
+ input(:hash) do
57
+ string :name, nullable: true
58
+ bool :enabled
59
+ integer :count
60
+ string :private_value, default: 'private default'
61
+ string :unused
62
+ end
63
+ output(:hash) do
64
+ integer :id
65
+ string :private_value
66
+ resource Owner, name: :owner, nullable: true
67
+ end
68
+ meta(:global) do
69
+ input { custom :includes }
70
+ end
71
+
72
+ example 'filtered sample' do # rubocop:disable RSpec/NoExpectationExample -- HaveAPI sample DSL.
73
+ path_params 'parent/with space', 0
74
+ request name: HOSTILE, enabled: false, count: 0, private_value: 'private sample'
75
+ response id: 0, private_value: 'private result', owner: { id: 9 }
76
+ end
77
+
78
+ def exec
79
+ raise 'Documentation must not execute an action'
80
+ end
81
+ end
82
+ end
83
+
84
+ define_action(:Update) do
85
+ self.action_name = 'child'
86
+ route 'update'
87
+ aliases ['perform']
88
+ authorize { allow }
89
+ input(:hash) { string :name, nullable: true }
90
+ output(:hash) { integer :id }
91
+
92
+ example 'nullable sample' do # rubocop:disable RSpec/NoExpectationExample -- HaveAPI sample DSL.
93
+ request name: nil
94
+ response id: 0
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def self.server(**go)
101
+ server = HaveAPI::Server.new(Resources)
102
+ server.use_version('7')
103
+ server.default_version = '7'
104
+ server.documentation_clients('7', go: { import_path: 'example.test/current-sdk/client', **go })
105
+ server.mount('/mounted/')
106
+ server
107
+ end
108
+
109
+ def self.description(server, user: nil)
110
+ server.describe_version(HaveAPI::Context.new(server, version: '7', user:, endpoint: false, doc: true))
111
+ end
112
+
113
+ def self.context(description, path: %w[set_timeout_resource child], action_name: 'client', sample: nil)
114
+ resource = path.inject(description) { |parent, name| parent.fetch(:resources).fetch(name) }
115
+ action = resource.fetch(:actions).fetch(action_name)
116
+ HaveAPI::ClientExample::Context.new(
117
+ version: '7', host: 'api.example.test', base_url: 'https://api.example.test/mounted/',
118
+ version_url: 'https://api.example.test/mounted/v7/', description:, resource_path: path,
119
+ resource:, action_name:, action:, sample: sample || action.fetch(:examples).first
120
+ )
121
+ end
122
+ end
@@ -20,7 +20,7 @@ class ClientTestServer
20
20
  def start
21
21
  return if @wait_thr
22
22
 
23
- env = { 'BUNDLE_GEMFILE' => @gemfile }
23
+ env = { 'BUNDLE_GEMFILE' => @gemfile, 'BUNDLE_LOCKFILE' => "#{@gemfile}.lock" }
24
24
  cmd = ['bundle', 'exec', 'ruby', @server_script, '--port', '0']
25
25
  @stdin, @stdout, @wait_thr = Open3.popen2e(env, *cmd, chdir: @cwd)
26
26
 
@@ -0,0 +1,356 @@
1
+ require 'haveapi/go_client/documentation'
2
+ require_relative '../support/documentation_fixture'
3
+
4
+ RSpec.describe HaveAPI::GoClient::Documentation do
5
+ let(:description) { GoDocumentationFixture.description }
6
+ let(:adapter) { described_class.new(description:, import_path: 'example.test/sdk/client', module_version: 'v1.2.3') }
7
+ let(:action) { description.dig(:resources, :client, :resources, :child, :actions, :client) }
8
+ let(:sample) { GoDocumentationFixture.sample }
9
+ let(:context) do
10
+ Struct.new(:version, :base_url, :description, :resource_path, :action_name, :action, :sample).new(version: '7', base_url: 'https://api.example.test/mounted/', description:,
11
+ resource_path: %w[client child], action_name: 'client', action:, sample:)
12
+ end
13
+
14
+ def render
15
+ adapter.render(kind: :example, context:, mode: :sync)
16
+ end
17
+
18
+ it 'leaves an omitted module version unresolved and prints the Go latest selector' do
19
+ unversioned = described_class.new(description:, import_path: 'example.test/sdk/client')
20
+ expect(unversioned).to be_frozen
21
+ expect(unversioned.instance_variable_get(:@module_version)).to be_nil
22
+ expect(unversioned.render(kind: :init, context:, mode: :sync).fetch(:code)).to start_with("// go get example.test/sdk/client@latest\n")
23
+ expect(unversioned.render(kind: :example, context:, mode: :sync).fetch(:code)).to eq(
24
+ render.fetch(:code).sub('@v1.2.3', '@latest')
25
+ )
26
+ end
27
+
28
+ it 'treats an explicit nil version as omission without changing the canonical model' do
29
+ unversioned = described_class.new(description:, import_path: 'example.test/sdk/client', module_version: nil)
30
+ resolved = unversioned.resolve(resource_path: %w[client child], action_name: 'perform')
31
+ expect(resolved.action.go_member_names).to eq(adapter.resolve(resource_path: %w[client child], action_name: 'perform').action.go_member_names)
32
+ expect(unversioned.render(kind: :example, context:, mode: :sync).fetch(:code)).to include('client "example.test/sdk/client"', 'api.Client.Child.ClientAction.Prepare()')
33
+ end
34
+
35
+ it 'still rejects hostile imports when the module version is omitted' do
36
+ expect do
37
+ described_class.new(description:, import_path: "example.test/sdk/client\npackage injected")
38
+ end.to raise_error(ArgumentError, 'invalid Go SDK import path or module version')
39
+ end
40
+
41
+ it 'retains explicit version selectors and copies configuration strings' do
42
+ version = +'v2.3.4-rc.1+build.7'
43
+ path = +'example.test/sdk/v2/client'
44
+ pinned = described_class.new(description:, import_path: path, module_version: version)
45
+ version.replace('v9.9.9')
46
+ path.replace('other.test/sdk/client')
47
+ expect(pinned.render(kind: :init, context:, mode: :sync).fetch(:code)).to start_with("// go get example.test/sdk/v2/client@v2.3.4-rc.1+build.7\n")
48
+ end
49
+
50
+ ['', 'latest', '1.2.3', "v1.2.3\npackage injected", false, 7].each do |invalid|
51
+ it "rejects a supplied invalid module version #{invalid.inspect}" do
52
+ expect do
53
+ described_class.new(description:, import_path: 'example.test/sdk/client', module_version: invalid)
54
+ end.to raise_error(ArgumentError, 'invalid Go SDK import path or module version')
55
+ end
56
+ end
57
+
58
+ it 'resolves canonical generator members and aliases without generator or serving I/O' do
59
+ allow(HaveAPI::GoClient::Generator).to receive(:new).and_call_original
60
+ allow(HaveAPI::GoClient::Generator).to receive(:allocate).and_call_original
61
+ allow(File).to receive(:read).and_call_original
62
+ model = HaveAPI::GoClient::ApiVersion.new(GoDocumentationFixture.description)
63
+ resolved = adapter.resolve(resource_path: %w[client child], action_name: 'perform')
64
+ canonical = model.resources.find { |resource| resource.name == 'client' }.resources.first.actions.first
65
+
66
+ expect(resolved.action.go_member_names).to eq(canonical.go_member_names)
67
+ expect(resolved.resource.resource_path.map(&:go_member_name)).to eq(%w[Client Child])
68
+ expect(resolved.action.go_member_name).to eq('ClientAction')
69
+ expect(render[:status]).to eq(:code)
70
+ expect(HaveAPI::GoClient::Generator).not_to have_received(:new)
71
+ expect(HaveAPI::GoClient::Generator).not_to have_received(:allocate)
72
+ expect(File).not_to have_received(:read)
73
+ end
74
+
75
+ it 'uses the full saved model even when the visible description has fewer siblings' do
76
+ description[:resources][:set_timeout] = { resources: {}, actions: {} }
77
+ adapter
78
+ context.description = Marshal.load(Marshal.dump(description))
79
+ context.description[:resources].delete(:hidden)
80
+ context.description[:resources].delete(:set_timeout)
81
+
82
+ expect(adapter.resolve(resource_path: ['set_timeout'], action_name: 'missing')).to be_nil
83
+ expect(render[:code]).not_to include('hidden', 'secret', 'SetTimeout')
84
+ end
85
+
86
+ it 'selects no inputs before supplied setters, preserving null, omission, false and zero' do
87
+ result = render
88
+ expect(result).to be_frozen
89
+ expect(result[:code]).to include('client "example.test/sdk/client"', 'client.New("https://api.example.test")',
90
+ 'api.Client.Child.ClientAction.Prepare()',
91
+ 'invocation.SetPathParamInt("client_id", 42)',
92
+ 'invocation.SetPathParamString("child_id", "a/b")',
93
+ 'input := invocation.NewInput().SelectParameters()', 'input.SetNameNil(true)',
94
+ 'input.SetEnabled(false)', 'input.SetCount(0)')
95
+ expect(result[:code]).not_to include('SetUnused')
96
+ expect(result[:code].index('SelectParameters()')).to be < result[:code].index('input.SetNameNil')
97
+ end
98
+
99
+ it 'preserves an explicitly empty request and escapes source literals' do
100
+ sample[:request] = {}
101
+ expect(render[:code]).to include('invocation.NewInput().SelectParameters()')
102
+ expect(render[:code]).not_to include('input :=')
103
+ sample[:request] = { name: "quote\"\\\n雪</script>" }
104
+ expect(render[:code]).to include('input.SetName("quote\\"\\\\\\n雪</script>")')
105
+ end
106
+
107
+ it 'rejects list input, Custom fields, invalid values, missing paths and stale contracts' do
108
+ sample[:request][:count] = 'wrong'
109
+ expect(render[:status]).to eq(:limitation)
110
+ sample[:request][:count] = 0
111
+ sample[:path_params].pop
112
+ expect(render[:reason]).to include('path parameters')
113
+ sample[:path_params] << 'child'
114
+ action[:input][:parameters][:name][:nullable] = false
115
+ expect(render[:status]).to eq(:limitation)
116
+ end
117
+
118
+ it 'reports a list-input limitation rather than discarding fields' do
119
+ action[:input][:layout] = 'hash_list'
120
+ sample[:request] = [{ name: 'a' }]
121
+ expect(render).to include(status: :limitation, reason: /list input/)
122
+ end
123
+
124
+ it 'reports unsupported Custom input and output without rendering its contents' do
125
+ action[:input][:parameters][:custom] = { type: 'Custom' }
126
+ sample[:request][:custom] = { secret_value: 'do not render' }
127
+ expect(render).to include(status: :limitation, reason: /Custom/)
128
+ expect(render.to_s).not_to include('secret_value', 'do not render')
129
+ end
130
+
131
+ it 'supports list output and refuses scalar null output fidelity claims' do
132
+ action[:output][:layout] = 'hash_list'
133
+ sample[:response] = [{ id: 1 }, { id: 2 }]
134
+ expect(render[:code]).to include('for _, item := range response.Output', 'fmt.Println(item.Id)')
135
+ sample[:response] = [{ name: nil }]
136
+ expect(render).to include(status: :limitation, reason: /scalar null/)
137
+ end
138
+
139
+ it 'renders transport errors and API failures separately without invented success output' do
140
+ sample[:status] = false
141
+ sample[:response] = nil
142
+ expect(render[:code]).to include('if err != nil', 'if !response.Status', 'response.Message, response.Errors')
143
+ expect(render[:code]).not_to include('response.Output')
144
+ end
145
+
146
+ it 'never exposes a saved action absent from the authorized description' do
147
+ adapter
148
+ context.description = Marshal.load(Marshal.dump(description))
149
+ context.description[:resources].delete(:client)
150
+ expect(render).to include(status: :limitation)
151
+ expect(render.to_s).not_to include('secret', 'hidden')
152
+ end
153
+
154
+ it 'checks response schema drift only on fields in the filtered sample' do
155
+ adapter
156
+ context.description = Marshal.load(Marshal.dump(description))
157
+ context.description.dig(:resources, :client, :resources, :child, :actions, :client, :output, :parameters, :id)[:type] = 'String'
158
+ expect(render).to include(status: :limitation, reason: /response field/)
159
+ end
160
+
161
+ it 'only renders advertised authentication and uses environment placeholders' do
162
+ basic = adapter.render(kind: :auth, context:, mode: :sync, auth_method: :basic, auth_description: {})
163
+ expect(basic[:code]).to include('api.SetBasicAuthentication(os.Getenv("HAVEAPI_USER"), os.Getenv("HAVEAPI_PASSWORD"))')
164
+ oauth = adapter.render(kind: :auth, context:, mode: :sync, auth_method: :oauth2, auth_description: {})
165
+ expect(oauth[:code]).to include('api.SetExistingOAuth2Auth(os.Getenv("HAVEAPI_ACCESS_TOKEN"))')
166
+ token = adapter.render(kind: :auth, context:, mode: :sync, auth_method: :token, auth_description: {})
167
+ expect(token[:code]).to include('api.SetExistingTokenAuth(os.Getenv("HAVEAPI_TOKEN"))')
168
+ context.description = Marshal.load(Marshal.dump(description))
169
+ context.description[:authentication].delete(:token)
170
+ expect(adapter.render(kind: :auth, context:, mode: :sync, auth_method: :token, auth_description: {})).to include(status: :limitation)
171
+ end
172
+
173
+ it 'does not show operation helpers when action-state visibility is absent' do
174
+ action[:blocking] = true
175
+ action[:meta][:global][:output] = GoDocumentationFixture.io({ action_state_id: { type: 'Integer' } }, namespace: false)
176
+ adapter
177
+ context.description = Marshal.load(Marshal.dump(description))
178
+ context.description[:resources].delete(:action_state)
179
+ expect(render[:code]).not_to include('response.OperationStatus')
180
+ expect(render[:code]).to include('does not establish operation completion')
181
+ end
182
+
183
+ it 'uses the origin with mounted versioned SDK paths and rejects a changed mount' do
184
+ expect(render[:code]).to include('client.New("https://api.example.test")')
185
+ expect(adapter.resolve(resource_path: %w[client child], action_name: 'client').action.path).to start_with('/mounted/v7/')
186
+ adapter
187
+ context.description = Marshal.load(Marshal.dump(description))
188
+ context.description.dig(:resources, :client, :resources, :child, :actions, :client)[:path] = '/other/v7/widgets'
189
+ expect(render).to include(status: :limitation, reason: /route/)
190
+ end
191
+
192
+ it 'preserves the URL constructor and its authentication and generation options' do
193
+ connection = instance_double(HaveAPI::Client::Communicator, describe_api: description, authenticate: nil)
194
+ allow(HaveAPI::Client::Communicator).to receive(:new).with('https://fixture.invalid').and_return(connection)
195
+ generator = HaveAPI::GoClient::Generator.new('https://fixture.invalid', '/unused',
196
+ version: '7', module: 'example.test/sdk', package: 'client',
197
+ basic_user: 'fixture-user', basic_password: 'fixture-password')
198
+ expect(connection).to have_received(:authenticate).with(:basic, user: 'fixture-user', password: 'fixture-password')
199
+ expect(connection).to have_received(:describe_api).with('7')
200
+ expect(generator.dst).to eq('/unused')
201
+ expect(generator.module).to eq('example.test/sdk')
202
+ expect(generator.package).to eq('client')
203
+ end
204
+
205
+ it 'builds a fixture generator from saved input without fetching or writing' do
206
+ allow(HaveAPI::Client::Communicator).to receive(:new).and_call_original
207
+ allow(FileUtils).to receive(:mkpath).and_call_original
208
+ generator = HaveAPI::GoClient::Generator.from_description(description, '/unused', module: 'example.test/sdk', package: 'client')
209
+ expect(generator.dst).to eq('/unused')
210
+ expect(HaveAPI::Client::Communicator).not_to have_received(:new)
211
+ expect(FileUtils).not_to have_received(:mkpath)
212
+ end
213
+
214
+ it 'rejects malformed saved schemas at initialization' do
215
+ description[:resources][:client][:resources][:child][:actions][:client][:path] = nil
216
+ expect { adapter }.to raise_error(ArgumentError, /saved Go SDK action/)
217
+ end
218
+
219
+ it 'checks Go field and method name collisions' do
220
+ action[:input][:parameters][:select_parameters] = { type: 'String' }
221
+ expect(render).to include(status: :limitation, reason: /name collisions/)
222
+ end
223
+
224
+ it 'shows operation methods only for visible matching state actions and metadata' do
225
+ action[:blocking] = true
226
+ action[:meta][:global][:output] = GoDocumentationFixture.io({ action_state_id: { type: 'Integer' } }, namespace: false)
227
+ expect(render[:code]).to include('response.IsBlocking()', 'response.OperationStatus()')
228
+ end
229
+
230
+ %i[global object].each do |role|
231
+ [true, false].each do |success|
232
+ it "limits #{role} metadata required in the saved SDK for status #{success}" do
233
+ action[:meta][role] = { input: GoDocumentationFixture.io({ hidden_requirement: { type: 'Boolean', required: true, default: 'private default' } }) }
234
+ sample[:status] = success
235
+ adapter
236
+ context.description = Marshal.load(Marshal.dump(description))
237
+ visible = context.description.dig(:resources, :client, :resources, :child, :actions, :client)
238
+ visible[:meta].delete(role)
239
+
240
+ expect(render).to include(status: :limitation, reason: 'This action requires metadata that is absent from the sample.')
241
+ expect(render.to_s).not_to include('hidden_requirement', 'private default', 'SetHiddenRequirement')
242
+ end
243
+
244
+ it "limits authorized-live #{role} metadata newly required for status #{success}" do
245
+ adapter
246
+ context.description = Marshal.load(Marshal.dump(description))
247
+ visible = context.description.dig(:resources, :client, :resources, :child, :actions, :client)
248
+ visible[:meta][role] = { input: GoDocumentationFixture.io({ confirm: { type: 'Boolean', required: true } }) }
249
+ sample[:status] = success
250
+
251
+ expect(render).to include(status: :limitation, reason: 'This action requires metadata that is absent from the sample.')
252
+ expect(render.to_s).not_to include('confirm', 'SetConfirm')
253
+ end
254
+ end
255
+ end
256
+
257
+ it 'preserves optional global guidance and allows optional object metadata without sample values' do
258
+ action[:meta][:global][:input][:parameters][:includes][:required] = false
259
+ action[:meta][:object] = { input: GoDocumentationFixture.io({ confirm: { type: 'Boolean', required: false } }) }
260
+ expect(render[:code]).to include('// metadata := invocation.NewMetaInput().SelectParameters()', '// metadata.SetIncludes(value)')
261
+ expect(render[:code]).not_to include('SetConfirm')
262
+ end
263
+
264
+ it 'keeps metadata-free success and failure examples executable' do
265
+ action[:meta] = {}
266
+ expect(render[:status]).to eq(:code)
267
+ expect(render[:code]).not_to include('NewMetaInput')
268
+ sample[:status] = false
269
+ expect(render[:code]).to include('if !response.Status')
270
+ end
271
+
272
+ it 'does not disclose hidden global metadata setters' do
273
+ adapter
274
+ context.description = Marshal.load(Marshal.dump(description))
275
+ context.description.dig(:resources, :client, :resources, :child, :actions, :client, :meta, :global, :input, :parameters).clear
276
+ expect(render[:code]).not_to include('NewMetaInput', 'SetIncludes')
277
+ end
278
+
279
+ it 'rejects Custom output, invalid path values, and ambiguous aliases safely' do
280
+ action[:output][:parameters][:payload] = { type: 'Custom' }
281
+ sample[:response][:payload] = { private_name: 'private value' }
282
+ expect(render).to include(status: :limitation, reason: /Custom/)
283
+ expect(render.to_s).not_to include('private_name', 'private value')
284
+ end
285
+
286
+ it 'keeps alias resolution unambiguous and canonical names preferred' do
287
+ resource = description.dig(:resources, :client, :resources, :child)
288
+ resource[:actions][:other] = GoDocumentationFixture.action(aliases: %w[perform client])
289
+ expect(adapter.resolve(resource_path: %w[client child], action_name: 'perform')).to be_nil
290
+ expect(adapter.resolve(resource_path: %w[client child], action_name: 'client')).not_to be_nil
291
+ end
292
+
293
+ it 'preserves partial and nullable Resource output while rejecting scalar association values' do
294
+ action[:output][:parameters][:owner] = { type: 'Resource', resource: ['owner'], value_id: 'id', value_label: 'label', nullable: true }
295
+ sample[:response][:owner] = { id: 9 }
296
+ expect(render[:code]).to include('if response.Output.Owner != nil', 'fmt.Println(response.Output.Owner.Id)')
297
+ expect(render[:code]).not_to include('Owner.Label')
298
+ sample[:response][:owner] = nil
299
+ expect(render[:code]).to include('fmt.Println(response.Output.Owner == nil)')
300
+ sample[:response][:owner] = 9
301
+ expect(render).to include(status: :limitation, reason: /requires an object/)
302
+ end
303
+
304
+ it 'matches live Resource identifier symbols against a saved JSON contract' do
305
+ action[:output][:parameters][:owner] = { type: 'Resource', resource: ['owner'], value_id: :id, value_label: :label, nullable: true }
306
+ sample[:response][:owner] = { id: 9 }
307
+ saved = JSON.parse(JSON.generate(description))
308
+ json_adapter = described_class.new(description: saved, import_path: 'example.test/sdk/client', module_version: 'v1.2.3')
309
+ result = json_adapter.render(kind: :example, context:, mode: :sync)
310
+
311
+ expect(result[:status]).to eq(:code)
312
+ expect(result[:code]).to include('fmt.Println(response.Output.Owner.Id)')
313
+ expect(result[:code]).not_to include('Owner.Label')
314
+ expect(result[:code]).to eq(render.fetch(:code))
315
+ end
316
+
317
+ {
318
+ value_id: ['other', nil, false, 7, [], {}],
319
+ value_label: ['other', nil, false, 7, [], {}],
320
+ type: [:Resource], nullable: [false], resource: [[:owner]]
321
+ }.each do |key, values|
322
+ values.each do |value|
323
+ it "rejects a changed or invalid live Resource contract #{key}: #{value.inspect}" do
324
+ action[:output][:parameters][:owner] = { type: 'Resource', resource: ['owner'], value_id: 'id', value_label: 'label', nullable: true }
325
+ sample[:response][:owner] = { id: 9 }
326
+ adapter
327
+ action[:output][:parameters][:owner][key] = value
328
+
329
+ expect(render).to include(status: :limitation, reason: 'This response field does not match the configured Go SDK.')
330
+ end
331
+ end
332
+ end
333
+
334
+ it 'does not print associated fields missing from another documented list row' do
335
+ action[:output][:layout] = 'hash_list'
336
+ action[:output][:parameters][:owner] = { type: 'Resource', resource: ['owner'], value_id: 'id', value_label: 'label' }
337
+ sample[:response] = [{ owner: { id: 1, label: 'one' } }, { owner: { id: 2 } }]
338
+ expect(render[:code]).to include('fmt.Println(item.Owner.Id)')
339
+ expect(render[:code]).not_to include('Owner.Label')
340
+ end
341
+
342
+ it 'does not guess path argument types or wrap integers' do
343
+ sample[:path_params] = [2**64, 'child']
344
+ expect(render).to include(status: :limitation, reason: /int64 range/)
345
+ sample[:path_params] = [true, 'child']
346
+ expect(render).to include(status: :limitation, reason: /integer or string/)
347
+ end
348
+
349
+ it 'rejects malformed profile imports and accepts JSON descriptions' do
350
+ expect do
351
+ described_class.new(description:, import_path: "bad\nsource", module_version: 'v1.0.0')
352
+ end.to raise_error(ArgumentError)
353
+ json_adapter = described_class.new(description: JSON.parse(JSON.generate(description)), import_path: 'example.test/sdk', module_version: 'v1.0.0')
354
+ expect(json_adapter.resolve(resource_path: %w[client child], action_name: 'client')).not_to be_nil
355
+ end
356
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-go-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.12
4
+ version: 0.30.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.29.12
18
+ version: 0.30.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.29.12
25
+ version: 0.30.1
26
26
  description: Go client generator
27
27
  email:
28
28
  - jakub.skokan@vpsfree.cz
@@ -36,6 +36,7 @@ files:
36
36
  - README.md
37
37
  - Rakefile
38
38
  - bin/haveapi-go-client
39
+ - docs/documentation.md
39
40
  - haveapi-go-client.gemspec
40
41
  - lib/haveapi/go_client.rb
41
42
  - lib/haveapi/go_client/action.rb
@@ -47,6 +48,7 @@ files:
47
48
  - lib/haveapi/go_client/authentication/unsupported.rb
48
49
  - lib/haveapi/go_client/authentication_methods.rb
49
50
  - lib/haveapi/go_client/cli.rb
51
+ - lib/haveapi/go_client/documentation.rb
50
52
  - lib/haveapi/go_client/erb_template.rb
51
53
  - lib/haveapi/go_client/generator.rb
52
54
  - lib/haveapi/go_client/i18n_messages.yml
@@ -61,9 +63,17 @@ files:
61
63
  - lib/haveapi/go_client/resource.rb
62
64
  - lib/haveapi/go_client/utils.rb
63
65
  - lib/haveapi/go_client/version.rb
66
+ - spec/documentation.Gemfile
67
+ - spec/documentation_acceptance.rb
68
+ - spec/documentation_build_check.rb
69
+ - spec/documentation_profile_check.rb
70
+ - spec/documentation_wire_check.rb
64
71
  - spec/integration/generator_spec.rb
65
72
  - spec/spec_helper.rb
73
+ - spec/support/documentation_fixture.rb
74
+ - spec/support/documentation_profile_fixture.rb
66
75
  - spec/support/test_server.rb
76
+ - spec/unit/documentation_spec.rb
67
77
  - template/action.go.erb
68
78
  - template/authentication.go.erb
69
79
  - template/authentication/basic.go.erb