haveapi 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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Rakefile +1 -0
  4. data/doc/client-examples.md +198 -0
  5. data/haveapi.gemspec +1 -1
  6. data/lib/haveapi/client_example.rb +47 -1
  7. data/lib/haveapi/client_examples/dynamic_helpers.rb +129 -0
  8. data/lib/haveapi/client_examples/go_client.rb +37 -0
  9. data/lib/haveapi/client_examples/js_async_client.rb +14 -0
  10. data/lib/haveapi/client_examples/js_client.rb +179 -135
  11. data/lib/haveapi/client_examples/python_async_client.rb +14 -0
  12. data/lib/haveapi/client_examples/python_client.rb +172 -0
  13. data/lib/haveapi/client_examples/rust_client.rb +59 -0
  14. data/lib/haveapi/client_examples/ts_async_client.rb +14 -0
  15. data/lib/haveapi/client_examples/ts_client.rb +14 -0
  16. data/lib/haveapi/documentation_clients.rb +249 -0
  17. data/lib/haveapi/documentation_context.rb +47 -0
  18. data/lib/haveapi/documentation_schema.rb +73 -0
  19. data/lib/haveapi/public/js/highlight-LICENSE +29 -0
  20. data/lib/haveapi/public/js/highlight.pack.js +2 -2
  21. data/lib/haveapi/public/js/highlighter.js +16 -6
  22. data/lib/haveapi/public/js/main.js +9 -1
  23. data/lib/haveapi/public/js/nojs-tabs.js +37 -3
  24. data/lib/haveapi/server.rb +39 -0
  25. data/lib/haveapi/version.rb +1 -1
  26. data/lib/haveapi/views/main_layout.erb +11 -0
  27. data/lib/haveapi/views/version_page/auth_body.erb +3 -3
  28. data/lib/haveapi/views/version_page/client_auth.erb +2 -2
  29. data/lib/haveapi/views/version_page/client_example.erb +2 -11
  30. data/lib/haveapi/views/version_page/client_init.erb +2 -2
  31. data/lib/haveapi/views/version_page/client_result.erb +5 -0
  32. data/lib/haveapi/views/version_page/resource_body.erb +3 -7
  33. data/lib/haveapi/views/version_page.erb +13 -9
  34. data/spec/client_examples/assets/highlighter_test.cjs +26 -0
  35. data/spec/client_examples/assets/tabs_test.cjs +54 -0
  36. data/spec/client_examples/context_spec.rb +68 -0
  37. data/spec/client_examples/dynamic_acceptance.md +83 -0
  38. data/spec/client_examples/dynamic_acceptance.rb +151 -0
  39. data/spec/client_examples/dynamic_acceptance_spec.rb +76 -0
  40. data/spec/client_examples/dynamic_fixture.rb +36 -0
  41. data/spec/client_examples/dynamic_js_spec.rb +196 -0
  42. data/spec/client_examples/dynamic_metadata_spec.rb +52 -0
  43. data/spec/client_examples/dynamic_mounted_paths_spec.rb +65 -0
  44. data/spec/client_examples/dynamic_python_spec.rb +123 -0
  45. data/spec/client_examples/go_spec.rb +57 -0
  46. data/spec/client_examples/profile_mount_spec.rb +215 -0
  47. data/spec/client_examples/profiles_spec.rb +265 -0
  48. data/spec/client_examples/rust_client_spec.rb +75 -0
  49. data/spec/client_examples/structural_schema_spec.rb +48 -0
  50. data/spec/client_examples/syntax_spec.rb +2 -1
  51. data/spec/documentation/auth_filtering_spec.rb +12 -0
  52. data/spec/documentation/client_examples_spec.rb +92 -0
  53. data/spec/packaging_spec.rb +13 -4
  54. data/test_support/dynamic_documentation_api.rb +171 -0
  55. data/test_support/dynamic_protocol_spec.rb +133 -0
  56. data/test_support/dynamic_python_login_spec.rb +75 -0
  57. metadata +37 -4
  58. data/lib/haveapi/client_examples/fs_client.rb +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f9214742c7d4ef64a37a4a0891ebe1dc492d551f06ce0956e0b2993e432dad6
