haveapi 0.29.9 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Rakefile +1 -0
  4. data/doc/client-examples.md +96 -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 +126 -0
  17. data/lib/haveapi/documentation_context.rb +47 -0
  18. data/lib/haveapi/public/js/highlight-LICENSE +29 -0
  19. data/lib/haveapi/public/js/highlight.pack.js +2 -2
  20. data/lib/haveapi/public/js/highlighter.js +16 -6
  21. data/lib/haveapi/public/js/main.js +9 -1
  22. data/lib/haveapi/public/js/nojs-tabs.js +37 -3
  23. data/lib/haveapi/server.rb +34 -0
  24. data/lib/haveapi/version.rb +1 -1
  25. data/lib/haveapi/views/main_layout.erb +11 -0
  26. data/lib/haveapi/views/version_page/auth_body.erb +3 -3
  27. data/lib/haveapi/views/version_page/client_auth.erb +2 -2
  28. data/lib/haveapi/views/version_page/client_example.erb +2 -11
  29. data/lib/haveapi/views/version_page/client_init.erb +2 -2
  30. data/lib/haveapi/views/version_page/client_result.erb +5 -0
  31. data/lib/haveapi/views/version_page/resource_body.erb +3 -7
  32. data/lib/haveapi/views/version_page.erb +13 -9
  33. data/spec/client_examples/assets/highlighter_test.cjs +26 -0
  34. data/spec/client_examples/assets/tabs_test.cjs +54 -0
  35. data/spec/client_examples/context_spec.rb +68 -0
  36. data/spec/client_examples/dynamic_acceptance.md +83 -0
  37. data/spec/client_examples/dynamic_acceptance.rb +151 -0
  38. data/spec/client_examples/dynamic_acceptance_spec.rb +76 -0
  39. data/spec/client_examples/dynamic_fixture.rb +36 -0
  40. data/spec/client_examples/dynamic_js_spec.rb +195 -0
  41. data/spec/client_examples/dynamic_metadata_spec.rb +52 -0
  42. data/spec/client_examples/dynamic_mounted_paths_spec.rb +65 -0
  43. data/spec/client_examples/dynamic_python_spec.rb +123 -0
  44. data/spec/client_examples/go_spec.rb +57 -0
  45. data/spec/client_examples/profiles_spec.rb +95 -0
  46. data/spec/client_examples/rust_client_spec.rb +75 -0
  47. data/spec/client_examples/syntax_spec.rb +2 -1
  48. data/spec/documentation/auth_filtering_spec.rb +12 -0
  49. data/spec/documentation/client_examples_spec.rb +82 -0
  50. data/spec/packaging_spec.rb +13 -4
  51. data/test_support/dynamic_documentation_api.rb +171 -0
  52. data/test_support/dynamic_protocol_spec.rb +133 -0
  53. data/test_support/dynamic_python_login_spec.rb +75 -0
  54. metadata +34 -4
  55. data/lib/haveapi/client_examples/fs_client.rb +0 -119
@@ -1,164 +1,208 @@
1
- require 'haveapi/client_example'
1
+ require 'haveapi/client_examples/dynamic_helpers'
2
2
 
3
3
  module HaveAPI::ClientExamples
4
4
  class JsClient < HaveAPI::ClientExample
5
- label 'JavaScript'
5
+ include DynamicHelpers
6
+
7
+ label 'JavaScript (callback)'
8
+ id 'javascript-callback'
6
9
  code :javascript
7
10
  order 10
8
11
 
9
- def init
10
- <<~END
11
- import HaveAPI from 'haveapi-client'
12
-
13
- var api = new HaveAPI.Client("#{base_url}", {version: "#{version}"});
14
- END
12
+ def asynchronous?
13
+ false
15
14
  end
16
15
 
17
- def auth(method, desc)
18
- case method
19
- when :basic
20
- <<~END
21
- #{init}
22
-
23
- api.authenticate("basic", {
24
- user: "user",
25
- password: "secret"
26
- }, function (client, status) {
27
- console.log("Authenticated?", status);
28
- });
29
- END
16
+ def typescript?
17
+ false
18
+ end
30
19
 
