haveapi-go-client 0.30.0 → 0.30.2
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/docs/documentation.md +56 -37
- data/haveapi-go-client.gemspec +1 -1
- data/lib/haveapi/go_client/documentation.rb +13 -6
- data/lib/haveapi/go_client/version.rb +1 -1
- data/spec/documentation_acceptance.rb +2 -0
- data/spec/documentation_build_check.rb +1 -0
- data/spec/documentation_profile_check.rb +99 -0
- data/spec/support/documentation_profile_fixture.rb +122 -0
- data/spec/unit/documentation_spec.rb +70 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d7b665ff9866f57fd844908a8ee6d8ce83bf784578949ade2a057e465f50a0c
|
|
4
|
+
data.tar.gz: 549ecd828535d2a740758805ba7e04494e963a1e67acdf7c76a05eaaa2dcb6eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae6a2f23d846e821d03ab0b9aa6c9cd64680d35db0c4996df2a1cbeea432a499cee1f6be57e86700282a297a73d82863e811e02d9216bfbbba0d868b2750255a
|
|
7
|
+
data.tar.gz: 39db10f2394870b215d28ad44a142318694d5ff295810c38e4407dda43677f73907e90c969dfb08228ba698096cc7cd9a3aa624a5b7839b8fe69db239224ce4e
|
data/docs/documentation.md
CHANGED
|
@@ -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.
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
'
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
-
collision suffixes, come from the
|
|
41
|
-
|
|
42
|
-
can appear. A saved SDK
|
|
43
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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(
|
data/haveapi-go-client.gemspec
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
|
@@ -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.
|
|
4
|
+
version: 0.30.2
|
|
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.
|
|
18
|
+
version: 0.30.2
|
|
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.
|
|
25
|
+
version: 0.30.2
|
|
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
|