4
- data.tar.gz: f1a203af566678c875c2fa347d9a3fbfcc131024bf8ec7f9fc95c46dad79a186
3
+ metadata.gz: 3d58d9e427d490d025fec95d60739ea4aa17b4e86cc3fb9978b7c00b751e6e75
4
+ data.tar.gz: e0b958ac9924d4cacaf75513d79690c3ed2519d6b5d1522a0433d92285742887
5
5
  SHA512:
6
- metadata.gz: 63418943311b0f40e2b74060976a04c309141c9496d68dca6fe41607ece1beecc19a4309213666b1d427aae0670e4b1c5a77c4bed0482da96b2c52e6a218426b
7
- data.tar.gz: b4dfa50972de0704f29f49c4d1f964643da27982e6123d7361ad3475e28441b139da3099d4aa9db53051e4cc1a95bd697be06e616f365a06bd9c1677efd42978
6
+ metadata.gz: 78980868da2569146dccf39ce14309e27b97d51b4a266b7709813faafb191577520f31408dd6ebf1e5310b37bd43197e9b4e838503cd60c5d8b480607e8c5f6d
7
+ data.tar.gz: 5a2a6ad3e686e3d77b86fb83e7e27b5eaf5e2ac573013fa938255b203405fdabe899a605f71cea8088769e81f323b6678be00272889c56d629757356d9170d87
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  doc/protocol.*
2
2
  doc/create-client.md
3
3
  doc/typed-input-validation.md
4
+ doc/client-examples.md
4
5
  doc/json-schema.html
data/Rakefile CHANGED
@@ -13,6 +13,7 @@ task :check_doc do
13
13
  doc/protocol.md
14
14
  doc/create-client.md
15
15
  doc/typed-input-validation.md
16
+ doc/client-examples.md
16
17
  doc/protocol.png
17
18
  ]
18
19
  missing = required.reject { |path| File.file?(path) && File.size?(path) }
