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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/benchmarks/documentation.rb +29 -6
  4. data/benchmarks/endpoints.rb +153 -24
  5. data/benchmarks/spec/client_examples_spec.rb +109 -37
  6. data/doc/client-examples.md +218 -45
  7. data/doc/documentation-oauth.md +157 -0
  8. data/doc/documentation-ui.md +192 -0
  9. data/doc/index.md +3 -6
  10. data/doc/performance.md +39 -12
  11. data/haveapi.gemspec +1 -1
  12. data/lib/haveapi/authentication/chain.rb +19 -0
  13. data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
  14. data/lib/haveapi/client_example.rb +143 -15
  15. data/lib/haveapi/client_examples/curl.rb +30 -20
  16. data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
  17. data/lib/haveapi/client_examples/go_client.rb +9 -2
  18. data/lib/haveapi/client_examples/http.rb +9 -4
  19. data/lib/haveapi/client_examples/js_async_client.rb +7 -0
  20. data/lib/haveapi/client_examples/js_client.rb +47 -36
  21. data/lib/haveapi/client_examples/php_client.rb +161 -75
  22. data/lib/haveapi/client_examples/python_async_client.rb +3 -0
  23. data/lib/haveapi/client_examples/python_client.rb +61 -47
  24. data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
  25. data/lib/haveapi/client_examples/ruby_client.rb +80 -31
  26. data/lib/haveapi/client_examples/rust_client.rb +20 -9
  27. data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
  28. data/lib/haveapi/client_examples/ts_client.rb +3 -0
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
  30. data/lib/haveapi/documentation_context.rb +7 -2
  31. data/lib/haveapi/documentation_helpers.rb +223 -0
  32. data/lib/haveapi/documentation_oauth.rb +399 -0
  33. data/lib/haveapi/documentation_search.rb +225 -0
  34. data/lib/haveapi/documentation_search_index.rb +79 -0
  35. data/lib/haveapi/documentation_validators.rb +179 -0
  36. data/lib/haveapi/public/css/documentation.css +343 -0
  37. data/lib/haveapi/public/js/documentation.js +992 -0
  38. data/lib/haveapi/server.rb +144 -86
  39. data/lib/haveapi/shared_documentation_helpers.rb +54 -0
  40. data/lib/haveapi/version.rb +1 -1
  41. data/lib/haveapi/views/doc_layout.erb +16 -0
  42. data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
  43. data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
  44. data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
  45. data/lib/haveapi/views/documentation/search_form.erb +10 -0
  46. data/lib/haveapi/views/documentation/search_index.erb +1 -0
  47. data/lib/haveapi/views/documentation/search_results.erb +20 -0
  48. data/lib/haveapi/views/index.erb +2 -4
  49. data/lib/haveapi/views/main_layout.erb +12 -6
  50. data/lib/haveapi/views/shared_documentation.erb +12 -0
  51. data/lib/haveapi/views/shared_sidebar.erb +25 -0
  52. data/lib/haveapi/views/version_page/auth_body.erb +12 -33
  53. data/lib/haveapi/views/version_page/client_overview.erb +40 -0
  54. data/lib/haveapi/views/version_page/example_panels.erb +18 -0
  55. data/lib/haveapi/views/version_page/resource_body.erb +91 -132
  56. data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
  57. data/lib/haveapi/views/version_page/usage.erb +7 -0
  58. data/lib/haveapi/views/version_page.erb +36 -117
  59. data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
  60. data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
  61. data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
  62. data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
  63. data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
  64. data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
  65. data/lib/haveapi/views/version_sidebar.erb +59 -48
  66. data/spec/authentication/basic_spec.rb +10 -10
  67. data/spec/authentication/oauth2_chain_spec.rb +72 -0
  68. data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
  69. data/spec/client_examples/assets/documentation_test.cjs +817 -0
  70. data/spec/client_examples/context_performance_spec.rb +5 -4
  71. data/spec/client_examples/context_spec.rb +23 -2
  72. data/spec/client_examples/detail_spec.rb +175 -0
  73. data/spec/client_examples/dynamic_acceptance.md +59 -26
  74. data/spec/client_examples/dynamic_acceptance.rb +164 -44
  75. data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
  76. data/spec/client_examples/dynamic_fixture.rb +4 -3
  77. data/spec/client_examples/dynamic_js_spec.rb +20 -11
  78. data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
  79. data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
  80. data/spec/client_examples/dynamic_python_spec.rb +62 -11
  81. data/spec/client_examples/go_spec.rb +14 -1
  82. data/spec/client_examples/presentation_spec.rb +247 -0
  83. data/spec/client_examples/profile_mount_spec.rb +40 -13
  84. data/spec/client_examples/rust_client_spec.rb +35 -1
  85. data/spec/client_examples/shell_spec.rb +268 -0
  86. data/spec/client_examples/syntax_spec.rb +192 -0
  87. data/spec/client_examples/ts_generated_client_spec.rb +15 -2
  88. data/spec/client_examples/typescript_profiles_spec.rb +1 -1
  89. data/spec/documentation/auth_filtering_spec.rb +9 -4
  90. data/spec/documentation/canonical_routes_spec.rb +89 -0
  91. data/spec/documentation/client_examples_spec.rb +39 -5
  92. data/spec/documentation/client_overview_spec.rb +145 -0
  93. data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
  94. data/spec/documentation/example_variants_spec.rb +93 -0
  95. data/spec/documentation/fixtures/search_index.json +476 -0
  96. data/spec/documentation/identity_and_guides_spec.rb +130 -0
  97. data/spec/documentation/identity_context_spec.rb +180 -0
  98. data/spec/documentation/navigation_spec.rb +296 -0
  99. data/spec/documentation/oauth_fixture_spec.rb +39 -0
  100. data/spec/documentation/oauth_spec.rb +912 -0
  101. data/spec/documentation/oauth_transport_spec.rb +45 -0
  102. data/spec/documentation/performance_spec.rb +47 -6
  103. data/spec/documentation/reference_headings_spec.rb +190 -0
  104. data/spec/documentation/search_index_routes_spec.rb +202 -0
  105. data/spec/documentation/search_index_spec.rb +85 -0
  106. data/spec/documentation/search_routes_spec.rb +199 -0
  107. data/spec/documentation/search_spec.rb +134 -0
  108. data/spec/documentation/shared_pages_spec.rb +240 -0
  109. data/spec/documentation/validators_spec.rb +123 -0
  110. data/spec/documentation_spec.rb +3 -3
  111. data/spec/packaging_spec.rb +21 -2
  112. data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
  113. data/spec/performance/documentation_shapes_spec.rb +97 -0
  114. data/spec/performance/json_activation_spec.rb +78 -1
  115. data/spec/performance/release_lineage_spec.rb +4 -3
  116. data/spec/performance/runner_spec.rb +1 -0
  117. data/test_support/all_client_performance_fixture.rb +6 -10
  118. data/test_support/documentation_oauth_fixture.rb +54 -0
  119. data/test_support/documentation_performance_fixture.rb +19 -1
  120. data/test_support/dynamic_documentation_api.rb +35 -10
  121. data/test_support/dynamic_python_login_spec.rb +3 -11
  122. 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 SDK profile is required.' }.freeze
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).merge(lifetime: 'fixed')
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=$client_id&state=$state&redirect_uri=$client_redirect_uri HTTP/1.1
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 $client_redirect_uri?code=$authorization_code&state=$state
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=$authorization_code&redirect_uri=$client_redirect_uri&client_id=$client_id&client_secret=$client_secret
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('}(client, ready) => {
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: credential("HAVEAPI_TOKEN")}',
100
- basic: '{user: credential("HAVEAPI_USER"), password: credential("HAVEAPI_PASSWORD")}',
101
- oauth2: '{access_token: {access_token: credential("HAVEAPI_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: credential("HAVEAPI_USER"), password: credential("HAVEAPI_PASSWORD")}'
126
+ '{user: "user", password: "secret"}'
117
127
  when :token
