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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Rakefile +1 -0
- data/doc/client-examples.md +96 -0
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/client_example.rb +47 -1
- data/lib/haveapi/client_examples/dynamic_helpers.rb +129 -0
- data/lib/haveapi/client_examples/go_client.rb +37 -0
- data/lib/haveapi/client_examples/js_async_client.rb +14 -0
- data/lib/haveapi/client_examples/js_client.rb +179 -135
- data/lib/haveapi/client_examples/python_async_client.rb +14 -0
- data/lib/haveapi/client_examples/python_client.rb +172 -0
- data/lib/haveapi/client_examples/rust_client.rb +59 -0
- data/lib/haveapi/client_examples/ts_async_client.rb +14 -0
- data/lib/haveapi/client_examples/ts_client.rb +14 -0
- data/lib/haveapi/documentation_clients.rb +126 -0
- data/lib/haveapi/documentation_context.rb +47 -0
- data/lib/haveapi/public/js/highlight-LICENSE +29 -0
- data/lib/haveapi/public/js/highlight.pack.js +2 -2
- data/lib/haveapi/public/js/highlighter.js +16 -6
- data/lib/haveapi/public/js/main.js +9 -1
- data/lib/haveapi/public/js/nojs-tabs.js +37 -3
- data/lib/haveapi/server.rb +34 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/main_layout.erb +11 -0
- data/lib/haveapi/views/version_page/auth_body.erb +3 -3
- data/lib/haveapi/views/version_page/client_auth.erb +2 -2
- data/lib/haveapi/views/version_page/client_example.erb +2 -11
- data/lib/haveapi/views/version_page/client_init.erb +2 -2
- data/lib/haveapi/views/version_page/client_result.erb +5 -0
- data/lib/haveapi/views/version_page/resource_body.erb +3 -7
- data/lib/haveapi/views/version_page.erb +13 -9
- data/spec/client_examples/assets/highlighter_test.cjs +26 -0
- data/spec/client_examples/assets/tabs_test.cjs +54 -0
- data/spec/client_examples/context_spec.rb +68 -0
- data/spec/client_examples/dynamic_acceptance.md +83 -0
- data/spec/client_examples/dynamic_acceptance.rb +151 -0
- data/spec/client_examples/dynamic_acceptance_spec.rb +76 -0
- data/spec/client_examples/dynamic_fixture.rb +36 -0
- data/spec/client_examples/dynamic_js_spec.rb +195 -0
- data/spec/client_examples/dynamic_metadata_spec.rb +52 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +65 -0
- data/spec/client_examples/dynamic_python_spec.rb +123 -0
- data/spec/client_examples/go_spec.rb +57 -0
- data/spec/client_examples/profiles_spec.rb +95 -0
- data/spec/client_examples/rust_client_spec.rb +75 -0
- data/spec/client_examples/syntax_spec.rb +2 -1
- data/spec/documentation/auth_filtering_spec.rb +12 -0
- data/spec/documentation/client_examples_spec.rb +82 -0
- data/spec/packaging_spec.rb +13 -4
- data/test_support/dynamic_documentation_api.rb +171 -0
- data/test_support/dynamic_protocol_spec.rb +133 -0
- data/test_support/dynamic_python_login_spec.rb +75 -0
- metadata +34 -4
- data/lib/haveapi/client_examples/fs_client.rb +0 -119
|
@@ -1,164 +1,208 @@
|
|
|
1
|
-
require 'haveapi/
|
|
1
|
+
require 'haveapi/client_examples/dynamic_helpers'
|
|
2
2
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class JsClient < HaveAPI::ClientExample
|
|
5
|
-
|
|
5
|
+
include DynamicHelpers
|
|
6
|
+
|
|
7
|
+
label 'JavaScript (callback)'
|
|
8
|
+
id 'javascript-callback'
|
|
6
9
|
code :javascript
|
|
7
10
|
order 10
|
|
8
11
|
|
|
9
|
-
def
|
|
10
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}, function (client, status) {
|
|
39
|
-
console.log("Authenticated?", status);
|
|
36
|
+
def client_name
|
|
37
|
+
asynchronous? ? 'AsyncClient' : 'CallbackClient'
|
|
38
|
+
end
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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,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
|