@@ -0,0 +1,198 @@
1
+ # Client examples in API documentation
2
+
3
+ The Ruby server renders examples from the current user's API description.
4
+ JavaScript and TypeScript examples cover callback and async clients; Python
5
+ examples cover sync and async clients. These clients need no server configuration.
6
+ Ruby, PHP, CLI, curl and HTTP examples are also available. A sample
7
+ that a client cannot represent shows a limitation instead of dropping input or
8
+ inventing a successful result.
9
+
10
+ Go and Rust tabs require a profile for a generated SDK. Their package versions
11
+ are independent of HaveAPI and the API version. Without a profile, the server
12
+ hides those clients from the overview and all initialization, authentication and
13
+ action panels. Install the matching generator gem only when configuring its
14
+ profile; ordinary servers do not depend on either generator.
15
+
16
+ ## Configure a generated SDK
17
+
18
+ Register profiles before mounting the server. For an SDK that matches the current
19
+ API, configure its package identity:
20
+
21
+ ```ruby
22
+ api.documentation_clients('1.0',
23
+ go: {
24
+ import_path: 'example.org/widget/client'
25
+ },
26
+ rust: {
27
+ crate_name: 'widget-client'
28
+ }
29
+ )
30
+ api.mount('/api/')
31
+ ```
32
+
33
+ `generator_version` defaults to the server's `HaveAPI::VERSION`. Install the
34
+ optional `haveapi-go-client` or `haveapi-rust-client` generator gem at that
35
+ version. An explicit `generator_version` requires the installed generator to
36
+ report that version; it never downloads one. It is independent of the SDK and
37
+ API versions.
38
+
39
+ At the end of mount, the server builds one private structural model per configured
40
+ API version. The model includes authentication resources, routes and changes from
41
+ post-mount hooks, so canonical names and parent arguments use the complete API
42
+ declaration. The private copy excludes examples, prose and parameter defaults.
43
+ HTML requests use the current user's authorized description, fields and samples;
44
+ they never expose the private model or its initialization diagnostics. Without
45
+ a configured profile, the server loads no optional generator and shows no Go or
46
+ Rust panels.
47
+
48
+ Automatic profiles assume that the installed SDK matches the current server's
49
+ generation contract. A package name or version does not prove its contents.
50
+ An older SDK can lack new actions or use different names after a collision or
51
+ route change. Check actual SDK compatibility before advertising it; use a saved
52
+ schema override below when the SDK represents an older or restricted API.
53
+
54
+ ### Optional SDK versions and sources
55
+
56
+ Set `module_version: 'v1.2.3'` for Go or `crate_version: '1.2.3'` for Rust when a
57
+ specific package version is needed. Omit the field or use nil to let the user's
58
+ package manager select a release. The server does not query a registry or claim
59
+ to have resolved a version. Omitted versions produce these installation hints:
60
+
61
+ ```sh
62
+ go get example.org/widget/client@latest
63
+ cargo add widget-client
64
+ # Blocking Rust variant:
65
+ cargo add widget-client --features blocking
66
+ ```
67
+
68
+ Go resolves `@latest` by its normal rules. Cargo selects a compatible release
69
+ according to its resolver, manifest and toolchain; existing lockfiles still
70
+ apply. Explicit Cargo versions remain version requirements, without an added
71
+ exact `=` pin. Latest selection can change over time; supply a version to control
72
+ the selection. Cargo never receives a literal `version = "latest"`. Rust
73
+ examples also show their Tokio and serde_json requirements.
74
+
75
+ For an unpublished Rust SDK, supply an HTTPS Git URL and a full commit SHA:
76
+
77
+ ```ruby
78
+ api.documentation_clients('7.0',
79
+ rust: {
80
+ crate_name: 'vpsadmin-client',
81
+ source: {
82
+ git: 'https://github.com/vpsfreecz/vpsadmin-rust-client',
83
+ rev: 'ff876abe17e3301da62c1b6afbda055ec89db9d2'
84
+ }
85
+ }
86
+ )
87
+ ```
88
+
89
+ The dependency hint uses that source instead of a registry release:
90
+
91
+ ```toml
92
+ # Cargo.toml
93
+ vpsadmin-client = { git = "https://github.com/vpsfreecz/vpsadmin-rust-client", rev = "ff876abe17e3301da62c1b6afbda055ec89db9d2" }
94
+ # The blocking variant also sets features = ["blocking"].
95
+ ```
96
+
97
+ `source` accepts only `git` and `rev`. URLs must be absolute HTTPS without
98
+ credentials, query strings, fragments or control characters; revisions must be
99
+ 40 hexadecimal characters. Branches, tags, local paths and custom registries are
100
+ not supported. An explicit crate version additionally constrains the selected
101
+ source package. The server never fetches the repository or runs these user
102
+ installation commands. These settings do not regenerate or publish an SDK and
103
+ do not establish that this particular SDK matches a given API surface.
104
+
105
+ ### Saved-schema overrides
106
+
107
+ The existing file-based profiles remain supported:
108
+
109
+ ```ruby
110
+ api.documentation_clients('1.0',
111
+ go: {
112
+ import_path: 'example.org/widget/client',
113
+ module_version: 'v1.2.3',
114
+ description_path: '/srv/widget-sdk/api-1.0.json',
115
+ generator_version: '0.30.0'
116
+ },
117
+ rust: {
118
+ snapshot_path: '/srv/widget-sdk/api-1.0.snapshot.json',
119
+ generator_config_path: '/srv/widget-sdk/generator.toml',
120
+ generator_version: '0.30.0'
121
+ }
122
+ )
123
+ ```
124
+
125
+ Paths name trusted absolute local files. `description_path` is Go's saved
126
+ machine-readable description, not HTML. `snapshot_path` is Rust's saved schema
127
+ and capture metadata. They select an exact SDK shape in place of the current
128
+ server declaration. Keep the files private and retain them with the SDK release.
129
+
130
+ Rust's legacy `generator_config_path` supplies the crate name/version and full
131
+ packaging configuration. It is mutually exclusive with inline `crate_name` and
132
+ `crate_version`, and can be paired with either a saved snapshot or automatic
133
+ schema. A saved snapshot can instead use inline crate identity. `source` works
134
+ with either identity form. The server validates the full legacy TOML; normal
135
+ inline documentation needs no license, compiler or formatter settings.
136
+
137
+ Saved files are read and validated during registration. Automatic models are
138
+ initialized at the end of mount. Missing files or generators, actual generator
139
+ version mismatches, unknown fields, conflicting identities and invalid schemas
140
+ fail startup. Mounting also rejects unavailable API versions and mismatched Rust
141
+ snapshot versions. Failed mounting publishes no profiles and leaves no usable
142
+ server application. An older explicitly pinned generator can serve a suitable
143
+ legacy file profile, but automatic/inline Rust mode requires the new factory
144
+ interface and never guesses a fallback.
145
+
146
+ A saved SDK can still serve examples after unrelated live additions. Each sample
147
+ must match its saved route, layout and supplied parameter contracts; unavailable
148
+ or stale samples show a limitation. Hidden actions, fields, defaults and samples
149
+ remain hidden from the current user. The server never fetches descriptions,
150
+ regenerates packages or runs a compiler/formatter during an HTML request.
151
+
152
+ Old profile settings work on the new server. Inline settings require the new
153
+ server code; rolling back that code also requires restoring compatible settings.
154
+ Mixed server workers can show different documentation examples without changing
155
+ wire requests or persisted data.
156
+
157
+ ## Renderer extensions
158
+
159
+ Existing renderer positional constructors and `init`, `auth(method, desc)` and
160
+ `example(sample)` or `request(sample)`/`response(sample)` methods still work.
161
+ The registry binds a request-local `HaveAPI::ClientExample::Context` through
162
+ `with_context`. It holds the selected version, URLs including the API mount,
163
+ the filtered description, and the current resource, action and sample.
164
+ `context.with(...)` creates another frozen copy without freezing the server's
165
+ shared description.
166
+
167
+ Renderers may return a String or nil through their existing methods. New
168
+ renderers return either `{status: :code, code: source}` or
169
+ `{status: :limitation, reason: text}`. Source and limitation text are plain
170
+ strings, escaped once by the templates. A stable class `id` identifies a
171
+ variant; `code` selects its syntax language. `available?(context)` controls the
172
+ overview and every panel before rendering.
173
+
174
+ Generated-client adapters belong to their generator gems and consume context
175
+ readers without depending on the server gem. They resolve names using the
176
+ complete private generator model and never run a compiler, formatter or generator
177
+ in an HTML request. Keep adapter rendering read-only: configured adapters are frozen
178
+ and shared, while filtered contexts are copied per request.
179
+
180
+ ## Verification
181
+
182
+ `make test-documentation` checks renderers, contexts, profiles, rendered HTML and
183
+ local assets. Prepare the component bundles and JavaScript development
184
+ dependencies with `make release-deps` first. The target builds the JavaScript
185
+ package before compiling TypeScript examples against its public declarations;
186
+ the Ruby server CI uses the same order.
187
+
188
+ `make test-documentation-python` additionally checks the Python renderer limits
189
+ against the real request encoder. It requires the locked Python environment
190
+ prepared by `make release-deps` and is included in `make test`. The ordinary
191
+ Ruby renderer checks run without the Python package environment.
192
+
193
+ Each client also has acceptance commands that check executable snippets against
194
+ installed packages or a generated SDK. These use
195
+ rendered source against disposable fixtures; syntax checks alone do not
196
+ establish that client calls or authentication work. See each client's
197
+ development documentation for its test environment. Highlighter rebuilding is
198
+ described in [utils/highlight](../utils/highlight/README.md).
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.29.12'
18
+ s.add_dependency 'haveapi-client', '~> 0.30.1'
19
19
  s.add_dependency 'i18n', '>= 1.6', '< 2'
