haveapi 0.32.1 → 0.32.2

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -8
  3. data/README.md +3 -422
  4. data/Rakefile +17 -7
  5. data/benchmarks/performance_compare.rb +27 -6
  6. data/doc/cs/client-examples.md +535 -0
  7. data/doc/cs/create-client.md +118 -0
  8. data/doc/cs/json-schema.html +1182 -0
  9. data/doc/cs/protocol.md +595 -0
  10. data/doc/cs/readme.md +432 -0
  11. data/doc/cs/typed-input-validation.md +122 -0
  12. data/doc/{client-examples.md → en/client-examples.md} +20 -10
  13. data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
  14. data/doc/{performance.md → en/performance.md} +11 -0
  15. data/doc/{protocol.md → en/protocol.md} +2 -2
  16. data/doc/en/readme.md +423 -0
  17. data/haveapi.gemspec +2 -2
  18. data/lib/haveapi/client_example.rb +18 -5
  19. data/lib/haveapi/client_examples/curl.rb +5 -5
  20. data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
  21. data/lib/haveapi/client_examples/go_client.rb +1 -1
  22. data/lib/haveapi/client_examples/http.rb +4 -4
  23. data/lib/haveapi/client_examples/js_client.rb +10 -10
  24. data/lib/haveapi/client_examples/php_client.rb +24 -25
  25. data/lib/haveapi/client_examples/python_client.rb +11 -11
  26. data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
  27. data/lib/haveapi/client_examples/ruby_client.rb +24 -16
  28. data/lib/haveapi/client_examples/rust_client.rb +1 -1
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
  30. data/lib/haveapi/documentation_context.rb +14 -2
  31. data/lib/haveapi/documentation_helpers.rb +92 -15
  32. data/lib/haveapi/documentation_locale.rb +65 -0
  33. data/lib/haveapi/documentation_oauth.rb +12 -12
  34. data/lib/haveapi/documentation_search.rb +5 -5
  35. data/lib/haveapi/documentation_validators.rb +43 -33
  36. data/lib/haveapi/locales/cs.yml +468 -0
  37. data/lib/haveapi/locales/en.yml +467 -0
  38. data/lib/haveapi/public/css/documentation.css +8 -4
  39. data/lib/haveapi/public/js/documentation.js +73 -20
  40. data/lib/haveapi/server.rb +32 -47
  41. data/lib/haveapi/shared_documentation_helpers.rb +53 -5
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/doc_layout.erb +3 -2
  44. data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
  45. data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
  46. data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
  47. data/lib/haveapi/views/documentation/search_form.erb +5 -4
  48. data/lib/haveapi/views/documentation/search_results.erb +6 -6
  49. data/lib/haveapi/views/index.erb +3 -3
  50. data/lib/haveapi/views/main_layout.erb +4 -4
  51. data/lib/haveapi/views/shared_documentation.erb +8 -5
  52. data/lib/haveapi/views/shared_sidebar.erb +11 -11
  53. data/lib/haveapi/views/version_page/auth_body.erb +7 -7
  54. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  55. data/lib/haveapi/views/version_page/client_example.erb +1 -1
  56. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  57. data/lib/haveapi/views/version_page/client_overview.erb +7 -5
  58. data/lib/haveapi/views/version_page/example_panels.erb +5 -2
  59. data/lib/haveapi/views/version_page/resource_body.erb +24 -24
  60. data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
  61. data/lib/haveapi/views/version_page/usage.erb +5 -5
  62. data/lib/haveapi/views/version_page.erb +11 -11
  63. data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
  64. data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
  65. data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
  66. data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
  67. data/lib/haveapi/views/version_sidebar.erb +33 -18
  68. data/spec/client_examples/assets/documentation_test.cjs +107 -9
  69. data/spec/client_examples/comment_capture.rb +72 -0
  70. data/spec/client_examples/comments_spec.rb +162 -0
  71. data/spec/client_examples/context_spec.rb +18 -2
  72. data/spec/client_examples/detail_spec.rb +68 -0
  73. data/spec/documentation/client_overview_spec.rb +60 -0
  74. data/spec/documentation/example_selection_spec.rb +192 -0
  75. data/spec/documentation/language_settings_spec.rb +132 -0
  76. data/spec/documentation/locale_spec.rb +367 -0
  77. data/spec/documentation/navigation_spec.rb +61 -3
  78. data/spec/documentation/oauth_spec.rb +42 -0
  79. data/spec/documentation/search_index_routes_spec.rb +8 -4
  80. data/spec/documentation/search_spec.rb +1 -1
  81. data/spec/documentation/shared_pages_spec.rb +63 -2
  82. data/spec/documentation/validators_spec.rb +25 -0
  83. data/spec/extensions/exception_mailer_spec.rb +2 -2
  84. data/spec/packaging_spec.rb +68 -9
  85. data/spec/performance/event_baseline_spec.rb +1 -1
  86. data/spec/performance/source_versions_spec.rb +111 -2
  87. metadata +24 -11
  88. /data/doc/{create-client.md → en/create-client.md} +0 -0
  89. /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
  90. /data/doc/{json-schema.html → en/json-schema.html} +0 -0
  91. /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
