haveapi-go-client 0.30.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 890df2231c3212cf60786444dca1049aaa7fa74a7b7d3b354aecefebc41d8972
4
- data.tar.gz: 5fe100b28f13414663f0cef2c382040c612c512d6f1caefcc2744519d637fa15
3
+ metadata.gz: 01cc7cc1dc1d35a4534b09c585f43d2e7cc580d578f9da56462c40dfa4902bdc
4
+ data.tar.gz: 729045a40f15fda3d22ea7af4afc349988c862760d45458bb36baec7304d9102
5
5
  SHA512:
6
- metadata.gz: 430d52e49783ab4b0699ce1ea03dbadc64a446ee1d29f71c114837e9013d1de60e17d70a9a41a149e258ad641858b0a7e3d1e50cb0fb220aa0d23b5685fabc5c
7
- data.tar.gz: a2b472cd256d13b824540947f1d708bfd09ace1c2a2814f1b882ca8325fb05882e75fd2086dee07443a308249860829ac1931557fa5e44a69512e902c7b34e4c
6
+ metadata.gz: 651e21690230e8ca09f40117c408de448c99b19801629d1a730fa80b450d5318f85b7d0166752b6429be71da72a1cf8c7ab076ee12add2eaa4f749ef8a26ce3f
7
+ data.tar.gz: 8f89d9988902469c2055155ebf7ba46674e985f1bf2f86e714c3d02e7c402d31474916fab7857d310ad5866695600a4bda08ebc5fae065a547afc5d5dbf55f23
@@ -1,52 +1,70 @@
1
1
  # Generated API documentation examples
2
2
 
3
- The Ruby server can show Go examples for a configured generated SDK. The SDK's
4
- module version is independent of the HaveAPI gem version and API version.
5
- Install the `haveapi-go-client` gem at the exact generator version used for the
6
- SDK, alongside the server gem. Configure the profile before mounting the API:
3
+ The Ruby server can show Go examples for a configured generated SDK. Install the
4
+ optional `haveapi-go-client` generator gem alongside the server gem. Register the
5
+ profile before mounting the API:
7
6
 
8
7
  ```ruby
9
- api.documentation_clients(
10
- '7',
11
- go: {
12
- import_path: 'example.org/widget-sdk/v2/client',
13
- module_version: 'v2.3.4',
14
- description_path: '/etc/widget-api/sdk-v7.json',
15
- generator_version: '0.30.0'
16
- }
8
+ api.documentation_clients('7',
9
+ go: { import_path: 'example.org/widget-sdk/v2/client' }
17
10
  )
11
+ api.mount('/api/')
18
12
  ```
19
13
 
20
- `import_path` identifies the published SDK package, including its package
21
- subdirectory. Examples import it with the explicit alias `client`. The local
22
- absolute JSON file must contain the complete description used to generate that
23
- SDK, including sibling resources, aliases and authentication descriptions.
24
- Retain this file with the SDK release; do not create it from a filtered public
25
- documentation page. The Go description has no API-version field. The configuration's version key
26
- binds the retained description to that API version.
27
- Keep the description private because it can include privileged API information.
28
-
29
- A missing profile hides the Go overview entry and all Go example panels. An
30
- invalid profile or a missing or mismatched generator gem fails configuration.
31
- The server loads the file and constructs the adapter during configuration. HTML
32
- requests do not read files, fetch API descriptions, generate source, invoke Go
33
- or write an SDK. Removing the profile disables Go examples without changing API
34
- behavior or existing clients.
14
+ `import_path` identifies the SDK package, including its package subdirectory.
15
+ Examples use the explicit import alias `client`. `generator_version` defaults
16
+ to the server's `HaveAPI::VERSION` and must match the installed generator gem.
17
+ An explicit value requires the installed generator to report that version. It
18
+ does not select the SDK version or download a generator.
19
+
20
+ `module_version` is optional. Omission or nil renders
21
+ `go get example.org/widget-sdk/v2/client@latest`; `module_version: 'v2.3.4'`
22
+ selects that version instead. Go resolves latest by its normal rules and existing
23
+ module configuration still applies. Latest can change over time. The server
24
+ never queries a package registry or claims to have resolved the installed SDK.
25
+
26
+ At the end of mount, the server builds one private structural model per
27
+ configured API version from the complete declaration, including authentication
28
+ routes and changes from post-mount hooks. The copy excludes prose, examples and
29
+ parameter defaults; canonical names still include every sibling and ancestor.
30
+ Requests use the current user's authorized help and samples. The configured SDK
31
+ is expected to match this generation contract. Its package name or version does
32
+ not prove that match; older SDKs can lack actions or use different collision names.
33
+
34
+ For an older or deliberately restricted SDK, add
35
+ `description_path: '/etc/widget-api/sdk-v7.json'`. This optional trusted absolute
36
+ JSON file contains the complete description used to generate that SDK, including
37
+ siblings, aliases and authentication descriptions. Retain it with the SDK; do
38
+ not reconstruct it from a filtered documentation page. The description has no
39
+ API-version field, so the profile's version key binds it to the selected version.
40
+ Keep saved descriptions private because they can contain privileged information.
41
+ The prior explicit `module_version`/`description_path`/`generator_version` form
42
+ remains supported.
43
+
44
+ A missing profile hides Go everywhere. Unknown fields, invalid profiles, missing
45
+ files and actual generator-version mismatches fail startup. Saved descriptions
46
+ are loaded during registration; automatic adapters are initialized at the end of
47
+ mount. No profiles are published when mount fails. HTML requests read no files,
48
+ fetch no descriptions and run no generator, compiler or formatter. Removing the
49
+ profile changes documentation availability without changing API wire behavior.
50
+ See the [server profile guide](../../../doc/client-examples.md) for configuration,
51
+ compatibility assumptions and rollback requirements.
35
52
 
