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
|
@@ -2,7 +2,9 @@ require 'haveapi/client_example'
|
|
|
2
2
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class GoClient < HaveAPI::ClientExample
|
|
5
|
-
label 'Go'
|
|
5
|
+
label 'Go API client'
|
|
6
|
+
language 'go'
|
|
7
|
+
language_label 'Go'
|
|
6
8
|
id 'go'
|
|
7
9
|
code :go
|
|
8
10
|
order 16
|
|
@@ -11,6 +13,11 @@ module HaveAPI::ClientExamples
|
|
|
11
13
|
context.profile && context.profile.language == :go
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
17
|
+
adapter_detail_levels_for(adapter: context.profile.adapter, kind:, context:, result:,
|
|
18
|
+
mode: :sync, auth_method:, auth_description:)
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
def init
|
|
15
22
|
render_documentation(:init)
|
|
16
23
|
end
|
|
@@ -27,7 +34,7 @@ module HaveAPI::ClientExamples
|
|
|
27
34
|
|
|
28
35
|
def render_documentation(kind, sample: nil, **)
|
|
29
36
|
unless context && self.class.available?(context)
|
|
30
|
-
return { status: :limitation, reason: 'A Go
|
|
37
|
+
return { status: :limitation, reason: 'A Go API client profile is required.' }.freeze
|
|
31
38
|
end
|
|
32
39
|
|
|
33
40
|
selected = kind == :example ? context.with(sample:) : context
|
|
@@ -5,6 +5,9 @@ require 'haveapi/client_example'
|
|
|
5
5
|
module HaveAPI::ClientExamples
|
|
6
6
|
class Http < HaveAPI::ClientExample
|
|
7
7
|
label 'HTTP'
|
|
8
|
+
language 'http'
|
|
9
|
+
language_label 'HTTP'
|
|
10
|
+
detail_levels :simple
|
|
8
11
|
code :http
|
|
9
12
|
order 100
|
|
10
13
|
|
|
@@ -27,7 +30,7 @@ module HaveAPI::ClientExamples
|
|
|
27
30
|
END
|
|
28
31
|
|
|
29
32
|
when :token
|
|
30
|
-
login = auth_token_credentials(desc)
|
|
33
|
+
login = auth_token_credentials(desc)
|
|
31
34
|
|
|
32
35
|
<<~END
|
|
33
36
|
POST /_auth/token/tokens HTTP/1.1
|
|
@@ -40,22 +43,24 @@ module HaveAPI::ClientExamples
|
|
|
40
43
|
when :oauth2
|
|
41
44
|
<<~END
|
|
42
45
|
# 1) Request authorization code
|
|
43
|
-
GET #{desc[:authorize_path]}?response_type=code&client_id
|
|
46
|
+
GET #{desc[:authorize_path]}?response_type=code&client_id=example-client&state=example-state&redirect_uri=https%3A%2F%2Fexample.org%2Fcallback HTTP/1.1
|
|
44
47
|
Host: #{host}
|
|
45
48
|
|
|
46
49
|
# 2) The user logs in using this API
|
|
47
50
|
|
|
48
51
|
# 3) The API then redirects the user back to the client application
|
|
49
|
-
GET
|
|
52
|
+
GET /callback?code=example-code&state=example-state
|
|
50
53
|
Host: client-application
|
|
51
54
|
|
|
52
55
|
# 4) The client application requests access token
|
|
53
56
|
POST #{desc[:token_path]}
|
|
54
57
|
Content-Type: application/x-www-form-urlencoded
|
|
55
58
|
|
|
56
|
-
grant_type=authorization_code&code
|
|
59
|
+
grant_type=authorization_code&code=example-code&redirect_uri=https%3A%2F%2Fexample.org%2Fcallback&client_id=example-client&client_secret=example-client-secret
|
|
57
60
|
END
|
|
58
61
|
end
|
|
62
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
63
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def request(sample)
|
|
@@ -3,10 +3,17 @@ require 'haveapi/client_examples/js_client'
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class JsAsyncClient < JsClient
|
|
5
5
|
label 'JavaScript (async)'
|
|
6
|
+
language 'javascript'
|
|
7
|
+
language_label 'JavaScript'
|
|
8
|
+
variant 'Async'
|
|
6
9
|
id 'javascript-async'
|
|
7
10
|
code :javascript
|
|
8
11
|
order 11
|
|
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
|
|
@@ -5,10 +5,17 @@ module HaveAPI::ClientExamples
|
|
|
5
5
|
include DynamicHelpers
|
|
6
6
|
|
|
7
7
|
label 'JavaScript (callback)'
|
|
8
|
+
language 'javascript'
|
|
9
|
+
language_label 'JavaScript'
|
|
10
|
+
variant 'Callback'
|
|
8
11
|
id 'javascript-callback'
|
|
9
12
|
code :javascript
|
|
10
13
|
order 10
|
|
11
14
|
|
|
15
|
+
def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
16
|
+
kind == :example && result[:status] != :limitation ? DEFAULT_DETAIL_LEVELS : SIMPLE_DETAIL_LEVELS
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def asynchronous?
|
|
13
20
|
false
|
|
14
21
|
end
|
|
@@ -37,21 +44,24 @@ module HaveAPI::ClientExamples
|
|
|
37
44
|
asynchronous? ? 'AsyncClient' : 'CallbackClient'
|
|
38
45
|
end
|
|
39
46
|
|
|
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
|
|
50
|
-
|
|
51
47
|
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
|
|
48
|
+
imports = "import { #{client_name}#{', AsyncError' if asynchronous? && thorough?} } from \"haveapi-client\";\n"
|
|
54
49
|
declaration = "const api = new #{client_name}(#{js_literal(api_base)}, {version: #{js_literal(api_version)}});\n"
|
|
50
|
+
unless thorough?
|
|
51
|
+
return "#{imports}\n#{declaration}#{authentication || "await api.setup();\n"}#{body}" if asynchronous?
|
|
52
|
+
|
|
53
|
+
return <<~JS
|
|
54
|
+
#{imports}
|
|
55
|
+
#{declaration}
|
|
56
|
+
#{authentication || 'api.setup('}(api, ready) => {
|
|
57
|
+
if (!ready) {
|
|
58
|
+
console.error("Discovery or authentication failed");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
#{indent(body)}});
|
|
62
|
+
JS
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
if asynchronous?
|
|
56
66
|
<<~JS
|
|
57
67
|
#{imports}
|
|
@@ -71,7 +81,7 @@ module HaveAPI::ClientExamples
|
|
|
71
81
|
<<~JS
|
|
72
82
|
#{imports}
|
|
73
83
|
#{declaration}
|
|
74
|
-
#{authentication || 'api.setup('}(
|
|
84
|
+
#{authentication || 'api.setup('}(api, ready) => {
|
|
75
85
|
if (!ready) {
|
|
76
86
|
console.error("Discovery or authentication failed");
|
|
77
87
|
return;
|
|
@@ -86,7 +96,7 @@ module HaveAPI::ClientExamples
|
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def init
|
|
89
|
-
code_result { program("console.log(\"Discovery complete\");\n") }
|
|
99
|
+
code_result { program(thorough? ? "console.log(\"Discovery complete\");\n" : '') }
|
|
90
100
|
end
|
|
91
101
|
|
|
92
102
|
def action_authentication
|
|
@@ -96,9 +106,9 @@ module HaveAPI::ClientExamples
|
|
|
96
106
|
raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
|
|
97
107
|
|
|
98
108
|
options = {
|
|
99
|
-
token: '{token:
|
|
100
|
-
basic: '{user:
|
|
101
|
-
oauth2: '{access_token: {access_token:
|
|
109
|
+
token: '{token: "example-token"}',
|
|
110
|
+
basic: '{user: "user", password: "secret"}',
|
|
111
|
+
oauth2: '{access_token: {access_token: "example-access-token"}}'
|
|
102
112
|
}.fetch(method)
|
|
103
113
|
if asynchronous?
|
|
104
114
|
"await api.authenticate(#{js_literal(method.to_s)}, #{options});\n"
|
|
@@ -113,42 +123,41 @@ module HaveAPI::ClientExamples
|
|
|
113
123
|
code_result do
|
|
114
124
|
options = case method.to_sym
|
|
115
125
|
when :basic
|
|
116
|
-
'{user:
|
|
126
|
+
'{user: "user", password: "secret"}'
|
|
117
127
|
when :token
|
|
118
128
|
if token_login_fields(description).empty?
|
|
119
|
-
'{token:
|
|
129
|
+
'{token: "example-token"}'
|
|
120
130
|
else
|
|
121
|
-
|
|
131
|
+
token_login_options(description)
|
|
122
132
|
end
|
|
123
133
|
when :oauth2
|
|
124
|
-
'{access_token: {access_token:
|
|
134
|
+
'{access_token: {access_token: "example-access-token"}}'
|
|
125
135
|
end
|
|
126
136
|
authentication = if asynchronous?
|
|
127
137
|
"await api.authenticate(#{js_literal(method.to_s)}, #{options});\n"
|
|
128
138
|
else
|
|
129
139
|
"api.authenticate(#{js_literal(method.to_s)}, #{options}, "
|
|
130
140
|
end
|
|
131
|
-
source = program("console.log(\"Authenticated\");\n", authentication:)
|
|
141
|
+
source = program(thorough? ? "console.log(\"Authenticated\");\n" : '', authentication:)
|
|
132
142
|
if method.to_sym == :oauth2
|
|
133
143
|
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
144
|
end
|
|
137
145
|
source
|
|
138
146
|
end
|
|
139
147
|
end
|
|
140
148
|
|
|
141
149
|
def token_login_options(description)
|
|
142
|
-
fields =
|
|
143
|
-
|
|
144
|
-
end
|
|
150
|
+
fields = auth_token_credentials(description).map { |name, value| "[#{js_literal(name)}]: #{js_literal(value)}" }
|
|
151
|
+
continuations = auth_token_continuations(description)
|
|
145
152
|
challenge = asynchronous? ? 'onChallenge' : 'callback'
|
|
146
|
-
annotation = typescript? ? ': Record<string, unknown
|
|
153
|
+
annotation = typescript? ? ': Record<string, Record<string, unknown>>' : ''
|
|
147
154
|
fields << <<~JS.strip
|
|
148
155
|
#{challenge}: (action, parameters) => {
|
|
149
|
-
const
|
|
156
|
+
const continuations#{annotation} = #{js_literal(continuations)};
|
|
157
|
+
if (!Object.prototype.hasOwnProperty.call(continuations, action)) throw new Error("Unknown authentication continuation");
|
|
158
|
+
const input = continuations[action];
|
|
150
159
|
for (const name of Object.keys(parameters)) {
|
|
151
|
-
if (name !== "token"
|
|
160
|
+
if (name !== "token" && !Object.prototype.hasOwnProperty.call(input, name)) throw new Error("Unknown authentication input");
|
|
152
161
|
}
|
|
153
162
|
return input;
|
|
154
163
|
}
|
|
@@ -179,13 +188,14 @@ module HaveAPI::ClientExamples
|
|
|
179
188
|
end.join
|
|
180
189
|
options = request.nil? ? [] : ["{params: #{js_literal(request)}}"]
|
|
181
190
|
body = "const result = await #{resource}.action(#{js_literal(current_action_name)})(#{(args + options).join(', ')});\n"
|
|
182
|
-
body << "console.log(result.value);\
|
|
191
|
+
body << "console.log(result.value);\n"
|
|
192
|
+
body << "console.log(result.response.meta);\n" if thorough?
|
|
183
193
|
if field(current_action, :blocking)
|
|
184
194
|
body << "// Request success alone does not establish operation completion.\n"
|
|
185
195
|
body << "const completed = await api.waitForCompletion(result, {timeoutMs: 300000});\nconsole.log(completed.state);\n"
|
|
186
196
|
end
|
|
187
197
|
else
|
|
188
|
-
body = "const resource = CallbackClient.findResource(
|
|
198
|
+
body = "const resource = CallbackClient.findResource(api, #{js_literal(current_resource_path)});\n"
|
|
189
199
|
body << "const action = CallbackClient.findAction(resource, #{js_literal(current_action_name)});\n"
|
|
190
200
|
options = request.nil? ? '' : "params: #{js_literal(request)},\n"
|
|
191
201
|
options << <<~JS
|
|
@@ -195,13 +205,14 @@ module HaveAPI::ClientExamples
|
|
|
195
205
|
return;
|
|
196
206
|
}
|
|
197
207
|
console.log(reply.response());
|
|
198
|
-
console.log(reply.meta());
|
|
199
|
-
}
|
|
200
208
|
JS
|
|
209
|
+
options << " console.log(reply.meta());\n" if thorough?
|
|
210
|
+
options << "}\n"
|
|
201
211
|
body << "action.directInvoke(#{(args + ["{\n#{indent(options)}}"]).join(', ')});\n"
|
|
202
212
|
body << "// This prints the initial response; use its action-state metadata to monitor completion.\n" if field(current_action, :blocking)
|
|
203
213
|
end
|
|
204
|
-
|
|
214
|
+
authentication = action_authentication
|
|
215
|
+
thorough? ? program(body, authentication:) : body
|
|
205
216
|
end
|
|
206
217
|
end
|
|
207
218
|
end
|
|
@@ -3,85 +3,159 @@ require 'haveapi/client_example'
|
|
|
3
3
|
module HaveAPI::ClientExamples
|
|
4
4
|
class PhpClient < HaveAPI::ClientExample
|
|
5
5
|
label 'PHP'
|
|
6
|
+
language 'php'
|
|
7
|
+
language_label 'PHP'
|
|
6
8
|
code :php
|
|
7
9
|
order 20
|
|
8
10
|
|
|
11
|
+
def self.detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
|
|
12
|
+
result[:status] == :limitation ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialization
|
|
16
|
+
"$api = new \\HaveAPI\\Client(#{value(base_url)}, #{value(version.to_s)});\n"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_error_handling(body)
|
|
20
|
+
<<~PHP
|
|
21
|
+
try {
|
|
22
|
+
#{body.lines.map { |line| line == "\n" ? line : " #{line}" }.join.chomp}
|
|
23
|
+
} catch (\\HaveAPI\\Client\\Exception\\ValidationError $error) {
|
|
24
|
+
echo $error->getMessage() . "\\n";
|
|
25
|
+
print_r($error->getErrors());
|
|
26
|
+
} catch (\\HaveAPI\\Client\\Exception\\ActionFailed $error) {
|
|
27
|
+
echo $error->getMessage() . "\\n";
|
|
28
|
+
print_r($error->getErrors());
|
|
29
|
+
} catch (\\HaveAPI\\Client\\Exception\\Base $error) {
|
|
30
|
+
echo $error->getMessage() . "\\n";
|
|
31
|
+
} catch (\\Exception $error) {
|
|
32
|
+
// Dependency failures still propagate to the application.
|
|
33
|
+
echo $error->getMessage() . "\\n";
|
|
34
|
+
throw $error;
|
|
35
|
+
}
|
|
36
|
+
PHP
|
|
37
|
+
end
|
|
38
|
+
|
|
9
39
|
def init
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
40
|
+
thorough? ? with_error_handling("#{initialization}$api->setup();\n") : initialization
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def auth_login_literal(name, parameter, value)
|
|
44
|
+
return value unless value.nil?
|
|
45
|
+
|
|
46
|
+
# Token::requestToken uses isset: false survives, explicit null does not.
|
|
47
|
+
auth_literal(name, parameter, excluding: [nil])
|
|
13
48
|
end
|
|
14
49
|
|
|
15
50
|
def auth(method, desc)
|
|
16
|
-
case method
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
source = case method
|
|
52
|
+
when :basic
|
|
53
|
+
<<~END
|
|
54
|
+
#{initialization.chomp}
|
|
55
|
+
|
|
56
|
+
$api->authenticate('basic', ['user' => 'user', 'password' => 'secret']);
|
|
57
|
+
END
|
|
58
|
+
|
|
59
|
+
when :token
|
|
60
|
+
<<~END
|
|
61
|
+
#{initialization.chomp}
|
|
62
|
+
|
|
63
|
+
// Get token using username and password
|
|
64
|
+
$api->authenticate('token', #{token_options(desc)});
|
|
65
|
+
|
|
66
|
+
// Next time, the client can authenticate using the token directly
|
|
67
|
+
$api->authenticate('token', ['token' => 'example-token']);
|
|
68
|
+
END
|
|
69
|
+
|
|
70
|
+
when :oauth2
|
|
71
|
+
<<~END
|
|
72
|
+
// OAuth2 requires session
|
|
73
|
+
session_start();
|
|
74
|
+
|
|
75
|
+
// Client instance
|
|
76
|
+
#{initialization}
|
|
77
|
+
// Check if we already have an access token
|
|
78
|
+
if (isset($_SESSION['access_token'])) {
|
|
79
|
+
// We're already authenticated, reuse the existing access token
|
|
80
|
+
$api->authenticate('oauth2', ['access_token' => $_SESSION['access_token']]);
|
|
81
|
+
} else {
|
|
82
|
+
// Follow the OAuth2 authorization process to get an access token using
|
|
83
|
+
// authorization code
|
|
84
|
+
$api->authenticate('oauth2', [
|
|
85
|
+
// Client id and secret are given by the API server
|
|
86
|
+
'client_id' => 'example-client',
|
|
87
|
+
'client_secret' => 'example-client-secret',
|
|
88
|
+
|
|
89
|
+
// This example code should run on the URL below
|
|
90
|
+
'redirect_uri' => 'https://your-client.tld/oauth2-callback',
|
|
91
|
+
|
|
92
|
+
// Scopes are specific to the API implementation
|
|
93
|
+
'scope' => 'all',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
$provider = $api->getAuthenticationProvider();
|
|
97
|
+
|
|
98
|
+
// We don't have authorization code yet, request one
|
|
99
|
+
if (!isset($_GET['code'])) {
|
|
100
|
+
// Redirect the user to the authorization endpoint
|
|
101
|
+
$provider->requestAuthorizationCode();
|
|
102
|
+
exit;
|
|
103
|
+
} else {
|
|
104
|
+
// Request access token using the token endpoint
|
|
105
|
+
$provider->requestAccessToken();
|
|
106
|
+
|
|
107
|
+
// Store the access token in the session
|
|
108
|
+
$_SESSION['access_token'] = $provider->jsonSerialize();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
END
|
|
112
|
+
end
|
|
113
|
+
return unless source
|
|
114
|
+
|
|
115
|
+
source = source.sub(initialization, "#{initialization}$api->setup();\n") if thorough?
|
|
116
|
+
thorough? ? with_error_handling(source) : source
|
|
117
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
118
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def token_options(description)
|
|
122
|
+
fields = auth_token_credentials(description).map { |name, item| "#{value(name)} => #{value(item)}" }
|
|
123
|
+
continuations = auth_token_continuations(description)
|
|
124
|
+
unless continuations.empty?
|
|
125
|
+
fields << <<~PHP.strip
|
|
126
|
+
'callback' => function ($action, $token, $parameters) {
|
|
127
|
+
$continuations = #{value(continuations)};
|
|
128
|
+
if (!array_key_exists($action, $continuations)) {
|
|
129
|
+
throw new \\InvalidArgumentException('Unknown authentication continuation');
|
|
130
|
+
}
|
|
131
|
+
$input = $continuations[$action];
|
|
132
|
+
foreach (array_keys((array) $parameters) as $name) {
|
|
133
|
+
if ($name !== 'token' && !array_key_exists($name, $input)) {
|
|
134
|
+
throw new \\InvalidArgumentException('Unknown authentication input');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return $input;
|
|
79
138
|
}
|
|
80
|
-
|
|
139
|
+
PHP
|
|
81
140
|
end
|
|
141
|
+
return '[]' if fields.empty?
|
|
142
|
+
|
|
143
|
+
"[\n#{fields.map { |field| field.lines.map { |line| " #{line}" }.join.chomp }.join(",\n")}\n]"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def action_authentication
|
|
147
|
+
return '' unless action[:auth] && context
|
|
148
|
+
|
|
149
|
+
providers = auth_field(context.description, :authentication, {}).keys.map(&:to_s)
|
|
150
|
+
return "$api->authenticate('token', ['token' => 'example-token']);" if providers.include?('token')
|
|
151
|
+
return "$api->authenticate('basic', ['user' => 'user', 'password' => 'secret']);" if providers.include?('basic')
|
|
152
|
+
return "$api->authenticate('oauth2', ['access_token' => ['access_token' => 'example-access-token']]);" if providers.include?('oauth2')
|
|
153
|
+
|
|
154
|
+
raise HaveAPI::ClientExample::UnsupportedAuthentication, 'This action requires authentication unsupported by this client.'
|
|
82
155
|
end
|
|
83
156
|
|
|
84
157
|
def example(sample)
|
|
158
|
+
credentials = action_authentication
|
|
85
159
|
args = []
|
|
86
160
|
|
|
87
161
|
args.concat(sample[:path_params]) if sample[:path_params]
|
|
@@ -90,14 +164,18 @@ module HaveAPI::ClientExamples
|
|
|
90
164
|
args << format_parameters(:input, sample[:request])
|
|
91
165
|
end
|
|
92
166
|
|
|
93
|
-
out =
|
|
167
|
+
out = +''
|
|
94
168
|
out << "$reply = $api->#{resource_path.join('->')}->#{action_name}"
|
|
95
169
|
out << "(#{args.join(', ')});\n"
|
|
96
170
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
171
|
+
out << if sample[:status]
|
|
172
|
+
response(sample)
|
|
173
|
+
else
|
|
174
|
+
'// Throws exception \\HaveAPI\\Client\\Exception\\ActionFailed'
|
|
175
|
+
end
|
|
176
|
+
thorough? ? with_error_handling(["#{initialization}$api->setup();", credentials, out.chomp].reject(&:empty?).join("\n\n")) : out
|
|
177
|
+
rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
|
|
178
|
+
{ status: :limitation, reason: e.message }.freeze
|
|
101
179
|
end
|
|
102
180
|
|
|
103
181
|
def response(sample)
|
|
@@ -148,7 +226,7 @@ module HaveAPI::ClientExamples
|
|
|
148
226
|
|
|
149
227
|
def format_parameters(dir, params, prefix = '')
|
|
150
228
|
ret = params.map do |k, v|
|
|
151
|
-
"#{prefix}
|
|
229
|
+
"#{prefix} #{value(k.to_s)} => #{value(v)}"
|
|
152
230
|
end
|
|
153
231
|
|
|
154
232
|
"#{prefix}[\n#{ret.join(",\n")}\n#{prefix}]"
|
|
@@ -165,8 +243,16 @@ module HaveAPI::ClientExamples
|
|
|
165
243
|
when ::Hash
|
|
166
244
|
"[#{v.map { |key, item| "#{value(key.to_s)} => #{value(item)}" }.join(', ')}]"
|
|
167
245
|
else
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
string = v.to_s
|
|
247
|
+
if string.match?(/[\x00-\x1f\x7f]/)
|
|
248
|
+
escaped = string.gsub(/[\\"$\x00-\x1f\x7f]/) do |char|
|
|
249
|
+
char.ord < 32 || char.ord == 127 ? format('\\x%02x', char.ord) : "\\#{char}"
|
|
250
|
+
end
|
|
251
|
+
"\"#{escaped}\""
|
|
252
|
+
else
|
|
253
|
+
escaped = string.gsub(/[\\']/) { |char| "\\#{char}" }
|
|
254
|
+
"'#{escaped}'"
|
|
255
|
+
end
|
|
170
256
|
end
|
|
171
257
|
end
|
|
172
258
|
end
|