@@ -47,9 +47,9 @@ module HaveAPI::ClientExamples
47
47
  result = {}
48
48
  value.each do |key, entry|
49
49
  unless key.is_a?(String) || key.is_a?(Symbol)
50
- raise Unsupported, 'This sample has a non-string object key.'
50
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.object_key')
51
51
  end
52
- raise Unsupported, 'This sample has duplicate serialized object keys.' if result.has_key?(key.to_s)
52
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.duplicate_object_keys') if result.has_key?(key.to_s)
53
53
 
54
54
  result[key.to_s] = json_data(entry)
55
55
  end
@@ -59,17 +59,17 @@ module HaveAPI::ClientExamples
59
59
  when String, Integer, TrueClass, FalseClass, NilClass
60
60
  value
61
61
  when Float
62
- raise Unsupported, 'This sample has a non-finite number.' unless value.finite?
62
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.non_finite_number') unless value.finite?
63
63
 
64
64
  value
65
65
  else
66
- raise Unsupported, 'This sample contains a value without a supported literal representation.'
66
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.literal')
67
67
  end
68
68
  end
69
69
 
70
70
  def structured_sample(sample)
71
71
  unless sample.is_a?(Hash) && (!field(sample, :request).nil? || !field(sample, :response).nil?)
72
- raise Unsupported, 'This example contains prose only; no executable request is supplied.'
72
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.prose_only')
73
73
  end
74
74
 
75
75
  %i[global object].each do |scope|
@@ -77,29 +77,29 @@ module HaveAPI::ClientExamples
77
77
 
78
78
  input = field(field(field(current_action, :meta), scope), :input)
79
79
  if field(input, :parameters, {}).values.any? { |parameter| field(parameter, :required) }
80
- raise Unsupported, 'This action requires input metadata that the sample format cannot supply.'
80
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.input_metadata')
81
81
  end
82
82
  end
83
83
 
84
84
  placeholders = field(current_action, :path, '').scan(/\{([a-zA-Z0-9_-]+)\}/).flatten
85
85
  values = field(sample, :path_params, []) || []
86
86
  unless values.is_a?(Array) && values.length == placeholders.length
87
- raise Unsupported, 'The sample does not supply every action path argument.'
87
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.path_arguments_missing')
88
88
  end
89
89
  unless values.all? { |value| value.is_a?(String) || value.is_a?(Integer) }
90
- raise Unsupported, 'Action path arguments must be strings or integers.'
90
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.path_argument_type')
91
91
  end
92
92
 
93
93
  request = field(sample, :request)
94
94
  input = field(current_action, :input)
95
95
  if !request.nil? && input.nil? && request != {}
96
- raise Unsupported, 'The sample supplies input for an action without an input description.'
96
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.input_description_missing')
97
97
  end
98
98
 
99
99
  if request.is_a?(Hash) && input
100
100
  known = field(input, :parameters, {}).keys.map(&:to_s)
101
101
  unless (request.keys.map(&:to_s) - known).empty?
102
- raise Unsupported, 'The sample contains input parameters absent from the visible description.'
102
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.input_hidden')
103
103
  end
104
104
  end
105
105
  [placeholders.zip(values).to_h, json_data(request)]
@@ -34,7 +34,7 @@ module HaveAPI::ClientExamples
34
34
 
35
35
  def render_documentation(kind, sample: nil, **)
36
36
  unless context && self.class.available?(context)
37
- return { status: :limitation, reason: 'A Go API client profile is required.' }.freeze
37
+ return { status: :limitation, reason: HaveAPI.t('haveapi.documentation.limitations.go_profile') }.freeze
38
38
  end
39
39
 
40
40
  selected = kind == :example ? context.with(sample:) : context
@@ -42,17 +42,17 @@ module HaveAPI::ClientExamples
42
42
 
43
43
  when :oauth2
44
44
  <<~END
45
- # 1) Request authorization code
45
+ #{comment_lines(example_comment('http_oauth_code', default: '1) Request authorization code'), '# ')}
46
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
47
47
  Host: #{host}
48
48
 
49
- # 2) The user logs in using this API
49
+ #{comment_lines(example_comment('http_oauth_login', default: '2) The user logs in using this API'), '# ')}
50
50
 