36
53
  ## Mapping and supported samples
37
54
 
38
55
  `HaveAPI::GoClient::Documentation` constructs the generator's `ApiVersion` from
39
- the full saved description. Resource and action members, including aliases and
40
- collision suffixes, come from the generator's existing name allocator. The
41
- current user's filtered server description controls which samples and fields
42
- can appear. A saved SDK can lag unrelated server additions. Each rendered sample
43
- must still match its route, input and output layouts, and supplied field contracts.
56
+ the complete private description, whether automatic or saved. Resource and
57
+ action members, including aliases and collision suffixes, come from the
58
+ generator's existing name allocator. The current user's filtered server
59
+ description controls which samples and fields can appear. A saved SDK description
60
+ can lag unrelated server additions. Each rendered sample must still match its
61
+ route, input and output layouts, and supplied field contracts.
44
62
  Unsupported or stale samples show a short limitation instead of incomplete code.
45
63
 
46
64
  Programs initialize the client with the API origin. Saved action paths include
47
65
  the mounted root and selected `/vVERSION/` segment. Ordered sample path arguments
48
- become the generated `SetPathParamInt` or `SetPathParamString` calls. Object/hash input uses
49
- `NewInput().SelectParameters()` before setters; this preserves omitted fields,
66
+ become the generated `SetPathParamInt` or `SetPathParamString` calls. Object/hash
67
+ input uses `NewInput().SelectParameters()` before setters; this preserves omitted fields,
50
68
  explicit zero, false and empty strings. Nullable fields use `SetFIELDNil(true)`.
51
69
  An empty request selects no fields. A missing request leaves input unallocated.
52
70
 
@@ -78,9 +96,9 @@ The documentation adapter accepts loaded data and returns frozen plain results:
78
96
 
79
97
  ```ruby
80
98
  adapter = HaveAPI::GoClient::Documentation.new(
81
- description: saved_description,
99
+ description: full_description,
82
100
  import_path: 'example.org/widget-sdk/v2/client',
83
- module_version: 'v2.3.4'
101
+ module_version: nil # Optional; use 'v2.3.4' to select a version.
84
102
  )
85
103
  resolved = adapter.resolve(resource_path: %w[widget], action_name: 'update')
86
104
  result = adapter.render(kind: :example, context: context, mode: :sync)
@@ -91,7 +109,8 @@ adapter does not depend on the server gem. Results have either
91
109
  `{status: :code, code: source}` or `{status: :limitation, reason: text}`. Renderers
92
110
  return source/plain text; the server template escapes HTML once.
93
111
 
94
- For SDK fixtures or an already captured schema, use the offline constructor. It preserves the ordinary URL constructor's generation options:
112
+ For SDK fixtures or an already captured schema, use the offline constructor.
113
+ It preserves the ordinary URL constructor's generation options:
95
114
 
96
115
  ```ruby