20
20
  s.add_dependency 'json'
21
21
  s.add_dependency 'mail'
@@ -33,6 +33,47 @@ module HaveAPI
33
33
  @order
34
34
  end
35
35
 
36
+ def id(value = nil)
37
+ @id = value.to_s.freeze unless value.nil?
38
+ return @id if @id
39
+
40
+ # Preserve the anchors of the retained built-in formats.
41
+ { 'RubyClient' => '0', 'PhpClient' => '2', 'RubyCli' => '3', 'Curl' => '5', 'Http' => '6' }[name.to_s.split('::').last] ||
42
+ name.to_s.gsub('::', '-').downcase
43
+ end
44
+
45
+ def available?(_context)
46
+ true
47
+ end
48
+
49
+ def render(kind, context:, auth_method: nil, auth_description: nil)
50
+ arguments = [context.host, context.base_url, context.version]
51
+ arguments.push(context.resource_path, context.resource, context.action_name, context.action) if kind == :example
52
+ renderer = new(*arguments)
53
+ renderer.with_context(context)
54
+ result = case kind
55
+ when :init then renderer.init
56
+ when :auth then renderer.auth(auth_method, auth_description)
57
+ when :example
58
+ if renderer.respond_to?(:example)
59
+ renderer.example(context.sample)
60
+ else
61
+ [renderer.request(context.sample), renderer.response(context.sample)].compact.join("\n\n")
62
+ end
63
+ else raise ArgumentError, "Unknown client example kind: #{kind}"
64
+ end
65
+ return if result.nil?
66
+ return { status: :code, code: result.dup.freeze }.freeze if result.is_a?(String)
67
+
68
+ if result.is_a?(Hash) && result[:status] == :code && result[:code].is_a?(String)
69
+ { status: :code, code: result[:code].dup.freeze }.freeze
70
+ elsif result.is_a?(Hash) && result[:status] == :limitation && result[:reason].is_a?(String)
71
+ { status: :limitation, reason: result[:reason].dup.freeze }.freeze
72
+ else
73
+ raise ArgumentError, 'Invalid client example result'
74
+ end
75
+ end
76
+
36
77
  def register(klass)