51
- # 3) The API then redirects the user back to the client application
51
+ #{comment_lines(example_comment('http_oauth_redirect', default: '3) The API then redirects the user back to the client application'), '# ')}
52
52
  GET /callback?code=example-code&state=example-state
53
53
  Host: client-application
54
54
 
55
- # 4) The client application requests access token
55
+ #{comment_lines(example_comment('http_oauth_token', default: '4) The client application requests access token'), '# ')}
56
56
  POST #{desc[:token_path]}
57
57
  Content-Type: application/x-www-form-urlencoded
58
58
 
@@ -32,7 +32,7 @@ module HaveAPI::ClientExamples
32
32
  when Array
33
33
  "[#{value.map { |entry| js_literal(entry) }.join(', ')}]"
34
34
  when Integer
35
- raise Unsupported, 'This sample contains an integer outside JavaScript exact precision.' if value.abs > 9_007_199_254_740_991
35
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.javascript_integer') if value.abs > 9_007_199_254_740_991
36
36
 
37
37
  value.to_s
38
38
  else
@@ -103,7 +103,7 @@ module HaveAPI::ClientExamples
103
103
  return nil unless field(current_action, :auth)
104
104
 
105
105
  method = %i[token basic oauth2].find { |name| supported_auth?(name) }
106
- raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
106
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.action_authentication') unless method
107
107
 
108
108
  options = {
109
109
  token: '{token: "example-token"}',
@@ -118,7 +118,7 @@ module HaveAPI::ClientExamples
118
118
  end
119
119
 
120
120
  def auth(method, description)
121
- return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
121
+ return limitation(HaveAPI.t('haveapi.documentation.limitations.authentication_provider')) unless supported_auth?(method)
122
122
 
123
123
  code_result do
124
124
  options = case method.to_sym
@@ -140,7 +140,7 @@ module HaveAPI::ClientExamples
140
140
  end
141
141
  source = program(thorough? ? "console.log(\"Authenticated\");\n" : '', authentication:)
142
142
  if method.to_sym == :oauth2
143
- source = "// Use an existing access token; this client does not run an OAuth authorization flow.\n#{source}"
143
+ source = "#{comment_lines(example_comment('js_existing_oauth', default: 'Use an existing access token; this client does not run an OAuth authorization flow.'), '// ')}\n#{source}"
144
144
  end
145
145
  source
146
146
  end
@@ -169,16 +169,16 @@ module HaveAPI::ClientExamples
169
169
  code_result do
170
170
  path, request = structured_sample(sample)
171
171
  if request.is_a?(Array)
172
- raise Unsupported, 'This JavaScript client accepts object input only; list input cannot be represented without dropping values.'
172
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.javascript_list_input')
173
173
  end
174
174
  if !request.nil? && !request.is_a?(Hash)
175
- raise Unsupported, 'This JavaScript client requires object input.'
175
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.javascript_object_input')
176
176
  end
177
177
  if !asynchronous? && field(field(current_action, :input), :parameters, {}).keys.any? { |name| %w[hasOwnProperty __proto__].include?(name.to_s) }
178
- raise Unsupported, 'This callback client cannot safely represent this input parameter name.'
178
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.callback_parameter_name')
179
179
  end
180
180
  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) }
181
- raise Unsupported, 'This JavaScript client cannot preserve nested values in query input.'
181
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.javascript_nested_query')
182
182
  end
183
183
 
184
184
  args = path.values.map { |value| js_literal(value) }
@@ -191,7 +191,7 @@ module HaveAPI::ClientExamples
191
191
  body << "console.log(result.value);\n"
192
192
  body << "console.log(result.response.meta);\n" if thorough?
193
193
  if field(current_action, :blocking)
194
- body << "// Request success alone does not establish operation completion.\n"
194
+ body << "#{comment_lines(example_comment('js_request_completion', default: 'Request success alone does not establish operation completion.'), '// ')}\n"
195
195
  body << "const completed = await api.waitForCompletion(result, {timeoutMs: 300000});\nconsole.log(completed.state);\n"
196
196
  end
197
197
  else
@@ -209,7 +209,7 @@ module HaveAPI::ClientExamples
209
209
  options << " console.log(reply.meta());\n" if thorough?
210
210
  options << "}\n"
211
211
  body << "action.directInvoke(#{(args + ["{\n#{indent(options)}}"]).join(', ')});\n"
212
- body << "// This prints the initial response; use its action-state metadata to monitor completion.\n" if field(current_action, :blocking)
212
+ body << "#{comment_lines(example_comment('js_initial_response', default: 'This prints the initial response; use its action-state metadata to monitor completion.'), '// ')}\n" if field(current_action, :blocking)
213
213
  end
214
214
  authentication = action_authentication
215
215
  thorough? ? program(body, authentication:) : body
@@ -29,7 +29,7 @@ module HaveAPI::ClientExamples
29
29
  } catch (\\HaveAPI\\Client\\Exception\\Base $error) {
30
30
  echo $error->getMessage() . "\\n";
31
31
  } catch (\\Exception $error) {
32
- // Dependency failures still propagate to the application.
32
+ #{comment_lines(example_comment('dependency_failures', default: 'Dependency failures still propagate to the application.'), ' // ')}
33
33
  echo $error->getMessage() . "\\n";
34
34
  throw $error;
35
35
  }