31
- when :token
32
- <<~END
33
- #{init}
20
+ def js_literal(value)
21
+ value = json_data(value)
22
+ case value
23
+ when Hash
24
+ "{#{value.map { |key, entry| "[#{JSON.generate(key)}]: #{js_literal(entry)}" }.join(', ')}}"
25
+ when Array
26
+ "[#{value.map { |entry| js_literal(entry) }.join(', ')}]"
27
+ when Integer
28
+ raise Unsupported, 'This sample contains an integer outside JavaScript exact precision.' if value.abs > 9_007_199_254_740_991
29
+
30
+ value.to_s
31
+ else
32
+ JSON.generate(value).gsub("\u2028", '\\u2028').gsub("\u2029", '\\u2029')
33
+ end
34
+ end
34
35
 
35
- // Request a new token
36
- api.authenticate("token", {
37
- #{auth_token_credentials(desc).map { |k, v| "#{k}: \"#{v}\"" }.join(",\n ")}
38
- }, function (client, status) {
39
- console.log("Authenticated?", status);
36
+ def client_name
37
+ asynchronous? ? 'AsyncClient' : 'CallbackClient'
38
+ end
40
39
 
41
- if (status)
42
- console.log("Token is", client.authProvider.token);
43
- });
40
+ def env_helper
41
+ type = typescript? ? ': string' : ''
42
+ <<~JS
43
+ function credential(name#{type})#{type} {
44
+ const value = env[name];
45
+ if (value === undefined) throw new Error("Set " + name + " before running this example");
46
+ return value;
47
+ }
48
+ JS
49
+ end
44
50
 
45
- // Use an existing token
46
- api.authenticate("token", {
47
- token: "qwertyuiop..."
48
- }, function (client, status) {
49
- console.log("Authenticated?", status);
51
+ def program(body, authentication: nil)
52
+ imports = "import { #{client_name}#{', AsyncError' if asynchronous?} } from \"haveapi-client\";\n"
53
+ imports << "import { env } from \"node:process\";\n\n#{env_helper}\n" if authentication
54
+ declaration = "const api = new #{client_name}(#{js_literal(api_base)}, {version: #{js_literal(api_version)}});\n"
55
+ if asynchronous?
56
+ <<~JS
57
+ #{imports}
58
+ async function main()#{': Promise<void>' if typescript?} {
59
+ #{indent(declaration)} try {
60
+ #{indent(authentication || "await api.setup();\n", 4)}#{indent(body, 4)} } catch (error) {
61
+ if (error instanceof AsyncError) {
62
+ console.error(error.kind, error.message, error.errors);
63
+ } else {
64
+ throw error;
65
+ }
66
+ }
67
+ }
68
+ main().catch(error => console.error(error));
69
+ JS
70
+ else
71
+ <<~JS
72
+ #{imports}
73
+ #{declaration}
74
+ #{authentication || 'api.setup('}(client, ready) => {
75
+ if (!ready) {
76
+ console.error("Discovery or authentication failed");
77
+ return;
78
+ }
79
+ try {
80
+ #{indent(body, 4)} } catch (error) {
81
+ console.error(error);
82
+ }
50
83
  });
51
- END
52
-
53
- when :oauth2
54
- <<~END
55
- #{init}
56
- // The JavaScript client must be configured with OAuth2 access token, it does not
57
- // support the authorization procedure to obtain a new access token.
58
- var accessToken = {
59
- access_token: "the access token"
60
- };
61
-
62
- // The client is authenticated immediately, no need for a callback
63
- api.authenticate("oauth2", {access_token: accessToken});
64
- END
84
+ JS
65
85
  end
66
86
  end
67
87
 
68
- def example(sample)
69
- args = []
70
-
71
- args.concat(sample[:path_params]) if sample[:path_params]
88
+ def init
89
+ code_result { program("console.log(\"Discovery complete\");\n") }
90
+ end
72
91
 