97
116
  generator = HaveAPI::GoClient::Generator.from_description(
@@ -18,5 +18,5 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'haveapi-client', '~> 0.30.0'
21
+ spec.add_dependency 'haveapi-client', '~> 0.30.1'
22
22
  end
@@ -4,7 +4,7 @@ require 'haveapi/go_client'
4
4
  require 'haveapi/go_client/utils'
5
5
 
6
6
  module HaveAPI::GoClient
7
- # Pure adapter over the complete schema used to generate a published SDK.
7
+ # Pure adapter over a complete description matching the configured SDK.
8
8
  # Never reconstruct names from a user's reduced documentation description.
9
9
  class Documentation
10
10
  include Utils
@@ -15,15 +15,15 @@ module HaveAPI::GoClient
15
15
  PARAMETER_CONTRACT = %i[type nullable resource value_id value_label].freeze
16
16
  INPUT_MEMBERS = %w[SelectParameters UnselectParameters AnySelected].freeze
17
17
 
18
- def initialize(description:, import_path:, module_version:)
18
+ def initialize(description:, import_path:, module_version: nil)
19
19
  unless import_path.is_a?(String) && import_path.match?(%r{\A[A-Za-z0-9][A-Za-z0-9._~/-]*\z}) \
20
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/)
21
+ && (module_version.nil? || (module_version.is_a?(String) && module_version.match?(/\Av\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/)))
22
22
  raise ArgumentError, 'invalid Go SDK import path or module version'
23
23
  end
24
24
 
25
25
  @import_path = import_path.dup.freeze
26
- @module_version = module_version.dup.freeze
26
+ @module_version = module_version&.dup&.freeze
27
27
  @description = copy(description)
28
28
  validate_description
29
29
  @api = ApiVersion.new(@description)
@@ -433,7 +433,7 @@ module HaveAPI::GoClient
433
433
  origin = URI.parse(context.base_url)
434
434
  origin.path = ''
435
435
  origin.query = origin.fragment = nil
436
- header = ["// go get #{@import_path}@#{@module_version}", 'package main', '', 'import (']
436
+ header = ["// go get #{@import_path}@#{@module_version || 'latest'}", 'package main', '', 'import (']
437
437
  imports.each { |name| header << " #{go_string_literal(name)}" }
438
438
  header << " client #{go_string_literal(@import_path)}"
439
439
  header.push(')', '', 'func main() {', " api := client.New(#{go_string_literal(origin.to_s)})")
@@ -457,7 +457,14 @@ module HaveAPI::GoClient
457
457
  end
458
458
 
459
459
  def parameter_contract(desc)
460
- PARAMETER_CONTRACT.map { |key| desc[key] }
460
+ PARAMETER_CONTRACT.map do |key|
461
+ value = desc[key]
462
+ if %i[value_id value_label].include?(key) && (value.is_a?(String) || value.is_a?(Symbol))
463
+ value.to_s
464
+ else
465
+ value
466
+ end
467
+ end
461
468
  end
462
469
 
463
470
  def visible_action(description, path, name)
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module GoClient
3
- VERSION = '0.30.0'.freeze
3
+ VERSION = '0.30.1'.freeze
4
4
  end
5
5
  end
@@ -59,6 +59,7 @@ class GoDocumentationAcceptance
59
59
  generator_version: Gem.loaded_specs.fetch('haveapi-go-client').version.to_s
60
60
  })
61
61
  profiles.mount(['7'])
62
+ profiles.finalize(nil)
62
63
  raise 'configured Go profile is unavailable' unless profiles.for_version('7').fetch(:go).language == :go
63
64
  end
64
65
  end
@@ -125,6 +126,7 @@ class GoDocumentationAcceptance
125
126
  generator_version: HaveAPI::GoClient::VERSION
126
127
  })
127
128
  profiles.mount(['7'])
129
+ profiles.finalize(nil)
128
130
  @profile = profiles.for_version('7').fetch(:go)
129
131
  FileUtils.mkdir_p(File.join(dir, 'consumer'))
130
132
  FileUtils.mkdir_p(File.join(dir, 'bin'))
@@ -3,6 +3,7 @@ require_relative 'documentation_acceptance'
3
3
 
4
4
  class GoDocumentationBuildCheck
5
5
  def self.run!
6
+ GoDocumentationAcceptance.check_environment!
6
7
  Dir.mktmpdir('haveapi-go-documentation-build-') do |root|
7
8
  # Own this invalid ancestor marker; never rely on or alter ambient Git state.