@@ -60,51 +60,50 @@ module HaveAPI::ClientExamples
60
60
  <<~END
61
61
  #{initialization.chomp}
62
62
 
63
- // Get token using username and password
63
+ #{comment_lines(example_comment('token_credentials', default: 'Get token using username and password'), '// ')}
64
64
  $api->authenticate('token', #{token_options(desc)});
65
65
 
66
- // Next time, the client can authenticate using the token directly
66
+ #{comment_lines(example_comment('token_reuse_next', default: 'Next time, the client can authenticate using the token directly'), '// ')}
67
67
  $api->authenticate('token', ['token' => 'example-token']);
68
68
  END
69
69
 
70
70
  when :oauth2
71
71
  <<~END
72
- // OAuth2 requires session
72
+ #{comment_lines(example_comment('oauth_session', default: 'OAuth2 requires session'), '// ')}
73
73
  session_start();
74
74
 
75
- // Client instance
75
+ #{comment_lines(example_comment('client_instance', default: 'Client instance'), '// ')}
76
76
  #{initialization}
77
- // Check if we already have an access token
77
+ #{comment_lines(example_comment('oauth_check_token', default: 'Check if we already have an access token'), '// ')}
78
78
  if (isset($_SESSION['access_token'])) {
79
- // We're already authenticated, reuse the existing access token
79
+ #{comment_lines(example_comment('oauth_reuse_token', default: 'We\'re already authenticated, reuse the existing access token'), ' // ')}
80
80
  $api->authenticate('oauth2', ['access_token' => $_SESSION['access_token']]);
81
81
  } else {
82
- // Follow the OAuth2 authorization process to get an access token using
83
- // authorization code
82
+ #{comment_lines(example_comment('oauth_code_flow', default: "Follow the OAuth2 authorization process to get an access token using\nauthorization code"), ' // ')}
84
83
  $api->authenticate('oauth2', [
85
- // Client id and secret are given by the API server
84
+ #{comment_lines(example_comment('oauth_client_credentials', default: 'Client id and secret are given by the API server'), ' // ')}
86
85
  'client_id' => 'example-client',
87
86
  'client_secret' => 'example-client-secret',
88
87
 
89
- // This example code should run on the URL below
88
+ #{comment_lines(example_comment('oauth_callback_url', default: 'This example code should run on the URL below'), ' // ')}
90
89
  'redirect_uri' => 'https://your-client.tld/oauth2-callback',
91
90
 
92
- // Scopes are specific to the API implementation
91
+ #{comment_lines(example_comment('oauth_scopes', default: 'Scopes are specific to the API implementation'), ' // ')}
93
92
  'scope' => 'all',
94
93
  ]);
95
94
 
96
95
  $provider = $api->getAuthenticationProvider();
97
96
 
98
- // We don't have authorization code yet, request one
97
+ #{comment_lines(example_comment('oauth_need_code', default: 'We don\'t have authorization code yet, request one'), ' // ')}
99
98
  if (!isset($_GET['code'])) {
100
- // Redirect the user to the authorization endpoint
99
+ #{comment_lines(example_comment('oauth_redirect', default: 'Redirect the user to the authorization endpoint'), ' // ')}
101
100
  $provider->requestAuthorizationCode();
102
101
  exit;
103
102
  } else {
104
- // Request access token using the token endpoint
103
+ #{comment_lines(example_comment('oauth_request_token', default: 'Request access token using the token endpoint'), ' // ')}
105
104
  $provider->requestAccessToken();
106
105
 
107
- // Store the access token in the session
106
+ #{comment_lines(example_comment('oauth_store_token', default: 'Store the access token in the session'), ' // ')}
108
107
  $_SESSION['access_token'] = $provider->jsonSerialize();
109
108
  }
110
109
  }
@@ -151,7 +150,7 @@ module HaveAPI::ClientExamples
151
150
  return "$api->authenticate('basic', ['user' => 'user', 'password' => 'secret']);" if providers.include?('basic')
152
151
  return "$api->authenticate('oauth2', ['access_token' => ['access_token' => 'example-access-token']]);" if providers.include?('oauth2')
153
152
 
154
- raise HaveAPI::ClientExample::UnsupportedAuthentication, 'This action requires authentication unsupported by this client.'
153
+ raise HaveAPI::ClientExample::UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.action_authentication')
155
154
  end
156
155
 
157
156
  def example(sample)
@@ -171,7 +170,7 @@ module HaveAPI::ClientExamples
171
170
  out << if sample[:status]
172
171
  response(sample)
173
172
  else
174
- '// Throws exception \\HaveAPI\\Client\\Exception\\ActionFailed'
173
+ comment_lines(example_comment('throws_exception', default: 'Throws exception %{exception}', exception: '\\HaveAPI\\Client\\Exception\\ActionFailed'), '// ')
175
174
  end
176
175
  thorough? ? with_error_handling(["#{initialization}$api->setup();", credentials, out.chomp].reject(&:empty?).join("\n\n")) : out
177
176
  rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
@@ -183,16 +182,16 @@ module HaveAPI::ClientExamples
183
182
 
184
183
  case action[:output][:layout]
185
184
  when :hash
186
- out << "// $reply is an instance of \\HaveAPI\\Client\\Response\n"
187
- out << "// $reply->getResponse() returns an associative array of output parameters:\n"
185
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: '$reply', type: '\\HaveAPI\\Client\\Response'), '// ')}\n"
186
+ out << "#{comment_lines(example_comment('response_associative', default: '%{call} returns an associative array of output parameters:', call: '$reply->getResponse()'), '// ')}\n"
188
187
  out << format_parameters(:output, sample[:response] || {}, '// ')