73
- if sample[:request] && !sample[:request].empty?
74
- args << JSON.pretty_generate(sample[:request])
92
+ def action_authentication
93
+ return nil unless field(current_action, :auth)
94
+
95
+ method = %i[token basic oauth2].find { |name| supported_auth?(name) }
96
+ raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
97
+
98
+ options = {
99
+ token: '{token: credential("HAVEAPI_TOKEN")}',
100
+ basic: '{user: credential("HAVEAPI_USER"), password: credential("HAVEAPI_PASSWORD")}',
101
+ oauth2: '{access_token: {access_token: credential("HAVEAPI_ACCESS_TOKEN")}}'
102
+ }.fetch(method)
103
+ if asynchronous?
104
+ "await api.authenticate(#{js_literal(method.to_s)}, #{options});\n"
105
+ else
106
+ "api.authenticate(#{js_literal(method.to_s)}, #{options}, "
75
107
  end
108
+ end
76
109
 
77
- out = "#{init}\n"
78
- out << "api.#{resource_path.join('.')}.#{action_name}"
79
- out << '('
80
- out << "#{args.join(', ')}, " unless args.empty?
81
-
82
- callback = "function (client, reply) {\n"
83
- callback << " console.log('Response', reply);\n"
84
-
85
- callback << if sample[:status]
86
- response(sample)
87
-
88
- else
89
- error(sample)
110
+ def auth(method, description)
111
+ return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
112
+
113
+ code_result do
114
+ options = case method.to_sym
115
+ when :basic
116
+ '{user: credential("HAVEAPI_USER"), password: credential("HAVEAPI_PASSWORD")}'
117
+ when :token
118
+ if token_login_fields(description).empty?
119
+ '{token: credential("HAVEAPI_TOKEN")}'
120
+ else
121
+ "env.HAVEAPI_TOKEN !== undefined ? {token: credential(\"HAVEAPI_TOKEN\")} : #{token_login_options(description)}"
122
+ end
123
+ when :oauth2
124
+ '{access_token: {access_token: credential("HAVEAPI_ACCESS_TOKEN")}}'
90
125
  end
126
+ authentication = if asynchronous?
127
+ "await api.authenticate(#{js_literal(method.to_s)}, #{options});\n"
128
+ else
129
+ "api.authenticate(#{js_literal(method.to_s)}, #{options}, "
130
+ end
131
+ source = program("console.log(\"Authenticated\");\n", authentication:)
132
+ if method.to_sym == :oauth2
133
+ source = "// Use an existing access token; this client does not run an OAuth authorization flow.\n#{source}"
134
+ elsif method.to_sym == :token && !token_login_fields(description).empty?
135
+ source = "// Set HAVEAPI_TOKEN, or use login fields; HAVEAPI_AUTH_* and HAVEAPI_CHALLENGE_* contain JSON values.\n#{source}"
136
+ end
137
+ source
138
+ end
139
+ end
91
140
 
92
- callback << '}'
93
-
94
- out << callback.strip
95
- out << ');'
96
- out
141
+ def token_login_options(description)
142
+ fields = token_login_fields(description).map do |name, _|
143
+ "[#{js_literal(name.to_s)}]: JSON.parse(credential(#{js_literal("HAVEAPI_AUTH_#{name}")}))"
144
+ end
145
+ challenge = asynchronous? ? 'onChallenge' : 'callback'
146
+ annotation = typescript? ? ': Record<string, unknown>' : ''
147
+ fields << <<~JS.strip
148
+ #{challenge}: (action, parameters) => {
149
+ const input#{annotation} = Object.create(null);
150
+ for (const name of Object.keys(parameters)) {
151
+ if (name !== "token") input[name] = JSON.parse(credential("HAVEAPI_CHALLENGE_" + name));
152
+ }
153
+ return input;
154
+ }
155
+ JS
156
+ "{#{fields.join(",\n")}}"
97
157
  end