118
128
  if token_login_fields(description).empty?
119
- '{token: credential("HAVEAPI_TOKEN")}'
129
+ '{token: "example-token"}'
120
130
  else
121
- "env.HAVEAPI_TOKEN !== undefined ? {token: credential(\"HAVEAPI_TOKEN\")} : #{token_login_options(description)}"
131
+ token_login_options(description)
122
132
  end
123
133
  when :oauth2
124
- '{access_token: {access_token: credential("HAVEAPI_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 = token_login_fields(description).map do |name, _|
143
- "[#{js_literal(name.to_s)}]: JSON.parse(credential(#{js_literal("HAVEAPI_AUTH_#{name}")}))"
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 input#{annotation} = Object.create(null);
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") input[name] = JSON.parse(credential("HAVEAPI_CHALLENGE_" + name));
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);\nconsole.log(result.response.meta);\n"
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(client, #{js_literal(current_resource_path)});\n"
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
- program(body, authentication: action_authentication)
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
- <<~END
11
- $api = new \\HaveAPI\\Client("#{base_url}", "#{version}");
12
- END
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
- when :basic
18
- <<~END
19
- #{init}
20
-
21
- $api->authenticate("basic", ["user" => "user", "password" => "secret"]);
22
- END
23
-
24
- when :token
25
- <<~END
26
- #{init}
27
-
28
- // Get token using username and password
29
- $api->authenticate("token", [#{auth_token_credentials(desc).map { |k, v| "\"#{k}\" => \"#{v}\"" }.join(', ')}]);
30
-
31
- echo "Token = ".$api->getAuthenticationProvider()->getToken();
32
-
33
- // Next time, the client can authenticate using the token directly
34
- $api->authenticate("token", ["token" => $savedToken]);
35
- END
36
-
37
- when :oauth2
38
- <<~END
39
- // OAuth2 requires session
40
- session_start();
41
-
42
- // Client instance
43
- #{init}
44
- // Check if we already have an access token
45
- if (isset($_SESSION["access_token"])) {
46
- // We're already authenticated, reuse the existing access token
47
- $api->authenticate("oauth2", ["access_token" => $_SESSION["access_token"]]);
48
-
49
- } else {
50
- // Follow the OAuth2 authorization process to get an access token using
51
- // authorization code
52
- $api->authenticate("oauth2", [
53
- // Client id and secret are given by the API server
54
- "client_id" => "your client id",
55
- "client_secret" => "your client secret",
56
-
57
- // This example code should run on the URL below
58
- "redirect_uri" => "https://your-client.tld/oauth2-callback",
59
-
60
- // Scopes are specific to the API implementation
61
- "scope" => "all",
62
- ]);
63
-
64
- $provider = $api->getAuthenticationProvider();
65
-
66
- // We don't have authorization code yet, request one
67
- if (!isset($_GET['code'])) {
68
- // Redirect the user to the authorization endpoint
69
- $provider->requestAuthorizationCode();
70
- exit;
71
-
72
- } else {
73
- // Request access token using the token endpoint
74
- $provider->requestAccessToken();
75
-
76
- // Store the access token in the session
77
- $_SESSION['access_token'] = $provider->jsonSerialize();
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
- END
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 = "#{init}\n"
167
+ out = +''
94
168
  out << "$reply = $api->#{resource_path.join('->')}->#{action_name}"
95
169
  out << "(#{args.join(', ')});\n"
96
170
 
97
- return out << response(sample) if sample[:status]
98
-
99
- out << '// Throws exception \\HaveAPI\\Client\\Exception\\ActionFailed'
100
- out
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} #{value(k.to_s)} => #{value(v)}"
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
- escaped = v.to_s.gsub(/[\\']/) { |char| "\\#{char}" }
169
- "'#{escaped}'"
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
@@ -3,6 +3,9 @@ require 'haveapi/client_examples/python_client'
3
3
  module HaveAPI::ClientExamples
4
4
  class PythonAsyncClient < PythonClient
5
5
  label 'Python (async)'
6
+ language 'python'
7
+ language_label 'Python'
8
+ variant 'Async'
6
9
  id 'python-async'
7
10
  code :python
8
11
  order 15