189
188
 
190
189
  when :hash_list
191
- out << "// $reply is an instance of \\HaveAPI\\Client\\Response\n"
192
- out << "// $reply->getResponse() returns an array of associative arrays:\n"
190
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: '$reply', type: '\\HaveAPI\\Client\\Response'), '// ')}\n"
191
+ out << "#{comment_lines(example_comment('response_associative_arrays', default: '%{call} returns an array of associative arrays:', call: '$reply->getResponse()'), '// ')}\n"
193
192
 
194
193
  when :object
195
- out << "// $reply is an instance of \\HaveAPI\\Client\\ResourceInstance\n"
194
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: '$reply', type: '\\HaveAPI\\Client\\ResourceInstance'), '// ')}\n"
196
195
 
197
196
  (sample[:response] || {}).each do |pn, pv|
198
197
  param = action[:output][:parameters][pn]
@@ -210,7 +209,7 @@ module HaveAPI::ClientExamples
210
209
  out << ")\n"
211
210
 
212
211
  elsif param[:type] == 'Custom'
213
- out << "// $reply->#{pn} is a custom type"
212
+ out << comment_lines(example_comment('custom_type', default: '%{variable} is a custom type', variable: "$reply->#{pn}"), '// ')
214
213
 
215
214
  else
216
215
  out << "// $reply->#{pn} = #{PP.pp(pv, '')}"
@@ -218,7 +217,7 @@ module HaveAPI::ClientExamples
218
217
  end
219
218
 
220
219
  when :object_list
221
- out << '// $reply is an instance of \\HaveAPI\\Client\\ResourceInstanceList'
220
+ out << comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: '$reply', type: '\\HaveAPI\\Client\\ResourceInstanceList'), '// ')
222
221
  end
223
222
 
224
223
  out
@@ -70,20 +70,20 @@ module HaveAPI::ClientExamples
70
70
  end
71
71
 
72
72
  def init
73
- code_result { program(thorough? ? "print(\"Discovery complete\")\n" : "# Call API actions inside this block.\npass\n") }
73
+ code_result { program(thorough? ? "print(\"Discovery complete\")\n" : "#{comment_lines(example_comment('python_actions', default: 'Call API actions inside this block.'), '# ')}\npass\n") }
74
74
  end
75
75
 
76
76
  def auth(method, description)
77
- return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
77
+ return limitation(HaveAPI.t('haveapi.documentation.limitations.authentication_provider')) unless supported_auth?(method)
78
78
 
79
79
  code_result do
80
- body = thorough? ? "print(\"Authenticated\")\n" : "# Call API actions inside this block.\npass\n"
80
+ body = thorough? ? "print(\"Authenticated\")\n" : "#{comment_lines(example_comment('python_actions', default: 'Call API actions inside this block.'), '# ')}\npass\n"
81
81
  case method.to_sym
82
82
  when :basic
83
83
  program(body, imports: ['BasicAuth'], auth: 'BasicAuth("user", "secret")')
84
84
  when :oauth2
85
85
  source = program(body, imports: ['OAuth2Auth'], auth: 'OAuth2Auth("example-access-token")')
86
- "# Use an existing access token; OAuth acquisition and refresh are outside this client.\n#{source}"
86
+ "#{comment_lines(example_comment('python_existing_oauth', default: 'Use an existing access token; OAuth acquisition and refresh are outside this client.'), '# ')}\n#{source}"
87
87
  when :token