8
9
  Dir.mkdir(File.join(root, '.git'))
@@ -0,0 +1,99 @@
1
+ # Pure mounted-DSL/profile regression. No generation, compiler or listener.
2
+ require 'json'
3
+ require 'tempfile'
4
+ require 'haveapi'
5
+ require 'haveapi/go_client/documentation'
6
+ require_relative 'support/documentation_profile_fixture'
7
+
8
+ class GoDocumentationProfileCheck
9
+ FIXTURE = GoDocumentationProfileFixture
10
+
11
+ def self.run!
12
+ new.run!
13
+ end
14
+
15
+ def run!
16
+ server = FIXTURE.server
17
+ profile = server.documentation_client_profiles('7').fetch(:go)
18
+ assert(profile.package_version.nil?, 'omitted module version must remain unresolved')
19
+ full = FIXTURE.description(server)
20
+ model = HaveAPI::GoClient::ApiVersion.new(full)
21
+ public_description = FIXTURE.description(server, user: :public)
22
+ privileged_description = FIXTURE.description(server, user: :privileged)
23
+ context = FIXTURE.context(public_description)
24
+ adapter = profile.adapter
25
+ structural = adapter.instance_variable_get(:@description).dig(:resources, :set_timeout_resource, :resources, :child, :actions, :client)
26
+ assert(!structural.has_key?(:examples), 'the private model must not retain example payloads')
27
+ assert(!structural.dig(:input, :parameters, :private_value).has_key?(:default), 'the private model must not retain parameter defaults')
28
+ resolved = adapter.resolve(resource_path: context.resource_path, action_name: 'perform')
29
+ canonical = model.resources.find { |resource| resource.name == 'set_timeout_resource' }
30
+ assert(resolved.resource.parent.go_member_name == canonical.go_member_name, 'full-DSL sibling allocation must be canonical')
31
+ assert(canonical.go_member_name != 'SetTimeoutResource', 'hidden reserved-name sibling must affect allocation')
32
+ assert(resolved.action.go_member_name == 'ClientAction', 'nested action must keep the generator collision name')
33
+
34
+ source = code(adapter, context)
35
+ assert(source.start_with?("// go get example.test/current-sdk/client@latest\n"), 'omitted version must emit valid latest selection')
36
+ assert(source.include?('client "example.test/current-sdk/client"'), 'SDK import alias must stay explicit')
37
+ assert(source.include?("api.#{canonical.go_member_name}.Child.ClientAction.Prepare()"), 'rendered nested members must use the full model')
38
+ assert(source.include?('SetPathParamString("parent_id", "parent/with space")'), 'ordered string path argument was lost')
39
+ assert(source.include?('SetPathParamInt("child_id", 0)'), 'ordered zero path argument was lost')
40
+ assert(source.include?('input := invocation.NewInput().SelectParameters()'), 'input omission selection must stay explicit')
41
+ assert(source.include?('input.SetEnabled(false)') && source.include?('input.SetCount(0)'), 'false/zero inputs were lost')
42
+ assert(source.include?("input.SetName(#{JSON.generate(FIXTURE::HOSTILE)})"), 'hostile source literal was not escaped')
43
+ assert(source.include?('response.Output.Owner.Id') && !source.include?('Owner.Label'), 'nested output must use only supplied attributes')
44
+ assert(source.include?('// metadata.SetIncludes(value)'), 'advertised optional includes guidance must remain')
45
+ assert(!source.match?(/private sample|private result|private default|SetPrivateValue|SetUnused|\.PrivateValue/), 'filtered fields or values leaked')
46
+
47
+ privileged = code(adapter, FIXTURE.context(privileged_description))
48
+ assert(privileged.include?('input.SetPrivateValue("private sample")'), 'authorized supplied field was lost')
49
+ assert(code(adapter, FIXTURE.context(public_description)) == source, 'sequential users must not change the shared model')
50
+ hidden = full.fetch(:resources).fetch('set_timeout').fetch(:actions).fetch('secret')
51
+ hidden_context = context.with(resource_path: ['set_timeout'], action_name: 'secret', action: hidden,
52
+ sample: { status: true, response: { private_value: 'must not leak' } })
53
+ limited(adapter, hidden_context, 'unavailable')
54
+
55
+ root_context = FIXTURE.context(public_description, path: ['set_timeout_resource'], action_name: 'child')
56
+ root_source = code(adapter, root_context)
57
+ assert(root_source.include?("api.#{canonical.go_member_name}.ChildAction.Prepare()"), 'resource/action collision was reconstructed incorrectly')
58
+ assert(root_source.include?('input.SetNameNil(true)'), 'nullable supplied input must use the nil setter')
59
+
60
+ pinned = FIXTURE.server(module_version: 'v2.3.4', generator_version: HaveAPI::GoClient::VERSION)
61
+ pinned_adapter = pinned.documentation_client_profiles('7').fetch(:go).adapter
62
+ pinned_source = code(pinned_adapter, FIXTURE.context(FIXTURE.description(pinned, user: :public)))
63
+ assert(pinned_source == source.sub('@latest', '@v2.3.4'), 'explicit version must preserve all other rendering')
64
+ check_legacy(full, source)
65
+ $stdout.write("Go documentation profiles: current mounted DSL, optional/pinned versions, legacy data and authorized rendering passed\n") # rubocop:disable RSpec/Output
66
+ end
67
+
68
+ private
69
+
70
+ def check_legacy(full, current_source)
71
+ Tempfile.create(['haveapi-go-profile-', '.json']) do |file|
72
+ file.write(JSON.generate(full))
73
+ file.flush
74
+ server = FIXTURE.server(description_path: file.path, module_version: 'v2.3.4',
75
+ generator_version: HaveAPI::GoClient::VERSION)
76
+ adapter = server.documentation_client_profiles('7').fetch(:go).adapter
77
+ context = FIXTURE.context(FIXTURE.description(server, user: :public))
78
+ assert(code(adapter, context) == current_source.sub('@latest', '@v2.3.4'), 'legacy saved profile must preserve current matching rendering')
79
+ end
80
+ end
81
+
82
+ def code(adapter, context)
83
+ result = adapter.render(kind: :example, context:, mode: :sync)
84
+ assert(result[:status] == :code, "unexpected limitation: #{result[:reason]}")
85
+ result.fetch(:code)
86
+ end
87
+
88
+ def limited(adapter, context, reason)
89
+ result = adapter.render(kind: :example, context:, mode: :sync)
90
+ assert(result[:status] == :limitation && result[:reason].include?(reason), 'hidden action must remain unavailable')
91
+ assert(!result.to_s.include?('must not leak'), 'limitation disclosed the hidden sample')
92
+ end
93
+
94
+ def assert(condition, message)
95
+ raise message unless condition
96
+ end
97
+ end
98
+
99
+ GoDocumentationProfileCheck.run! if $0 == __FILE__
@@ -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
@@ -15,6 +15,46 @@ RSpec.describe HaveAPI::GoClient::Documentation do
15
15
  adapter.render(kind: :example, context:, mode: :sync)