37
78
  @clients ||= []
38
79
  @clients << klass
@@ -59,7 +100,7 @@ module HaveAPI
59
100
  end
60
101
  end
61
102
 
62
- attr_reader :resource_path, :resource, :action_name, :action, :host, :base_url, :version
103
+ attr_reader :resource_path, :resource, :action_name, :action, :host, :base_url, :version, :context
63
104
 
64
105
  def initialize(host, base_url, version, *args)
65
106
  @host = host
@@ -72,6 +113,11 @@ module HaveAPI
72
113
 
73
114
  def auth(method, desc); end
74
115
 
116
+ def with_context(value)
117
+ @context = value
118
+ self
119
+ end
120
+
75
121
  def version_url
76
122
  File.join(base_url, "v#{version}", '/')
77
123
  end
@@ -0,0 +1,129 @@
1
+ require 'json'
2
+ require 'haveapi/client_example'
3
+
4
+ module HaveAPI::ClientExamples
5
+ module DynamicHelpers
6
+ class Unsupported < StandardError; end
7
+
8
+ def field(value, name, default = nil)
9
+ return default unless value.is_a?(Hash)
10
+
11
+ value.fetch(name) { value.fetch(name.to_s, default) }
12
+ end
13
+
14
+ def code_result
15
+ { status: :code, code: yield.freeze }.freeze
16
+ rescue Unsupported, JSON::GeneratorError => e
17
+ { status: :limitation, reason: e.message.freeze }.freeze
18
+ end
19
+
20
+ def limitation(reason)
21
+ { status: :limitation, reason: reason.freeze }.freeze
22
+ end
23
+
24
+ def api_base
25
+ context ? context.base_url : base_url
26
+ end
27
+
28
+ def api_version
29
+ context ? context.version : version.to_s
30
+ end
31
+
32
+ def current_action
33
+ context&.action || action
34
+ end
35
+
36
+ def current_resource_path
37
+ context&.resource_path || resource_path
38
+ end
39
+
40
+ def current_action_name
41
+ context&.action_name || action_name
42
+ end
43
+
44
+ def json_data(value)
45
+ case value
46
+ when Hash
47
+ result = {}
48
+ value.each do |key, entry|
49
+ unless key.is_a?(String) || key.is_a?(Symbol)
50
+ raise Unsupported, 'This sample has a non-string object key.'
51
+ end
52
+ raise Unsupported, 'This sample has duplicate serialized object keys.' if result.has_key?(key.to_s)
53
+
54
+ result[key.to_s] = json_data(entry)
55
+ end
56
+ result
57
+ when Array
58
+ value.map { |entry| json_data(entry) }
59
+ when String, Integer, TrueClass, FalseClass, NilClass
60
+ value
61
+ when Float
62
+ raise Unsupported, 'This sample has a non-finite number.' unless value.finite?
63
+
64
+ value
65
+ else
66
+ raise Unsupported, 'This sample contains a value without a supported literal representation.'
67
+ end
68
+ end
69
+
70
+ def structured_sample(sample)
71
+ unless sample.is_a?(Hash) && (!field(sample, :request).nil? || !field(sample, :response).nil?)
72
+ raise Unsupported, 'This example contains prose only; no executable request is supplied.'
73
+ end
74
+
75
+ %i[global object].each do |scope|
76
+ next if scope == :object && field(current_action, :input).nil?
77
+
78
+ input = field(field(field(current_action, :meta), scope), :input)
79
+ if field(input, :parameters, {}).values.any? { |parameter| field(parameter, :required) }
80
+ raise Unsupported, 'This action requires input metadata that the sample format cannot supply.'
81
+ end
82
+ end
83
+
84
+ placeholders = field(current_action, :path, '').scan(/\{([a-zA-Z0-9_-]+)\}/).flatten
85
+ values = field(sample, :path_params, []) || []
86
+ unless values.is_a?(Array) && values.length == placeholders.length
87
+ raise Unsupported, 'The sample does not supply every action path argument.'
88
+ end
89
+ unless values.all? { |value| value.is_a?(String) || value.is_a?(Integer) }
90
+ raise Unsupported, 'Action path arguments must be strings or integers.'
91
+ end
92
+
93
+ request = field(sample, :request)
94
+ input = field(current_action, :input)
95
+ if !request.nil? && input.nil? && request != {}
96
+ raise Unsupported, 'The sample supplies input for an action without an input description.'
97
+ end
98
+
99
+ if request.is_a?(Hash) && input
100
+ known = field(input, :parameters, {}).keys.map(&:to_s)
101
+ unless (request.keys.map(&:to_s) - known).empty?
102
+ raise Unsupported, 'The sample contains input parameters absent from the visible description.'
103
+ end
104
+ end
105
+ [placeholders.zip(values).to_h, json_data(request)]
106
+ end
107
+
108
+ def supported_auth?(method)
109
+ %i[basic token oauth2].include?(method.to_sym) &&
110
+ (!context || field(context.description, :authentication, {}).keys.map(&:to_s).include?(method.to_s))
111
+ end
112
+
113
+ def token_request_fields(description)
114
+ token = field(field(description, :resources, {}), :token, {})
115
+ request = field(field(token, :actions, {}), :request, {})
116
+ field(field(request, :input, {}), :parameters, {})
117
+ end
118
+
119
+ def token_login_fields(description)
120
+ token_request_fields(description).reject do |name, _|
121
+ %w[token lifetime interval].include?(name.to_s)
122
+ end
123
+ end
124
+
125
+ def indent(source, spaces = 2)
126
+ source.lines.map { |line| line.strip.empty? ? line : "#{' ' * spaces}#{line}" }.join
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,37 @@
1
+ require 'haveapi/client_example'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class GoClient < HaveAPI::ClientExample
5
+ label 'Go'
6
+ id 'go'
7
+ code :go
8
+ order 16
9
+
10
+ def self.available?(context)
11
+ context.profile && context.profile.language == :go
12
+ end
13
+
14
+ def init
15
+ render_documentation(:init)
16
+ end
17
+
18
+ def auth(method, desc)
19
+ render_documentation(:auth, auth_method: method, auth_description: desc)
20
+ end
21
+
22
+ def example(sample)
23
+ render_documentation(:example, sample:)
24
+ end
25
+
26
+ protected
27
+
28
+ def render_documentation(kind, sample: nil, **)
29
+ unless context && self.class.available?(context)
30
+ return { status: :limitation, reason: 'A Go SDK profile is required.' }.freeze
31
+ end
32
+
33
+ selected = kind == :example ? context.with(sample:) : context
34
+ context.profile.adapter.render(kind:, context: selected, mode: :sync, **)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ require 'haveapi/client_examples/js_client'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class JsAsyncClient < JsClient
5
+ label 'JavaScript (async)'
6
+ id 'javascript-async'
7
+ code :javascript
8
+ order 11
9
+
10
+ def asynchronous?
11
+ true
12
+ end
13
+ end
14
+ end