88
88
  if token_login_fields(description).empty?
89
89
  program(body, imports: ['TokenAuth'], auth: 'TokenAuth("example-token")')
@@ -112,13 +112,13 @@ module HaveAPI::ClientExamples
112
112
  path, request = structured_sample(sample)
113
113
  method = field(current_action, :method).to_s.upcase
114
114
  unless %w[GET POST PUT DELETE].include?(method)
115
- raise Unsupported, 'This Python client supports GET, POST, PUT and DELETE actions only.'
115
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.python_method')
116
116
  end
117
117
  unless request.nil? || request.is_a?(Hash) || request.is_a?(Array)
118
- raise Unsupported, 'This Python client requires object or list input.'
118
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.python_input')
119
119
  end
120
120
  if request.is_a?(Array) && field(field(current_action, :input), :namespace).to_s.empty?
121
- raise Unsupported, 'This Python client requires a namespace for list input.'
121
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.python_list_namespace')
122
122
  end
123
123
 
124
124
  check_get_sample(request) if method == 'GET'
@@ -134,7 +134,7 @@ module HaveAPI::ClientExamples
134
134
  body << "print(response.meta)\n" if thorough?
135
135
  if field(current_action, :blocking)
136
136
  body << <<~PY
137
- # A request timeout does not cancel a server operation.
137
+ #{comment_lines(example_comment('timeout_no_cancel', default: 'A request timeout does not cancel a server operation.'), '# ')}
138
138
  if response.operation is not None:
139
139
  completed = #{await_prefix}response.operation.wait(timeout=300, interval=1)
140
140
  print(completed)
@@ -142,7 +142,7 @@ module HaveAPI::ClientExamples
142
142
  end
143
143
  if field(current_action, :auth)
144
144
  method = %i[token basic oauth2].find { |name| supported_auth?(name) }
145
- raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
145
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.action_authentication') unless method
146
146
 
147
147
  provider, auth = {
148
148
  token: ['TokenAuth', 'TokenAuth("example-token")'],
@@ -161,7 +161,7 @@ module HaveAPI::ClientExamples
161
161
  end
162
162
 
163
163
  def action_fragment(body)
164
- asynchronous? ? "# Run inside an async function after initializing api.\n#{body}" : body
164
+ asynchronous? ? "#{comment_lines(example_comment('python_async_context', default: 'Run inside an async function after initializing api.'), '# ')}\n#{body}" : body
165
165
  end
166
166
 
167
167
  def check_get_sample(request)
@@ -180,7 +180,7 @@ module HaveAPI::ClientExamples
180
180
  end)
181
181
  return if safe_namespace && safe_request
182
182
 
183
- raise Unsupported, 'Python GET examples support flat scalar input with bracket-free names; Custom values must be strings or nullable nulls.'
183
+ raise Unsupported, HaveAPI.t('haveapi.documentation.limitations.python_get_input')
184
184
  end
185
185
  end
186
186
  end
@@ -23,30 +23,27 @@ module HaveAPI::ClientExamples
23
23
  case method
24
24
  when :basic
25
25
  <<~END
26
- # Provide credentials on command line
26
+ #{comment_lines(example_comment('cli_credentials', default: 'Provide credentials on command line'), '# ')}
27
27
  #{init} --auth basic --user user --password secret
28
28
 
29
- # If username or password isn't provided, the user is asked on stdin
29
+ #{comment_lines(example_comment('cli_ask_stdin', default: 'If username or password isn\'t provided, the user is asked on stdin'), '# ')}
30
30
  #{init} --auth basic --user user
31
31
  # Password: secret
32
32
  END
33
33
 
34
34
  when :token
35
35
  <<~END
36
- # Get token using username and password and save it to disk
37
- # Note that the client always has to call some action. APIs should provide
38
- # an action to get information about the current user, so that's what we're
39
- # calling now.
36
+ #{comment_lines(example_comment('cli_save_token', default: 'Get token using username and password and save it to disk'), '# ')}
37
+ #{comment_lines(example_comment('cli_action_required', default: "Note that the client always has to call some action. APIs should provide\nan action to get information about the current user, so that's what we're\ncalling now."), '# ')}
40
38
  #{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
39
  # Password: secret
42
40
 
43
- # Now the token is read from disk and the user does not have to provide username
44
- # nor password and be authenticated
41
+ #{comment_lines(example_comment('cli_read_token', default: "Now the token is read from disk and the user does not have to provide username\nnor password and be authenticated"), '# ')}
45
42
  #{init} user current
46
43
  END
47
44
 
48
45
  when :oauth2