98
158
 
99
- def response(sample)
100
- out = ''
101
-
102
- case action[:output][:layout]
103
- when :hash
104
- out << " // reply is an instance of HaveAPI.Client.Response\n"
105
- out << " // reply.response() returns an object with output parameters:\n"
106
- out << JSON.pretty_generate(sample[:response] || {}).split("\n").map do |v|
107
- " // #{v}"
108
- end.join("\n")
109
- out << "\n"
110
-
111
- when :hash_list
112
- out << " // reply is an instance of HaveAPI.Client.Response\n"
113
- out << " // reply.response() returns an array of objects:\n"
114
- out << JSON.pretty_generate(sample[:response] || []).split("\n").map do |v|
115
- " // #{v}"
116
- end.join("\n")
117
- out << "\n"
118
-
119
- when :object
120
- out << " // reply is an instance of HaveAPI.Client.ResourceInstance\n"
121
-
122
- (sample[:response] || {}).each do |pn, pv|
123
- param = action[:output][:parameters][pn]
124
-
125
- if param[:type] == 'Resource'
126
- out << " // reply.#{pn} = HaveAPI.Client.ResourceInstance("
127
- out << "resource: #{param[:resource].join('.')}, "
128
-
129
- out << if pv.is_a?(::Hash)
130
- pv.map { |k, v| "#{k}: #{PP.pp(v, '').strip}" }.join(', ')
131
- else
132
- "id: #{pv}"
133
- end
134
-
135
- out << ")\n"
136
-
137
- elsif param[:type] == 'Custom' && (pv.is_a?(::Hash) || pv.is_a?(::Array))
138
- json = JSON.pretty_generate(pv).split("\n").map do |v|
139
- " // #{v}"
140
- end.join("\n")
141
-
142
- out << " // reply.#{pn} = #{json}"
143
-
144
- else
145
- out << " // reply.#{pn} = #{PP.pp(pv, '')}"
146
- end
159
+ def example(sample)
160
+ code_result do
161
+ path, request = structured_sample(sample)
162
+ if request.is_a?(Array)
163
+ raise Unsupported, 'This JavaScript client accepts object input only; list input cannot be represented without dropping values.'
164
+ end
165
+ if !request.nil? && !request.is_a?(Hash)
166
+ raise Unsupported, 'This JavaScript client requires object input.'
167
+ end
168
+ if !asynchronous? && field(field(current_action, :input), :parameters, {}).keys.any? { |name| %w[hasOwnProperty __proto__].include?(name.to_s) }
169
+ raise Unsupported, 'This callback client cannot safely represent this input parameter name.'
170
+ end
171
+ if request && %w[GET OPTIONS].include?(field(current_action, :method).to_s.upcase) && request.values.any? { |value| value.is_a?(Hash) || value.is_a?(Array) }
172
+ raise Unsupported, 'This JavaScript client cannot preserve nested values in query input.'
147
173
  end
148
174
 