16
16
  end
17
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
+
18
58
  it 'resolves canonical generator members and aliases without generator or serving I/O' do
19
59
  allow(HaveAPI::GoClient::Generator).to receive(:new).and_call_original
20
60
  allow(HaveAPI::GoClient::Generator).to receive(:allocate).and_call_original
@@ -261,6 +301,36 @@ RSpec.describe HaveAPI::GoClient::Documentation do
261
301
  expect(render).to include(status: :limitation, reason: /requires an object/)
262
302
  end
263
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
+
264
334
  it 'does not print associated fields missing from another documented list row' do
265
335
  action[:output][:layout] = 'hash_list'
266
336
  action[:output][:parameters][:owner] = { type: 'Resource', resource: ['owner'], value_id: 'id', value_label: 'label' }
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.30.0
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.30.0
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.30.0
25
+ version: 0.30.1
26
26
  description: Go client generator
27
27
  email:
28
28
  - jakub.skokan@vpsfree.cz
@@ -66,10 +66,12 @@ files:
66
66
  - spec/documentation.Gemfile
67
67
  - spec/documentation_acceptance.rb
68
68
  - spec/documentation_build_check.rb
69
+ - spec/documentation_profile_check.rb
69
70
  - spec/documentation_wire_check.rb
70
71
  - spec/integration/generator_spec.rb
71
72
  - spec/spec_helper.rb
72
73
  - spec/support/documentation_fixture.rb
74
+ - spec/support/documentation_profile_fixture.rb
73
75
  - spec/support/test_server.rb
74
76
  - spec/unit/documentation_spec.rb
75
77
  - template/action.go.erb