49
- '# OAuth2 is not supported by HaveAPI Ruby CLI.'
46
+ comment_lines(example_comment('cli_oauth_unsupported', default: 'OAuth2 is not supported by HaveAPI Ruby CLI.'), '# ')
50
47
  end
51
48
  rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
52
49
  { status: :limitation, reason: e.message }.freeze
@@ -14,11 +14,19 @@ module HaveAPI::ClientExamples
14
14
  end
15
15
 
16
16
  def initialization
17
- "require 'haveapi/client'\n\napi = HaveAPI::Client.new(#{base_url.dump}, version: #{version.to_s.dump})\n"
17
+ "#{client_library}#{client_initialization}"
18
+ end
19
+
20
+ def client_library
21
+ "require 'haveapi/client'\n\n"
22
+ end
23
+
24
+ def client_initialization
25
+ "api = HaveAPI::Client.new(#{base_url.dump}, version: #{version.to_s.dump})\n"
18
26
  end
19
27
 
20
28
  def with_error_handling(body)
21
- <<~RUBY
29
+ client_library + <<~RUBY
22
30
  begin
23
31
  #{body.lines.map { |line| line == "\n" ? line : " #{line}" }.join.chomp}
24
32
  rescue HaveAPI::Client::ValidationError => error
@@ -35,7 +43,7 @@ module HaveAPI::ClientExamples
35
43
  end
36
44
 
37
45
  def init
38
- thorough? ? with_error_handling("#{initialization}api.setup\n") : initialization
46
+ thorough? ? with_error_handling("#{client_initialization}api.setup\n") : initialization
39
47
  end
40
48
 
41
49
  def auth_login_literal(name, parameter, value)
@@ -65,13 +73,13 @@ module HaveAPI::ClientExamples
65
73
  end
66
74
  RUBY
67
75
  end
68
- %(#{code.chomp}\n\n# Reuse an existing token\napi.authenticate(:token, token: 'example-token'))
76
+ %(#{code.chomp}\n\n#{comment_lines(example_comment('reuse_token', default: 'Reuse an existing token'), '# ')}\napi.authenticate(:token, token: 'example-token'))
69
77
  when :oauth2
70
- return '# OAuth2 is not supported by HaveAPI Ruby client.'
78
+ return comment_lines(example_comment('ruby_oauth_unsupported', default: 'OAuth2 is not supported by HaveAPI Ruby client.'), '# ')
71
79
  end
72
80
  return unless body
73
81
 
74
- source = [initialization.chomp, (thorough? ? 'api.setup' : nil), body].compact.join("\n\n")
82
+ source = [(thorough? ? client_initialization : initialization).chomp, (thorough? ? 'api.setup' : nil), body].compact.join("\n\n")
75
83
  thorough? ? with_error_handling(source) : source
76
84
  rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
77
85
  { status: :limitation, reason: e.message }.freeze
@@ -84,7 +92,7 @@ module HaveAPI::ClientExamples
84
92
  return "api.authenticate(:token, token: 'example-token')\n" if providers.include?('token')
85
93
  return "api.authenticate(:basic, user: 'user', password: 'secret')\n" if providers.include?('basic')
86
94
 
87
- raise HaveAPI::ClientExample::UnsupportedAuthentication, 'This action requires authentication unsupported by this client.'
95
+ raise HaveAPI::ClientExample::UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.action_authentication')
88
96
  end
89
97
 
90
98
  def example(sample)
@@ -104,9 +112,9 @@ module HaveAPI::ClientExamples
104
112
  out << if sample[:status]
105
113
  response(sample)
106
114
  else
107
- "\n# Raises exception HaveAPI::Client::ActionFailed\n"
115
+ "\n#{comment_lines(example_comment('raises_exception', default: 'Raises exception %{exception}', exception: 'HaveAPI::Client::ActionFailed'), '# ')}\n"
108
116
  end
109
- thorough? ? with_error_handling(["#{initialization}api.setup", credentials.chomp, out].reject(&:empty?).join("\n\n")) : out
117
+ thorough? ? with_error_handling(["#{client_initialization}api.setup", credentials.chomp, out].reject(&:empty?).join("\n\n")) : out
110
118
  rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
111
119
  { status: :limitation, reason: e.message }.freeze
112
120
  end
@@ -116,17 +124,17 @@ module HaveAPI::ClientExamples
116
124
 
117
125
  case action[:output][:layout]
118
126
  when :hash
119
- out << "# reply is an instance of HaveAPI::Client::Response\n"
120
- out << "# reply.response() returns a hash of output parameters:\n"
127
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: 'reply', type: 'HaveAPI::Client::Response'), '# ')}\n"
128
+ out << "#{comment_lines(example_comment('response_hash', default: '%{call} returns a hash of output parameters:', call: 'reply.response()'), '# ')}\n"
121
129
  out << PP.pp(sample[:response] || {}, '').split("\n").map { |v| "# #{v}" }.join("\n")