149
- when :object_list
150
- out << " // reply is an instance of HaveAPI.Client.ResourceInstanceList\n"
175
+ args = path.values.map { |value| js_literal(value) }
176
+ if asynchronous?
177
+ resource = current_resource_path.each_with_index.map do |name, index|
178
+ "#{'api' if index == 0}.resource(#{js_literal(name)})"
179
+ end.join
180
+ options = request.nil? ? [] : ["{params: #{js_literal(request)}}"]
181
+ body = "const result = await #{resource}.action(#{js_literal(current_action_name)})(#{(args + options).join(', ')});\n"
182
+ body << "console.log(result.value);\nconsole.log(result.response.meta);\n"
183
+ if field(current_action, :blocking)
184
+ body << "// Request success alone does not establish operation completion.\n"
185
+ body << "const completed = await api.waitForCompletion(result, {timeoutMs: 300000});\nconsole.log(completed.state);\n"
186
+ end
187
+ else
188
+ body = "const resource = CallbackClient.findResource(client, #{js_literal(current_resource_path)});\n"
189
+ body << "const action = CallbackClient.findAction(resource, #{js_literal(current_action_name)});\n"
190
+ options = request.nil? ? '' : "params: #{js_literal(request)},\n"
191
+ options << <<~JS
192
+ onReply: (owner, reply) => {
193
+ if (!reply.isOk()) {
194
+ console.error(reply.message(), reply.envelope.errors);
195
+ return;
196
+ }
197
+ console.log(reply.response());
198
+ console.log(reply.meta());
199
+ }
200
+ JS
201
+ body << "action.directInvoke(#{(args + ["{\n#{indent(options)}}"]).join(', ')});\n"
202
+ body << "// This prints the initial response; use its action-state metadata to monitor completion.\n" if field(current_action, :blocking)
203
+ end
204
+ program(body, authentication: action_authentication)
151
205
  end
152
-
153
- out
154
- end
155
-
156
- def error(sample)
157
- out = ''
158
- out << " // reply.isOk() returns false\n"
159
- out << " // reply.message() returns the error message\n"
160
- out << " // reply.envelope.errors contains parameter errors\n"
161
- out
162
206
  end
163
207
  end
164
208
  end
@@ -0,0 +1,14 @@
1
+ require 'haveapi/client_examples/python_client'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class PythonAsyncClient < PythonClient
5
+ label 'Python (async)'
6
+ id 'python-async'
7
+ code :python
8
+ order 15
9
+
10
+ def asynchronous?
11
+ true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,172 @@
1
+ require 'haveapi/client_examples/dynamic_helpers'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class PythonClient < HaveAPI::ClientExample
5
+ include DynamicHelpers
6
+
7
+ label 'Python (sync)'
8
+ id 'python-sync'
9
+ code :python
10
+ order 14
11
+
12
+ def asynchronous?
13
+ false
14
+ end
15
+
16
+ def python_literal(value)
17
+ value = json_data(value)
18
+ case value
19
+ when Hash
20
+ "{#{value.map { |key, entry| "#{JSON.generate(key)}: #{python_literal(entry)}" }.join(', ')}}"
21
+ when Array
22
+ "[#{value.map { |entry| python_literal(entry) }.join(', ')}]"
23
+ when TrueClass then 'True'
24
+ when FalseClass then 'False'
25
+ when NilClass then 'None'
26
+ else JSON.generate(value)
27
+ end
28
+ end
29
+
30
+ def client_name
31
+ asynchronous? ? 'AsyncClient' : 'Client'
32
+ end
33
+
34
+ def await_prefix
35
+ asynchronous? ? 'await ' : ''
36
+ end
37
+
38
+ def program(body, imports: [], auth: nil)
39
+ package_imports = ([client_name, 'ActionError', 'HaveAPIError'] + imports).uniq.join(', ')
40
+ source = "import os\nimport json\n"
41
+ source << "import asyncio\n" if asynchronous?
42
+ source << "\nfrom haveapi_client import #{package_imports}\n\n\n"
43
+ source << "#{'async ' if asynchronous?}def main() -> None:\n try:\n"
44
+ options = ["version=#{python_literal(api_version)}"]
45
+ options << "auth=#{auth}" if auth
46
+ source << " #{'async ' if asynchronous?}with #{client_name}(#{python_literal(api_base)}, #{options.join(', ')}) as api:\n"
47
+ source << indent(body, 12)
48
+ source << indent(<<~PY, 4)
49
+ except ActionError as error:
50
+ print(error.message, error.errors)
51
+ except HaveAPIError as error:
52
+ print(error.message, error.status_code)
53
+ PY
54
+ source << "\n\nif __name__ == \"__main__\":\n #{asynchronous? ? 'asyncio.run(main())' : 'main()'}\n"
55
+ source
56
+ end
57
+
58
+ def init
59
+ code_result { program("print(\"Discovery complete\")\n") }
60
+ end
61
+
62
+ def auth(method, description)
63
+ return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
64
+
65
+ code_result do
66
+ case method.to_sym
67
+ when :basic
68
+ program("print(\"Authenticated\")\n", imports: ['BasicAuth'],
69
+ auth: 'BasicAuth(os.environ["HAVEAPI_USER"], os.environ["HAVEAPI_PASSWORD"])')
70
+ when :oauth2
71
+ source = program("print(\"Authenticated\")\n", imports: ['OAuth2Auth'],
72
+ auth: 'OAuth2Auth(os.environ["HAVEAPI_ACCESS_TOKEN"])')
73
+ "# Use an existing access token; OAuth acquisition and refresh are outside this client.\n#{source}"
74
+ when :token
75
+ fields = token_login_fields(description).merge(token_request_fields(description).select do |_, parameter|
76
+ field(parameter, :required)
77
+ end)
78
+ if fields.empty?
79
+ program("print(\"Authenticated\")\n", imports: ['TokenAuth'], auth: 'TokenAuth(os.environ["HAVEAPI_TOKEN"])')
80
+ else
81
+ challenge = asynchronous? ? 'AsyncAuthenticationChallenge' : 'AuthenticationChallenge'
82
+ input = fields.map do |name, _|
83
+ "#{python_literal(name.to_s)}: json.loads(os.environ[#{python_literal("HAVEAPI_AUTH_#{name}")}])"
84
+ end.join(', ')
85
+ body = <<~PY
86
+ if "HAVEAPI_TOKEN" in os.environ:
87
+ #{await_prefix}api.set_auth(TokenAuth(os.environ["HAVEAPI_TOKEN"]))
88
+ else:
89
+ # HAVEAPI_AUTH_* and HAVEAPI_CHALLENGE_* contain JSON values.
90
+ step = #{await_prefix}api.begin_token_login({#{input}})
91
+ while isinstance(step, #{challenge}):
92
+ values = {
93
+ name: json.loads(os.environ["HAVEAPI_CHALLENGE_" + name])
94
+ for name in step.input if name != "token"
95
+ }
96
+ step = #{await_prefix}step.submit(values)
97
+ print("Authenticated")
98
+ PY
99
+ program(body, imports: ['TokenAuth', challenge])
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def example(sample)
106
+ code_result do
107
+ path, request = structured_sample(sample)
108
+ method = field(current_action, :method).to_s.upcase
109
+ unless %w[GET POST PUT DELETE].include?(method)
110
+ raise Unsupported, 'This Python client supports GET, POST, PUT and DELETE actions only.'
111
+ end
112
+ unless request.nil? || request.is_a?(Hash) || request.is_a?(Array)
113
+ raise Unsupported, 'This Python client requires object or list input.'
114
+ end
115
+ if request.is_a?(Array) && field(field(current_action, :input), :namespace).to_s.empty?
116
+ raise Unsupported, 'This Python client requires a namespace for list input.'
117
+ end
118
+
119
+ check_get_sample(request) if method == 'GET'
120
+
121
+ resource = current_resource_path.each_with_index.map do |name, index|
122
+ "#{'api' if index == 0}.resources[#{python_literal(name)}]"
123
+ end.join
124
+ arguments = []
125
+ arguments << "path_params=#{python_literal(path)}" unless path.empty?
126
+ arguments << "params=#{python_literal(request)}" unless request.nil?
127
+ body = "response = #{await_prefix}#{resource}.actions[#{python_literal(current_action_name)}].call(#{arguments.join(', ')})\n"
128
+ body << "print(response.data)\nprint(response.meta)\n"
129
+ if field(current_action, :blocking)
130
+ body << <<~PY
131
+ # A request timeout does not cancel a server operation.
132
+ if response.operation is not None:
133
+ completed = #{await_prefix}response.operation.wait(timeout=300, interval=1)
134
+ print(completed)
135
+ PY
136
+ end
137
+ if field(current_action, :auth)
138
+ method = %i[token basic oauth2].find { |name| supported_auth?(name) }
139
+ raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
140
+
141
+ provider, auth = {
142
+ token: ['TokenAuth', 'TokenAuth(os.environ["HAVEAPI_TOKEN"])'],
143
+ basic: ['BasicAuth', 'BasicAuth(os.environ["HAVEAPI_USER"], os.environ["HAVEAPI_PASSWORD"])'],
144
+ oauth2: ['OAuth2Auth', 'OAuth2Auth(os.environ["HAVEAPI_ACCESS_TOKEN"])']
145
+ }.fetch(method)
146
+ program(body, imports: [provider], auth:)
147
+ else
148
+ program(body)
149
+ end
150
+ end
151
+ end
152
+
153
+ def check_get_sample(request)
154
+ input = field(current_action, :input)
155
+ namespace = field(input, :namespace)
156
+ parameters = field(input, :parameters, {}).to_h { |name, parameter| [name.to_s, parameter] }
157
+ safe_component = ->(value) { !value.to_s.empty? && !value.to_s.match?(/[\[\]]/) }
158
+ safe_namespace = !namespace || safe_component.call(namespace)
159
+ # Deliberately support a flat subset, leaving the full Rack encoder to the client.
160
+ safe_request = request.nil? || (request.is_a?(Hash) && request.all? do |name, value|
161
+ parameter = field(parameters, name)
162
+ custom = field(parameter, :type) == 'Custom'
163
+ scalar = !value.is_a?(Hash) && !value.is_a?(Array)
164
+ custom_value = value.is_a?(String) || (value.nil? && field(parameter, :nullable))
165
+ safe_component.call(name) && scalar && (!custom || custom_value)
166
+ end)
167
+ return if safe_namespace && safe_request
168
+
169
+ raise Unsupported, 'Python GET examples support flat scalar input with bracket-free names; Custom values must be strings or nullable nulls.'
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,59 @@
1
+ require 'haveapi/client_example'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class RustClient < HaveAPI::ClientExample
5
+ class << self
6
+ def available?(context)
7
+ context&.profile&.language == :rust
8
+ end
9
+ end
10
+
11
+ def init
12
+ render_rust(:init)
13
+ end
14
+
15
+ def auth(method, desc)
16
+ render_rust(:auth, auth_method: method, auth_description: desc)
17
+ end
18
+
19
+ def example(sample)
20
+ render_rust(:example, rendering_context: context&.with(sample:))
21
+ end
22
+
23
+ private
24
+
25
+ def render_rust(kind, rendering_context: context, **)
26
+ unless self.class.available?(rendering_context)
27
+ return { status: :limitation, reason: 'A Rust SDK documentation profile is required.' }.freeze
28
+ end
29
+
30
+ rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: rust_mode, **)
31
+ end
32
+ end
33
+
34
+ class RustAsyncClient < RustClient
35
+ id 'rust-async'
36
+ label 'Rust (async)'
37
+ code :rust
38
+ order 17
39
+
40
+ private
41
+
42
+ def rust_mode
43
+ :async
44
+ end
45
+ end
46
+
47
+ class RustBlockingClient < RustClient
48
+ id 'rust-blocking'
49
+ label 'Rust (blocking)'
50
+ code :rust
51
+ order 18
52
+
53
+ private
54
+
55
+ def rust_mode
56
+ :blocking
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ require 'haveapi/client_examples/ts_client'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class TsAsyncClient < TsClient
5
+ label 'TypeScript (async)'
6
+ id 'typescript-async'
7
+ code :typescript
8
+ order 13
9
+
10
+ def asynchronous?
11
+ true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'haveapi/client_examples/js_client'
2
+
3
+ module HaveAPI::ClientExamples
4
+ class TsClient < JsClient
5
+ label 'TypeScript (callback)'
6
+ id 'typescript-callback'
7
+ code :typescript
8
+ order 12
9
+
10
+ def typescript?
11
+ true
12
+ end
13
+ end
14
+ end