haveapi 0.31.1 → 0.32.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 +2 -0
- data/benchmarks/documentation.rb +29 -6
- data/benchmarks/endpoints.rb +153 -24
- data/benchmarks/spec/client_examples_spec.rb +109 -37
- data/doc/client-examples.md +218 -45
- data/doc/documentation-oauth.md +157 -0
- data/doc/documentation-ui.md +192 -0
- data/doc/index.md +3 -6
- data/doc/performance.md +39 -12
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/authentication/chain.rb +19 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
- data/lib/haveapi/client_example.rb +143 -15
- data/lib/haveapi/client_examples/curl.rb +30 -20
- data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
- data/lib/haveapi/client_examples/go_client.rb +9 -2
- data/lib/haveapi/client_examples/http.rb +9 -4
- data/lib/haveapi/client_examples/js_async_client.rb +7 -0
- data/lib/haveapi/client_examples/js_client.rb +47 -36
- data/lib/haveapi/client_examples/php_client.rb +161 -75
- data/lib/haveapi/client_examples/python_async_client.rb +3 -0
- data/lib/haveapi/client_examples/python_client.rb +61 -47
- data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
- data/lib/haveapi/client_examples/ruby_client.rb +80 -31
- data/lib/haveapi/client_examples/rust_client.rb +20 -9
- data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
- data/lib/haveapi/client_examples/ts_client.rb +3 -0
- data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
- data/lib/haveapi/documentation_context.rb +7 -2
- data/lib/haveapi/documentation_helpers.rb +223 -0
- data/lib/haveapi/documentation_oauth.rb +399 -0
- data/lib/haveapi/documentation_search.rb +225 -0
- data/lib/haveapi/documentation_search_index.rb +79 -0
- data/lib/haveapi/documentation_validators.rb +179 -0
- data/lib/haveapi/public/css/documentation.css +343 -0
- data/lib/haveapi/public/js/documentation.js +992 -0
- data/lib/haveapi/server.rb +144 -86
- data/lib/haveapi/shared_documentation_helpers.rb +54 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +16 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
- data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
- data/lib/haveapi/views/documentation/search_form.erb +10 -0
- data/lib/haveapi/views/documentation/search_index.erb +1 -0
- data/lib/haveapi/views/documentation/search_results.erb +20 -0
- data/lib/haveapi/views/index.erb +2 -4
- data/lib/haveapi/views/main_layout.erb +12 -6
- data/lib/haveapi/views/shared_documentation.erb +12 -0
- data/lib/haveapi/views/shared_sidebar.erb +25 -0
- data/lib/haveapi/views/version_page/auth_body.erb +12 -33
- data/lib/haveapi/views/version_page/client_overview.erb +40 -0
- data/lib/haveapi/views/version_page/example_panels.erb +18 -0
- data/lib/haveapi/views/version_page/resource_body.erb +91 -132
- data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
- data/lib/haveapi/views/version_page/usage.erb +7 -0
- data/lib/haveapi/views/version_page.erb +36 -117
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
- data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
- data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
- data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
- data/lib/haveapi/views/version_sidebar.erb +59 -48
- data/spec/authentication/basic_spec.rb +10 -10
- data/spec/authentication/oauth2_chain_spec.rb +72 -0
- data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
- data/spec/client_examples/assets/documentation_test.cjs +817 -0
- data/spec/client_examples/context_performance_spec.rb +5 -4
- data/spec/client_examples/context_spec.rb +23 -2
- data/spec/client_examples/detail_spec.rb +175 -0
- data/spec/client_examples/dynamic_acceptance.md +59 -26
- data/spec/client_examples/dynamic_acceptance.rb +164 -44
- data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
- data/spec/client_examples/dynamic_fixture.rb +4 -3
- data/spec/client_examples/dynamic_js_spec.rb +20 -11
- data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
- data/spec/client_examples/dynamic_python_spec.rb +62 -11
- data/spec/client_examples/go_spec.rb +14 -1
- data/spec/client_examples/presentation_spec.rb +247 -0
- data/spec/client_examples/profile_mount_spec.rb +40 -13
- data/spec/client_examples/rust_client_spec.rb +35 -1
- data/spec/client_examples/shell_spec.rb +268 -0
- data/spec/client_examples/syntax_spec.rb +192 -0
- data/spec/client_examples/ts_generated_client_spec.rb +15 -2
- data/spec/client_examples/typescript_profiles_spec.rb +1 -1
- data/spec/documentation/auth_filtering_spec.rb +9 -4
- data/spec/documentation/canonical_routes_spec.rb +89 -0
- data/spec/documentation/client_examples_spec.rb +39 -5
- data/spec/documentation/client_overview_spec.rb +145 -0
- data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
- data/spec/documentation/example_variants_spec.rb +93 -0
- data/spec/documentation/fixtures/search_index.json +476 -0
- data/spec/documentation/identity_and_guides_spec.rb +130 -0
- data/spec/documentation/identity_context_spec.rb +180 -0
- data/spec/documentation/navigation_spec.rb +296 -0
- data/spec/documentation/oauth_fixture_spec.rb +39 -0
- data/spec/documentation/oauth_spec.rb +912 -0
- data/spec/documentation/oauth_transport_spec.rb +45 -0
- data/spec/documentation/performance_spec.rb +47 -6
- data/spec/documentation/reference_headings_spec.rb +190 -0
- data/spec/documentation/search_index_routes_spec.rb +202 -0
- data/spec/documentation/search_index_spec.rb +85 -0
- data/spec/documentation/search_routes_spec.rb +199 -0
- data/spec/documentation/search_spec.rb +134 -0
- data/spec/documentation/shared_pages_spec.rb +240 -0
- data/spec/documentation/validators_spec.rb +123 -0
- data/spec/documentation_spec.rb +3 -3
- data/spec/packaging_spec.rb +21 -2
- data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
- data/spec/performance/documentation_shapes_spec.rb +97 -0
- data/spec/performance/json_activation_spec.rb +78 -1
- data/spec/performance/release_lineage_spec.rb +4 -3
- data/spec/performance/runner_spec.rb +1 -0
- data/test_support/all_client_performance_fixture.rb +6 -10
- data/test_support/documentation_oauth_fixture.rb +54 -0
- data/test_support/documentation_performance_fixture.rb +19 -1
- data/test_support/dynamic_documentation_api.rb +35 -10
- data/test_support/dynamic_python_login_spec.rb +3 -11
- metadata +52 -3
|
@@ -5,10 +5,17 @@ module HaveAPI::ClientExamples
|
|
|
5
5
|
include DynamicHelpers
|
|
6
6
|
|
|
7
7
|
label 'Python (sync)'
|
|
8
|
+
language 'python'
|
|
9
|
+
language_label 'Python'
|
|
10
|
+
variant 'Sync'
|
|
8
11
|
id 'python-sync'
|
|
9
12
|
code :python
|
|
10
13
|
order 14
|
|
11
14
|
|
|
15
|
+
def self.detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
16
|
+
result[:status] == :limitation ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def asynchronous?
|
|
13
20
|
false
|
|
14
21
|
end
|
|
@@ -35,68 +42,66 @@ module HaveAPI::ClientExamples
|
|
|
35
42
|
asynchronous? ? 'await ' : ''
|
|
36
43
|
end
|
|
37
44
|
|
|
38
|
-
def program(body, imports: [], auth: nil)
|
|
39
|
-
package_imports = ([client_name
|
|
40
|
-
source = "import
|
|
41
|
-
source << "
|
|
42
|
-
source << "
|
|
43
|
-
source << "
|
|
45
|
+
def program(body, imports: [], stdlib: [], auth: nil)
|
|
46
|
+
package_imports = ([client_name] + (thorough? ? %w[ActionError HaveAPIError] : []) + imports).uniq.join(', ')
|
|
47
|
+
source = (stdlib + (asynchronous? ? ['asyncio'] : [])).uniq.map { |name| "import #{name}\n" }.join
|
|
48
|
+
source << "\n" unless source.empty?
|
|
49
|
+
source << "from haveapi_client import #{package_imports}#{asynchronous? ? "\n\n\n" : "\n\n"}"
|
|
50
|
+
source << "async def main() -> None:\n" if asynchronous?
|
|
51
|
+
level = asynchronous? ? 4 : 0
|
|
52
|
+
if thorough?
|
|
53
|
+
source << indent("try:\n", level)
|
|
54
|
+
level += 4
|
|
55
|
+
end
|
|
44
56
|
options = ["version=#{python_literal(api_version)}"]
|
|
45
57
|
options << "auth=#{auth}" if auth
|
|
46
|
-
source << "
|
|
47
|
-
source << indent(body,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
source << indent("#{'async ' if asynchronous?}with #{client_name}(#{python_literal(api_base)}, #{options.join(', ')}) as api:\n", level)
|
|
59
|
+
source << indent(body, level + 4)
|
|
60
|
+
if thorough?
|
|
61
|
+
source << indent(<<~PY, level - 4)
|
|
62
|
+
except ActionError as error:
|
|
63
|
+
print(error.message, error.errors)
|
|
64
|
+
except HaveAPIError as error:
|
|
65
|
+
print(error.message, error.status_code)
|
|
66
|
+
PY
|
|
67
|
+
end
|
|
68
|
+
source << "\n\nasyncio.run(main())\n" if asynchronous?
|
|
55
69
|
source
|
|
56
70
|
end
|
|
57
71
|
|
|
58
72
|
def init
|
|
59
|
-
code_result { program("print(\"Discovery complete\")\n") }
|
|
73
|
+
code_result { program(thorough? ? "print(\"Discovery complete\")\n" : "# Call API actions inside this block.\npass\n") }
|
|
60
74
|
end
|
|
61
75
|
|
|
62
76
|
def auth(method, description)
|
|
63
77
|
return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
|
|
64
78
|
|
|
65
79
|
code_result do
|
|
80
|
+
body = thorough? ? "print(\"Authenticated\")\n" : "# Call API actions inside this block.\npass\n"
|
|
66
81
|
case method.to_sym
|
|
67
82
|
when :basic
|
|
68
|
-
program(
|
|
69
|
-
auth: 'BasicAuth(os.environ["HAVEAPI_USER"], os.environ["HAVEAPI_PASSWORD"])')
|
|
83
|
+
program(body, imports: ['BasicAuth'], auth: 'BasicAuth("user", "secret")')
|
|
70
84
|
when :oauth2
|
|
71
|
-
source = program(
|
|
72
|
-
auth: 'OAuth2Auth(os.environ["HAVEAPI_ACCESS_TOKEN"])')
|
|
85
|
+
source = program(body, imports: ['OAuth2Auth'], auth: 'OAuth2Auth("example-access-token")')
|
|
73
86
|
"# Use an existing access token; OAuth acquisition and refresh are outside this client.\n#{source}"
|
|
74
87
|
when :token
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end)
|
|
78
|
-
if fields.empty?
|
|
79
|
-
program("print(\"Authenticated\")\n", imports: ['TokenAuth'], auth: 'TokenAuth(os.environ["HAVEAPI_TOKEN"])')
|
|
88
|
+
if token_login_fields(description).empty?
|
|
89
|
+
program(body, imports: ['TokenAuth'], auth: 'TokenAuth("example-token")')
|
|
80
90
|
else
|
|
81
91
|
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
92
|
body = <<~PY
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
step = #{await_prefix}step.submit(values)
|
|
97
|
-
print("Authenticated")
|
|
93
|
+
continuations = #{python_literal(auth_token_continuations(description))}
|
|
94
|
+
step = #{await_prefix}api.begin_token_login(#{python_literal(auth_token_credentials(description))})
|
|
95
|
+
while isinstance(step, #{challenge}):
|
|
96
|
+
if step.next_action not in continuations:
|
|
97
|
+
raise ValueError("Unknown authentication continuation")
|
|
98
|
+
values = continuations[step.next_action]
|
|
99
|
+
if any(name != "token" and name not in values for name in step.input):
|
|
100
|
+
raise ValueError("Unknown authentication input")
|
|
101
|
+
step = #{await_prefix}step.submit(values)
|
|
98
102
|
PY
|
|
99
|
-
|
|
103
|
+
body << "print(\"Authenticated\")\n" if thorough?
|
|
104
|
+
program(body, imports: [challenge])
|
|
100
105
|
end
|
|
101
106
|
end
|
|
102
107
|
end
|
|
@@ -125,7 +130,8 @@ module HaveAPI::ClientExamples
|
|
|
125
130
|
arguments << "path_params=#{python_literal(path)}" unless path.empty?
|
|
126
131
|
arguments << "params=#{python_literal(request)}" unless request.nil?
|
|
127
132
|
body = "response = #{await_prefix}#{resource}.actions[#{python_literal(current_action_name)}].call(#{arguments.join(', ')})\n"
|
|
128
|
-
body << "print(response.data)\
|
|
133
|
+
body << "print(response.data)\n"
|
|
134
|
+
body << "print(response.meta)\n" if thorough?
|
|
129
135
|
if field(current_action, :blocking)
|
|
130
136
|
body << <<~PY
|
|
131
137
|
# A request timeout does not cancel a server operation.
|
|
@@ -139,17 +145,25 @@ module HaveAPI::ClientExamples
|
|
|
139
145
|
raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
|
|
140
146
|
|
|
141
147
|
provider, auth = {
|
|
142
|
-
token: ['TokenAuth', 'TokenAuth(
|
|
143
|
-
basic: ['BasicAuth', 'BasicAuth(
|
|
144
|
-
oauth2: ['OAuth2Auth', 'OAuth2Auth(
|
|
148
|
+
token: ['TokenAuth', 'TokenAuth("example-token")'],
|
|
149
|
+
basic: ['BasicAuth', 'BasicAuth("user", "secret")'],
|
|
150
|
+
oauth2: ['OAuth2Auth', 'OAuth2Auth("example-access-token")']
|
|
145
151
|
}.fetch(method)
|
|
146
|
-
|
|
152
|
+
if thorough?
|
|
153
|
+
program(body, imports: [provider], auth:)
|
|
154
|
+
else
|
|
155
|
+
action_fragment(body)
|
|
156
|
+
end
|
|
147
157
|
else
|
|
148
|
-
program(body)
|
|
158
|
+
thorough? ? program(body) : action_fragment(body)
|
|
149
159
|
end
|
|
150
160
|
end
|
|
151
161
|
end
|
|
152
162
|
|
|
163
|
+
def action_fragment(body)
|
|
164
|
+
asynchronous? ? "# Run inside an async function after initializing api.\n#{body}" : body
|
|
165
|
+
end
|
|
166
|
+
|
|
153
167
|
def check_get_sample(request)
|
|
154
168
|
input = field(current_action, :input)
|
|
155
169
|
namespace = field(input, :namespace)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'shellwords'
|
|
1
2
|
module HaveAPI
|
|
2
3
|
module CLI; end
|
|
3
4
|
end
|
|
@@ -8,11 +9,14 @@ require 'haveapi/client_example'
|
|
|
8
9
|
module HaveAPI::ClientExamples
|
|
9
10
|
class RubyCli < HaveAPI::ClientExample
|
|
10
11
|
label 'CLI'
|
|
12
|
+
language 'cli'
|
|
13
|
+
language_label 'CLI'
|
|
14
|
+
detail_levels :simple
|
|
11
15
|
code :bash
|
|
12
16
|
order 30
|
|
13
17
|
|
|
14
18
|
def init
|
|
15
|
-
"$ haveapi-cli -u #{base_url} --version #{version}"
|
|
19
|
+
"$ haveapi-cli -u #{Shellwords.escape(base_url.to_s)} --version #{Shellwords.escape(version.to_s)}"
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def auth(method, desc)
|
|
@@ -24,7 +28,7 @@ module HaveAPI::ClientExamples
|
|
|
24
28
|
|
|
25
29
|
# If username or password isn't provided, the user is asked on stdin
|
|
26
30
|
#{init} --auth basic --user user
|
|
27
|
-
Password: secret
|
|
31
|
+
# Password: secret
|
|
28
32
|
END
|
|
29
33
|
|
|
30
34
|
when :token
|
|
@@ -33,8 +37,8 @@ module HaveAPI::ClientExamples
|
|
|
33
37
|
# Note that the client always has to call some action. APIs should provide
|
|
34
38
|
# an action to get information about the current user, so that's what we're
|
|
35
39
|
# calling now.
|
|
36
|
-
#{init} --auth token #{auth_token_credentials(desc, password: false).map { |k, v| "--#{k} #{v}" }.join(' ')} --save user current
|
|
37
|
-
Password: secret
|
|
40
|
+
#{init} --auth token #{auth_token_credentials(desc, password: false).map { |k, v| "#{Shellwords.escape("--#{k}")} #{Shellwords.escape(v.to_s)}" }.join(' ')} --save user current
|
|
41
|
+
# Password: secret
|
|
38
42
|
|
|
39
43
|
# Now the token is read from disk and the user does not have to provide username
|
|
40
44
|
# nor password and be authenticated
|
|
@@ -44,13 +48,15 @@ module HaveAPI::ClientExamples
|
|
|
44
48
|
when :oauth2
|
|
45
49
|
'# OAuth2 is not supported by HaveAPI Ruby CLI.'
|
|
46
50
|
end
|
|
51
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
52
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def example(sample)
|
|
50
56
|
cmd = [init]
|
|
51
|
-
cmd << resource_path.join('.')
|
|
52
|
-
cmd << action_name
|
|
53
|
-
cmd.concat(sample[:path_params]) if sample[:path_params]
|
|
57
|
+
cmd << Shellwords.escape(resource_path.join('.'))
|
|
58
|
+
cmd << Shellwords.escape(action_name.to_s)
|
|
59
|
+
cmd.concat(sample[:path_params].map { |value| Shellwords.escape(value.to_s) }) if sample[:path_params]
|
|
54
60
|
|
|
55
61
|
if sample[:request] && !sample[:request].empty?
|
|
56
62
|
cmd << "-- \\\n"
|
|
@@ -65,16 +71,16 @@ module HaveAPI::ClientExamples
|
|
|
65
71
|
|
|
66
72
|
return response(sample, res) if sample[:status]
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
display = "Action failed: #{sample[:message]}\n"
|
|
69
75
|
|
|
70
76
|
if sample[:errors] && sample[:errors].any?
|
|
71
|
-
|
|
77
|
+
display << "Errors:\n"
|
|
72
78
|
sample[:errors].each do |param, e|
|
|
73
|
-
|
|
79
|
+
display << "\t#{param}: #{e.join('; ')}\n"
|
|
74
80
|
end
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
res
|
|
83
|
+
res << "\n" << comment_response(display)
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
def response(sample, res)
|
|
@@ -101,21 +107,24 @@ module HaveAPI::ClientExamples
|
|
|
101
107
|
cols << col
|
|
102
108
|
end
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
cols
|
|
107
|
-
)
|
|
108
|
-
res
|
|
110
|
+
display = HaveAPI::CLI::OutputFormatter.to_s(sample[:response], cols)
|
|
111
|
+
res << "\n" << comment_response(display)
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
def input_param(name, value)
|
|
112
115
|
option = name.to_s.gsub('_', '-')
|
|
113
116
|
|
|
114
117
|
if action[:input][:parameters][name][:type] == 'Boolean'
|
|
115
|
-
return value ? "--#{option}" : "--no-#{name}"
|
|
118
|
+
return Shellwords.escape(value ? "--#{option}" : "--no-#{name}")
|
|
116
119
|
end
|
|
117
120
|
|
|
118
|
-
"--#{option}
|
|
121
|
+
"#{Shellwords.escape("--#{option}")} #{Shellwords.escape(value.to_s)}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def comment_response(fragment)
|
|
127
|
+
fragment.gsub(/\r\n?/, "\n").lines.map { |line| "# #{line.delete_suffix("\n")}\n" }.join
|
|
119
128
|
end
|
|
120
129
|
end
|
|
121
130
|
end
|
|
@@ -4,45 +4,91 @@ require 'haveapi/client_example'
|
|
|
4
4
|
module HaveAPI::ClientExamples
|
|
5
5
|
class RubyClient < HaveAPI::ClientExample
|
|
6
6
|
label 'Ruby'
|
|
7
|
+
language 'ruby'
|
|
8
|
+
language_label 'Ruby'
|
|
7
9
|
code :ruby
|
|
8
10
|
order 0
|
|
9
11
|
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
13
|
+
result[:status] == :limitation || (kind == :auth && auth_method == :oauth2) ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
|
|
14
|
+
end
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
def initialization
|
|
17
|
+
"require 'haveapi/client'\n\napi = HaveAPI::Client.new(#{base_url.dump}, version: #{version.to_s.dump})\n"
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
def with_error_handling(body)
|
|
21
|
+
<<~RUBY
|
|
22
|
+
begin
|
|
23
|
+
#{body.lines.map { |line| line == "\n" ? line : " #{line}" }.join.chomp}
|
|
24
|
+
rescue HaveAPI::Client::ValidationError => error
|
|
25
|
+
warn error.message
|
|
26
|
+
warn error.errors.inspect
|
|
27
|
+
rescue HaveAPI::Client::ActionFailed => error
|
|
28
|
+
warn error.message
|
|
29
|
+
warn error.response.errors.inspect if error.response.is_a?(HaveAPI::Client::Response)
|
|
30
|
+
rescue HaveAPI::Client::Authentication::AuthenticationFailed, HaveAPI::Client::ProtocolError,
|
|
31
|
+
RestClient::Exception, JSON::ParserError => error
|
|
32
|
+
warn error.message
|
|
33
|
+
end
|
|
34
|
+
RUBY
|
|
35
|
+
end
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
def init
|
|
38
|
+
thorough? ? with_error_handling("#{initialization}api.setup\n") : initialization
|
|
39
|
+
end
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#{init}
|
|
41
|
+
def auth_login_literal(name, parameter, value)
|
|
42
|
+
return value unless value == false || value.nil?
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
# Token#request_token copies only truthy login options.
|
|
45
|
+
auth_literal(name, parameter, excluding: [false, nil])
|
|
46
|
+
end
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
def auth(method, desc)
|
|
49
|
+
body = case method
|
|
50
|
+
when :basic
|
|
51
|
+
"api.authenticate(:basic, user: 'user', password: 'secret')"
|
|
52
|
+
when :token
|
|
53
|
+
input = auth_token_credentials(desc).transform_keys(&:to_sym)
|
|
54
|
+
continuations = auth_token_continuations(desc)
|
|
55
|
+
code = "api.authenticate(:token, **#{input.inspect})"
|
|
56
|
+
unless continuations.empty?
|
|
57
|
+
code << <<~RUBY
|
|
58
|
+
do |action, parameters|
|
|
59
|
+
continuations = #{continuations.inspect}
|
|
60
|
+
input = continuations.fetch(action.to_s) { raise ArgumentError, 'Unknown authentication continuation' }
|
|
61
|
+
unless (parameters.keys.map(&:to_s) - input.keys - ['token']).empty?
|
|
62
|
+
raise ArgumentError, 'Unknown authentication input'
|
|
63
|
+
end
|
|
64
|
+
input.transform_keys(&:to_sym)
|
|
65
|
+
end
|
|
66
|
+
RUBY
|
|
67
|
+
end
|
|
68
|
+
%(#{code.chomp}\n\n# Reuse an existing token\napi.authenticate(:token, token: 'example-token'))
|
|
69
|
+
when :oauth2
|
|
70
|
+
return '# OAuth2 is not supported by HaveAPI Ruby client.'
|
|
71
|
+
end
|
|
72
|
+
return unless body
|
|
73
|
+
|
|
74
|
+
source = [initialization.chomp, (thorough? ? 'api.setup' : nil), body].compact.join("\n\n")
|
|
75
|
+
thorough? ? with_error_handling(source) : source
|
|
76
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
77
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
78
|
+
end
|
|
35
79
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
END
|
|
80
|
+
def action_authentication
|
|
81
|
+
return '' unless action[:auth] && context
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
providers = auth_field(context.description, :authentication, {}).keys.map(&:to_s)
|
|
84
|
+
return "api.authenticate(:token, token: 'example-token')\n" if providers.include?('token')
|
|
85
|
+
return "api.authenticate(:basic, user: 'user', password: 'secret')\n" if providers.include?('basic')
|
|
86
|
+
|
|
87
|
+
raise HaveAPI::ClientExample::UnsupportedAuthentication, 'This action requires authentication unsupported by this client.'
|
|
43
88
|
end
|
|
44
89
|
|
|
45
90
|
def example(sample)
|
|
91
|
+
credentials = action_authentication
|
|
46
92
|
args = []
|
|
47
93
|
|
|
48
94
|
args.concat(sample[:path_params]) if sample[:path_params]
|
|
@@ -51,15 +97,18 @@ module HaveAPI::ClientExamples
|
|
|
51
97
|
args << PP.pp(sample[:request], '').strip
|
|
52
98
|
end
|
|
53
99
|
|
|
54
|
-
out =
|
|
55
|
-
out << "reply =
|
|
100
|
+
out = +''
|
|
101
|
+
out << "reply = api.#{resource_path.join('.')}.#{action_name}"
|
|
56
102
|
out << "(#{args.join(', ')})" unless args.empty?
|
|
57
103
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
out << if sample[:status]
|
|
105
|
+
response(sample)
|
|
106
|
+
else
|
|
107
|
+
"\n# Raises exception HaveAPI::Client::ActionFailed\n"
|
|
108
|
+
end
|
|
109
|
+
thorough? ? with_error_handling(["#{initialization}api.setup", credentials.chomp, out].reject(&:empty?).join("\n\n")) : out
|
|
110
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
111
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
63
112
|
end
|
|
64
113
|
|
|
65
114
|
def response(sample)
|
|
@@ -6,6 +6,11 @@ module HaveAPI::ClientExamples
|
|
|
6
6
|
def available?(context)
|
|
7
7
|
context&.profile&.language == :rust
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
def detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
11
|
+
adapter_detail_levels_for(adapter: context.profile.adapter, kind:, context:, result:,
|
|
12
|
+
mode: rust_mode, auth_method:, auth_description:)
|
|
13
|
+
end
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
def init
|
|
@@ -22,9 +27,13 @@ module HaveAPI::ClientExamples
|
|
|
22
27
|
|
|
23
28
|
private
|
|
24
29
|
|
|
30
|
+
def rust_mode
|
|
31
|
+
self.class.rust_mode
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def render_rust(kind, rendering_context: context, **)
|
|
26
35
|
unless self.class.available?(rendering_context)
|
|
27
|
-
return { status: :limitation, reason: 'A Rust
|
|
36
|
+
return { status: :limitation, reason: 'A Rust API client documentation profile is required.' }.freeze
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: rust_mode, **)
|
|
@@ -33,26 +42,28 @@ module HaveAPI::ClientExamples
|
|
|
33
42
|
|
|
34
43
|
class RustAsyncClient < RustClient
|
|
35
44
|
id 'rust-async'
|
|
36
|
-
label 'Rust (async)'
|
|
45
|
+
label 'Rust API client (async)'
|
|
46
|
+
language 'rust'
|
|
47
|
+
language_label 'Rust'
|
|
48
|
+
variant 'Async'
|
|
37
49
|
code :rust
|
|
38
50
|
order 17
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def rust_mode
|
|
52
|
+
def self.rust_mode
|
|
43
53
|
:async
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
class RustBlockingClient < RustClient
|
|
48
58
|
id 'rust-blocking'
|
|
49
|
-
label 'Rust (blocking)'
|
|
59
|
+
label 'Rust API client (blocking)'
|
|
60
|
+
language 'rust'
|
|
61
|
+
language_label 'Rust'
|
|
62
|
+
variant 'Blocking'
|
|
50
63
|
code :rust
|
|
51
64
|
order 18
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def rust_mode
|
|
66
|
+
def self.rust_mode
|
|
56
67
|
:blocking
|
|
57
68
|
end
|
|
58
69
|
end
|
|
@@ -3,10 +3,17 @@ require 'haveapi/client_examples/ts_client'
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class TsAsyncClient < TsClient
|
|
5
5
|
label 'TypeScript (async)'
|
|
6
|
+
language 'typescript'
|
|
7
|
+
language_label 'TypeScript'
|
|
8
|
+
variant 'Async'
|
|
6
9
|
id 'typescript-async'
|
|
7
10
|
code :typescript
|
|
8
11
|
order 13
|
|
9
12
|
|
|
13
|
+
def self.detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
14
|
+
result[:status] == :limitation ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def asynchronous?
|
|
11
18
|
true
|
|
12
19
|
end
|
|
@@ -3,6 +3,9 @@ require 'haveapi/client_examples/js_client'
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class TsClient < JsClient
|
|
5
5
|
label 'TypeScript (callback)'
|
|
6
|
+
language 'typescript'
|
|
7
|
+
language_label 'TypeScript'
|
|
8
|
+
variant 'Callback'
|
|
6
9
|
id 'typescript-callback'
|
|
7
10
|
code :typescript
|
|
8
11
|
order 12
|
|
@@ -3,7 +3,10 @@ require 'haveapi/client_example'
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class TsGeneratedClient < HaveAPI::ClientExample
|
|
5
5
|
id 'typescript-generated'
|
|
6
|
-
label 'TypeScript
|
|
6
|
+
label 'TypeScript API client'
|
|
7
|
+
language 'typescript'
|
|
8
|
+
language_label 'TypeScript'
|
|
9
|
+
variant 'API client'
|
|
7
10
|
code :typescript
|
|
8
11
|
order 19
|
|
9
12
|
|
|
@@ -11,6 +14,11 @@ module HaveAPI::ClientExamples
|
|
|
11
14
|
context&.profile&.language == :typescript
|
|
12
15
|
end
|
|
13
16
|
|
|
17
|
+
def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
18
|
+
adapter_detail_levels_for(adapter: context.profile.adapter, kind:, context:, result:,
|
|
19
|
+
mode: :async, auth_method:, auth_description:)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def init
|
|
15
23
|
render_documentation(:init)
|
|
16
24
|
end
|
|
@@ -27,7 +35,7 @@ module HaveAPI::ClientExamples
|
|
|
27
35
|
|
|
28
36
|
def render_documentation(kind, rendering_context: context, **)
|
|
29
37
|
unless self.class.available?(rendering_context)
|
|
30
|
-
return { status: :limitation, reason: 'A
|
|
38
|
+
return { status: :limitation, reason: 'A TypeScript API client documentation profile is required.' }.freeze
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: :async, **)
|
|
@@ -4,12 +4,12 @@ module HaveAPI
|
|
|
4
4
|
class ClientExample
|
|
5
5
|
# A request-local snapshot: never freeze or retain the server's shared help.
|
|
6
6
|
class Context
|
|
7
|
-
FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile].freeze
|
|
7
|
+
FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile detail].freeze
|
|
8
8
|
attr_reader(*FIELDS)
|
|
9
9
|
|
|
10
10
|
def initialize(version:, host:, base_url:, version_url:, description:,
|
|
11
11
|
resource_path: [], resource: nil, action_name: nil, action: nil,
|
|
12
|
-
sample: nil, profile: nil)
|
|
12
|
+
sample: nil, profile: nil, detail: :simple)
|
|
13
13
|
@version = normalize(:version, version)
|
|
14
14
|
@host = normalize(:host, host)
|
|
15
15
|
@base_url = normalize(:base_url, base_url)
|
|
@@ -21,6 +21,7 @@ module HaveAPI
|
|
|
21
21
|
@action = normalize(:action, action)
|
|
22
22
|
@sample = normalize(:sample, sample)
|
|
23
23
|
@profile = normalize(:profile, profile)
|
|
24
|
+
@detail = normalize(:detail, detail)
|
|
24
25
|
freeze
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -54,6 +55,10 @@ module HaveAPI
|
|
|
54
55
|
when :action_name
|
|
55
56
|
value.nil? ? nil : value.to_s.dup.freeze
|
|
56
57
|
when :profile
|
|
58
|
+
value
|
|
59
|
+
when :detail
|
|
60
|
+
raise ArgumentError, 'detail must be :simple or :thorough' unless %i[simple thorough].include?(value)
|
|
61
|
+
|
|
57
62
|
value
|
|
58
63
|
else
|
|
59
64
|
copy(value)
|