122
130
 
123
131
  when :hash_list
124
- out << "# reply is an instance of HaveAPI::Client::Response\n"
125
- out << "# reply.response() returns an array of hashes:\n"
132
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: 'reply', type: 'HaveAPI::Client::Response'), '# ')}\n"
133
+ out << "#{comment_lines(example_comment('response_hashes', default: '%{call} returns an array of hashes:', call: 'reply.response()'), '# ')}\n"
126
134
  out << PP.pp(sample[:response] || [], '').split("\n").map { |v| "# #{v}" }.join("\n")
127
135
 
128
136
  when :object
129
- out << "# reply is an instance of HaveAPI::Client::ResourceInstance\n"
137
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: 'reply', type: 'HaveAPI::Client::ResourceInstance'), '# ')}\n"
130
138
 
131
139
  (sample[:response] || {}).each do |pn, pv|
132
140
  param = action[:output][:parameters][pn]
@@ -149,8 +157,8 @@ module HaveAPI::ClientExamples
149
157
  end
150
158
 
151
159
  when :object_list
152
- out << "# reply is an instance of HaveAPI::Client::ResourceInstanceList,\n"
153
- out << '# which is a subclass of Array'
160
+ out << "#{comment_lines(example_comment('instance_of', default: '%{variable} is an instance of %{type}', variable: 'reply', type: 'HaveAPI::Client::ResourceInstanceList'), '# ')},\n"
161
+ out << comment_lines(example_comment('array_subclass', default: 'which is a subclass of Array'), '# ')
154
162
  end
155
163
 
156
164
  out
@@ -33,7 +33,7 @@ module HaveAPI::ClientExamples
33
33
 
34
34
  def render_rust(kind, rendering_context: context, **)
35
35
  unless self.class.available?(rendering_context)
36
- return { status: :limitation, reason: 'A Rust API client documentation profile is required.' }.freeze
36
+ return { status: :limitation, reason: HaveAPI.t('haveapi.documentation.limitations.rust_profile') }.freeze
37
37
  end
38
38
 
39
39
  rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: rust_mode, **)
@@ -35,7 +35,7 @@ module HaveAPI::ClientExamples
35
35
 
36
36
  def render_documentation(kind, rendering_context: context, **)
37
37
  unless self.class.available?(rendering_context)
38
- return { status: :limitation, reason: 'A TypeScript API client documentation profile is required.' }.freeze
38
+ return { status: :limitation, reason: HaveAPI.t('haveapi.documentation.limitations.typescript_profile') }.freeze
39
39
  end
40
40
 
41
41
  rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: :async, **)
@@ -1,15 +1,16 @@
1
1
  require 'haveapi/client_example'
2
+ require 'haveapi/i18n'
2
3
 
3
4
  module HaveAPI
4
5
  class ClientExample
5
6
  # A request-local snapshot: never freeze or retain the server's shared help.
6
7
  class Context
7
- FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile detail].freeze
8
+ FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile detail locale].freeze
8
9
  attr_reader(*FIELDS)
9
10
 
10
11
  def initialize(version:, host:, base_url:, version_url:, description:,
11
12
  resource_path: [], resource: nil, action_name: nil, action: nil,
12
- sample: nil, profile: nil, detail: :simple)
13
+ sample: nil, profile: nil, detail: :simple, locale: :en)
13
14
  @version = normalize(:version, version)
14
15
  @host = normalize(:host, host)
15
16
  @base_url = normalize(:base_url, base_url)
@@ -22,6 +23,7 @@ module HaveAPI
22
23
  @sample = normalize(:sample, sample)
23
24
  @profile = normalize(:profile, profile)
24
25
  @detail = normalize(:detail, detail)
26
+ @locale = normalize(:locale, locale)
25
27
  freeze
26
28
  end
27
29
 
@@ -44,6 +46,14 @@ module HaveAPI
44
46
  derived.freeze
45
47
  end
46
48
 
49
+ def documentation_message(key, default:, **values)
50
+ ::I18n.t("haveapi.documentation.#{key}", locale:, default:, **values)
51
+ end
52
+
53
+ def example_comment(key, default:, **values)
54
+ documentation_message("comments.#{key}", default:, **values)
55
+ end
56
+
47
57
  private
48
58
 
49
59
  def normalize(field, value)
@@ -54,6 +64,8 @@ module HaveAPI
54
64
  copy(value.map(&:to_s))
55
65
  when :action_name
56
66
  value.nil? ? nil : value.to_s.dup.freeze
67
+ when :locale
68
+ value.to_sym
57
69
  when :profile
58
70
  value
59
71
  when :detail