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
@@ -5,10 +5,17 @@ module HaveAPI::ClientExamples
5
5
  include DynamicHelpers
6
6
 
7
7
  label 'Python (sync)'
8
+ language 'python'
9
+ language_label 'Python'
10
+ variant 'Sync'
8
11
  id 'python-sync'
9
12
  code :python
10
13
  order 14
11
14
 
15
+ def self.detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
16
+ result[:status] == :limitation ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
17
+ end
18
+
12
19
  def asynchronous?
13
20
  false
14
21
  end
@@ -35,68 +42,66 @@ module HaveAPI::ClientExamples
35
42
  asynchronous? ? 'await ' : ''
36
43
  end
37
44
 
38
- def program(body, imports: [], auth: nil)
39
- package_imports = ([client_name, 'ActionError', 'HaveAPIError'] + imports).uniq.join(', ')
40
- source = "import os\nimport json\n"
41
- source << "import asyncio\n" if asynchronous?
42
- source << "\nfrom haveapi_client import #{package_imports}\n\n\n"
43
- source << "#{'async ' if asynchronous?}def main() -> None:\n try:\n"
45
+ def program(body, imports: [], stdlib: [], auth: nil)
46
+ package_imports = ([client_name] + (thorough? ? %w[ActionError HaveAPIError] : []) + imports).uniq.join(', ')
47
+ source = (stdlib + (asynchronous? ? ['asyncio'] : [])).uniq.map { |name| "import #{name}\n" }.join
48
+ source << "\n" unless source.empty?
49
+ source << "from haveapi_client import #{package_imports}#{asynchronous? ? "\n\n\n" : "\n\n"}"
50
+ source << "async def main() -> None:\n" if asynchronous?
51
+ level = asynchronous? ? 4 : 0
52
+ if thorough?
53
+ source << indent("try:\n", level)
54
+ level += 4
55
+ end
44
56
  options = ["version=#{python_literal(api_version)}"]
45
57
  options << "auth=#{auth}" if auth
46
- source << " #{'async ' if asynchronous?}with #{client_name}(#{python_literal(api_base)}, #{options.join(', ')}) as api:\n"
47
- source << indent(body, 12)
48
- source << indent(<<~PY, 4)
49
- except ActionError as error:
50
- print(error.message, error.errors)
51
- except HaveAPIError as error:
52
- print(error.message, error.status_code)
53
- PY
54
- source << "\n\nif __name__ == \"__main__\":\n #{asynchronous? ? 'asyncio.run(main())' : 'main()'}\n"
58
+ source << indent("#{'async ' if asynchronous?}with #{client_name}(#{python_literal(api_base)}, #{options.join(', ')}) as api:\n", level)
59
+ source << indent(body, level + 4)
60
+ if thorough?
61
+ source << indent(<<~PY, level - 4)
62
+ except ActionError as error:
63
+ print(error.message, error.errors)
64
+ except HaveAPIError as error:
65
+ print(error.message, error.status_code)
66
+ PY
67
+ end
68
+ source << "\n\nasyncio.run(main())\n" if asynchronous?
55
69
  source
56
70
  end
57
71
 
58
72
  def init
59
- code_result { program("print(\"Discovery complete\")\n") }
73
+ code_result { program(thorough? ? "print(\"Discovery complete\")\n" : "# Call API actions inside this block.\npass\n") }
60
74
  end
61
75
 
62
76
  def auth(method, description)
63
77
  return limitation('This authentication provider is not advertised or supported by this client.') unless supported_auth?(method)
64
78
 
65
79
  code_result do
80
+ body = thorough? ? "print(\"Authenticated\")\n" : "# Call API actions inside this block.\npass\n"
66
81
  case method.to_sym
67
82
  when :basic
68
- program("print(\"Authenticated\")\n", imports: ['BasicAuth'],
69
- auth: 'BasicAuth(os.environ["HAVEAPI_USER"], os.environ["HAVEAPI_PASSWORD"])')
83
+ program(body, imports: ['BasicAuth'], auth: 'BasicAuth("user", "secret")')
70
84
  when :oauth2
71
- source = program("print(\"Authenticated\")\n", imports: ['OAuth2Auth'],
72
- auth: 'OAuth2Auth(os.environ["HAVEAPI_ACCESS_TOKEN"])')
85
+ source = program(body, imports: ['OAuth2Auth'], auth: 'OAuth2Auth("example-access-token")')
73
86
  "# Use an existing access token; OAuth acquisition and refresh are outside this client.\n#{source}"
74
87
  when :token
75
- fields = token_login_fields(description).merge(token_request_fields(description).select do |_, parameter|
76
- field(parameter, :required)
77
- end)
78
- if fields.empty?
79
- program("print(\"Authenticated\")\n", imports: ['TokenAuth'], auth: 'TokenAuth(os.environ["HAVEAPI_TOKEN"])')
88
+ if token_login_fields(description).empty?
89
+ program(body, imports: ['TokenAuth'], auth: 'TokenAuth("example-token")')
80
90
  else
81
91
  challenge = asynchronous? ? 'AsyncAuthenticationChallenge' : 'AuthenticationChallenge'
82
- input = fields.map do |name, _|
83
- "#{python_literal(name.to_s)}: json.loads(os.environ[#{python_literal("HAVEAPI_AUTH_#{name}")}])"
84
- end.join(', ')
85
92
  body = <<~PY
86
- if "HAVEAPI_TOKEN" in os.environ:
87
- #{await_prefix}api.set_auth(TokenAuth(os.environ["HAVEAPI_TOKEN"]))
88
- else:
89
- # HAVEAPI_AUTH_* and HAVEAPI_CHALLENGE_* contain JSON values.
90
- step = #{await_prefix}api.begin_token_login({#{input}})
91
- while isinstance(step, #{challenge}):
92
- values = {
93
- name: json.loads(os.environ["HAVEAPI_CHALLENGE_" + name])
94
- for name in step.input if name != "token"
95
- }
96
- step = #{await_prefix}step.submit(values)
97
- print("Authenticated")
93
+ continuations = #{python_literal(auth_token_continuations(description))}
94
+ step = #{await_prefix}api.begin_token_login(#{python_literal(auth_token_credentials(description))})
95
+ while isinstance(step, #{challenge}):
96
+ if step.next_action not in continuations:
97
+ raise ValueError("Unknown authentication continuation")
98
+ values = continuations[step.next_action]
99
+ if any(name != "token" and name not in values for name in step.input):
100
+ raise ValueError("Unknown authentication input")
101
+ step = #{await_prefix}step.submit(values)
98
102
  PY
99
- program(body, imports: ['TokenAuth', challenge])
103
+ body << "print(\"Authenticated\")\n" if thorough?
104
+ program(body, imports: [challenge])
100
105
  end
101
106
  end
102
107
  end
@@ -125,7 +130,8 @@ module HaveAPI::ClientExamples
125
130
  arguments << "path_params=#{python_literal(path)}" unless path.empty?
126
131
  arguments << "params=#{python_literal(request)}" unless request.nil?
127
132
  body = "response = #{await_prefix}#{resource}.actions[#{python_literal(current_action_name)}].call(#{arguments.join(', ')})\n"
128
- body << "print(response.data)\nprint(response.meta)\n"
133
+ body << "print(response.data)\n"
134
+ body << "print(response.meta)\n" if thorough?
129
135
  if field(current_action, :blocking)
130
136
  body << <<~PY
131
137
  # A request timeout does not cancel a server operation.
@@ -139,17 +145,25 @@ module HaveAPI::ClientExamples
139
145
  raise Unsupported, 'This action requires an authentication provider unsupported by this client.' unless method
140
146
 
141
147
  provider, auth = {
142
- token: ['TokenAuth', 'TokenAuth(os.environ["HAVEAPI_TOKEN"])'],
143
- basic: ['BasicAuth', 'BasicAuth(os.environ["HAVEAPI_USER"], os.environ["HAVEAPI_PASSWORD"])'],
144
- oauth2: ['OAuth2Auth', 'OAuth2Auth(os.environ["HAVEAPI_ACCESS_TOKEN"])']
148
+ token: ['TokenAuth', 'TokenAuth("example-token")'],
149
+ basic: ['BasicAuth', 'BasicAuth("user", "secret")'],
150
+ oauth2: ['OAuth2Auth', 'OAuth2Auth("example-access-token")']
145
151
  }.fetch(method)
146
- program(body, imports: [provider], auth:)
152
+ if thorough?
153
+ program(body, imports: [provider], auth:)
154
+ else
155
+ action_fragment(body)
156
+ end
147
157
  else
148
- program(body)
158
+ thorough? ? program(body) : action_fragment(body)
149
159
  end
150
160
  end
151
161
  end
152
162
 
163
+ def action_fragment(body)
164
+ asynchronous? ? "# Run inside an async function after initializing api.\n#{body}" : body
165
+ end
166
+
153
167
  def check_get_sample(request)
154
168
  input = field(current_action, :input)
155
169
  namespace = field(input, :namespace)
@@ -1,3 +1,4 @@
1
+ require 'shellwords'
1
2
  module HaveAPI
2
3
  module CLI; end
3
4
  end
@@ -8,11 +9,14 @@ require 'haveapi/client_example'
8
9
  module HaveAPI::ClientExamples
9
10
  class RubyCli < HaveAPI::ClientExample
10
11
  label 'CLI'
12
+ language 'cli'
13
+ language_label 'CLI'
14
+ detail_levels :simple
11
15
  code :bash
12
16
  order 30
13
17
 
14
18
  def init
15
- "$ haveapi-cli -u #{base_url} --version #{version}"
19
+ "$ haveapi-cli -u #{Shellwords.escape(base_url.to_s)} --version #{Shellwords.escape(version.to_s)}"
16
20
  end
17
21
 
18
22
  def auth(method, desc)
@@ -24,7 +28,7 @@ module HaveAPI::ClientExamples
24
28
 
25
29
  # If username or password isn't provided, the user is asked on stdin
26
30
  #{init} --auth basic --user user
27
- Password: secret
31
+ # Password: secret
28
32
  END
29
33
 
30
34
  when :token
@@ -33,8 +37,8 @@ module HaveAPI::ClientExamples
33
37
  # Note that the client always has to call some action. APIs should provide
34
38
  # an action to get information about the current user, so that's what we're
35
39
  # calling now.
36
- #{init} --auth token #{auth_token_credentials(desc, password: false).map { |k, v| "--#{k} #{v}" }.join(' ')} --save user current
37
- Password: secret
40
+ #{init} --auth token #{auth_token_credentials(desc, password: false).map { |k, v| "#{Shellwords.escape("--#{k}")} #{Shellwords.escape(v.to_s)}" }.join(' ')} --save user current
41
+ # Password: secret
38
42
 
39
43
  # Now the token is read from disk and the user does not have to provide username
40
44
  # nor password and be authenticated
@@ -44,13 +48,15 @@ module HaveAPI::ClientExamples
44
48
  when :oauth2
45
49
  '# OAuth2 is not supported by HaveAPI Ruby CLI.'
46
50
  end
51
+ rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
52
+ { status: :limitation, reason: e.message }.freeze
47
53
  end
48
54
 
49
55
  def example(sample)
50
56
  cmd = [init]
51
- cmd << resource_path.join('.')
52
- cmd << action_name
53
- cmd.concat(sample[:path_params]) if sample[:path_params]
57
+ cmd << Shellwords.escape(resource_path.join('.'))
58
+ cmd << Shellwords.escape(action_name.to_s)
59
+ cmd.concat(sample[:path_params].map { |value| Shellwords.escape(value.to_s) }) if sample[:path_params]
54
60
 
55
61
  if sample[:request] && !sample[:request].empty?
56
62
  cmd << "-- \\\n"
@@ -65,16 +71,16 @@ module HaveAPI::ClientExamples
65
71
 
66
72
  return response(sample, res) if sample[:status]
67
73
 
68
- res << "\nAction failed: #{sample[:message]}\n"
74
+ display = "Action failed: #{sample[:message]}\n"
69
75
 
70
76
  if sample[:errors] && sample[:errors].any?
71
- res << "Errors:\n"
77
+ display << "Errors:\n"
72
78
  sample[:errors].each do |param, e|
73
- res << "\t#{param}: #{e.join('; ')}\n"
79
+ display << "\t#{param}: #{e.join('; ')}\n"
74
80
  end
75
81
  end
76
82
 
77
- res
83
+ res << "\n" << comment_response(display)
78
84
  end
79
85
 
80
86
  def response(sample, res)
@@ -101,21 +107,24 @@ module HaveAPI::ClientExamples
101
107
  cols << col
102
108
  end
103
109
 
104
- res << "\n" << HaveAPI::CLI::OutputFormatter.to_s(
105
- sample[:response],
106
- cols
107
- )
108
- res
110
+ display = HaveAPI::CLI::OutputFormatter.to_s(sample[:response], cols)
111
+ res << "\n" << comment_response(display)
109
112
  end
110
113
 
111
114
  def input_param(name, value)
112
115
  option = name.to_s.gsub('_', '-')
113
116
 
114
117
  if action[:input][:parameters][name][:type] == 'Boolean'
115
- return value ? "--#{option}" : "--no-#{name}"
118
+ return Shellwords.escape(value ? "--#{option}" : "--no-#{name}")
116
119
  end
117
120
 
118
- "--#{option} '#{value}'"
121
+ "#{Shellwords.escape("--#{option}")} #{Shellwords.escape(value.to_s)}"
122
+ end
123
+
124
+ private
125
+
126
+ def comment_response(fragment)
127
+ fragment.gsub(/\r\n?/, "\n").lines.map { |line| "# #{line.delete_suffix("\n")}\n" }.join
119
128
  end
120
129
  end
121
130
  end
@@ -4,45 +4,91 @@ require 'haveapi/client_example'
4
4
  module HaveAPI::ClientExamples
5
5
  class RubyClient < HaveAPI::ClientExample
6
6
  label 'Ruby'
7
+ language 'ruby'
8
+ language_label 'Ruby'
7
9
  code :ruby
8
10
  order 0
9
11
 
10
- def init
11
- <<~END
12
- require 'haveapi-client'
12
+ def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
13
+ result[:status] == :limitation || (kind == :auth && auth_method == :oauth2) ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
14
+ end
13
15
 
14
- client = HaveAPI::Client.new("#{base_url}", version: "#{version}")
15
- END
16
+ def initialization
17
+ "require 'haveapi/client'\n\napi = HaveAPI::Client.new(#{base_url.dump}, version: #{version.to_s.dump})\n"
16
18
  end
17
19
 
18
- def auth(method, desc)
19
- case method
20
- when :basic
21
- <<~END
22
- #{init}
20
+ def with_error_handling(body)
21
+ <<~RUBY
22
+ begin
23
+ #{body.lines.map { |line| line == "\n" ? line : " #{line}" }.join.chomp}
24
+ rescue HaveAPI::Client::ValidationError => error
25
+ warn error.message
26
+ warn error.errors.inspect
27
+ rescue HaveAPI::Client::ActionFailed => error
28
+ warn error.message
29
+ warn error.response.errors.inspect if error.response.is_a?(HaveAPI::Client::Response)
30
+ rescue HaveAPI::Client::Authentication::AuthenticationFailed, HaveAPI::Client::ProtocolError,
31
+ RestClient::Exception, JSON::ParserError => error
32
+ warn error.message
33
+ end
34
+ RUBY
35
+ end
23
36
 
24
- client.authenticate(:basic, user: "user", password: "secret")
25
- END
37
+ def init
38
+ thorough? ? with_error_handling("#{initialization}api.setup\n") : initialization
39
+ end
26
40
 
27
- when :token
28
- <<~END
29
- #{init}
41
+ def auth_login_literal(name, parameter, value)
42
+ return value unless value == false || value.nil?
30
43
 
31
- # Get token using username and password
32
- client.authenticate(:token, #{auth_token_credentials(desc).map { |k, v| "#{k}: \"#{v}\"" }.join(', ')})
44
+ # Token#request_token copies only truthy login options.
45
+ auth_literal(name, parameter, excluding: [false, nil])
46
+ end
33
47
 
34
- puts "Token = \#{client.auth.token}"
48
+ def auth(method, desc)
49
+ body = case method
50
+ when :basic
51
+ "api.authenticate(:basic, user: 'user', password: 'secret')"
52
+ when :token
53
+ input = auth_token_credentials(desc).transform_keys(&:to_sym)
54
+ continuations = auth_token_continuations(desc)
55
+ code = "api.authenticate(:token, **#{input.inspect})"
56
+ unless continuations.empty?
57
+ code << <<~RUBY
58
+ do |action, parameters|
59
+ continuations = #{continuations.inspect}
60
+ input = continuations.fetch(action.to_s) { raise ArgumentError, 'Unknown authentication continuation' }
61
+ unless (parameters.keys.map(&:to_s) - input.keys - ['token']).empty?
62
+ raise ArgumentError, 'Unknown authentication input'
63
+ end
64
+ input.transform_keys(&:to_sym)
65
+ end
66
+ RUBY
67
+ end
68
+ %(#{code.chomp}\n\n# Reuse an existing token\napi.authenticate(:token, token: 'example-token'))
69
+ when :oauth2
70
+ return '# OAuth2 is not supported by HaveAPI Ruby client.'
71
+ end
72
+ return unless body
73
+
74
+ source = [initialization.chomp, (thorough? ? 'api.setup' : nil), body].compact.join("\n\n")
75
+ thorough? ? with_error_handling(source) : source
76
+ rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
77
+ { status: :limitation, reason: e.message }.freeze
78
+ end
35
79
 
36
- # Next time, the client can authenticate using the token directly
37
- client.authenticate(:token, token: saved_token)
38
- END
80
+ def action_authentication
81
+ return '' unless action[:auth] && context
39
82
 
40
- when :oauth2
41
- '# OAuth2 is not supported by HaveAPI Ruby client.'
42
- end
83
+ providers = auth_field(context.description, :authentication, {}).keys.map(&:to_s)
84
+ return "api.authenticate(:token, token: 'example-token')\n" if providers.include?('token')
85
+ return "api.authenticate(:basic, user: 'user', password: 'secret')\n" if providers.include?('basic')
86
+
87
+ raise HaveAPI::ClientExample::UnsupportedAuthentication, 'This action requires authentication unsupported by this client.'
43
88
  end
44
89
 
45
90
  def example(sample)
91
+ credentials = action_authentication
46
92
  args = []
47
93
 
48
94
  args.concat(sample[:path_params]) if sample[:path_params]
@@ -51,15 +97,18 @@ module HaveAPI::ClientExamples
51
97
  args << PP.pp(sample[:request], '').strip
52
98
  end
53
99
 
54
- out = "#{init}\n"
55
- out << "reply = client.#{resource_path.join('.')}.#{action_name}"
100
+ out = +''
101
+ out << "reply = api.#{resource_path.join('.')}.#{action_name}"
56
102
  out << "(#{args.join(', ')})" unless args.empty?
57
103
 
58
- return out << response(sample) if sample[:status]
59
-
60
- out << "\n"
61
- out << '# Raises exception HaveAPI::Client::ActionFailed'
62
- out
104
+ out << if sample[:status]
105
+ response(sample)
106
+ else
107
+ "\n# Raises exception HaveAPI::Client::ActionFailed\n"
108
+ end
109
+ thorough? ? with_error_handling(["#{initialization}api.setup", credentials.chomp, out].reject(&:empty?).join("\n\n")) : out
110
+ rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
111
+ { status: :limitation, reason: e.message }.freeze
63
112
  end
64
113
 
65
114
  def response(sample)
@@ -6,6 +6,11 @@ module HaveAPI::ClientExamples
6
6
  def available?(context)
7
7
  context&.profile&.language == :rust
8
8
  end
9
+
10
+ def detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
11
+ adapter_detail_levels_for(adapter: context.profile.adapter, kind:, context:, result:,
12
+ mode: rust_mode, auth_method:, auth_description:)
13
+ end
9
14
  end
10
15
 
11
16
  def init
@@ -22,9 +27,13 @@ module HaveAPI::ClientExamples
22
27
 
23
28
  private
24
29
 
30
+ def rust_mode
31
+ self.class.rust_mode
32
+ end
33
+
25
34
  def render_rust(kind, rendering_context: context, **)
26
35
  unless self.class.available?(rendering_context)
27
- return { status: :limitation, reason: 'A Rust SDK documentation profile is required.' }.freeze
36
+ return { status: :limitation, reason: 'A Rust API client documentation profile is required.' }.freeze
28
37
  end
29
38
 
30
39
  rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: rust_mode, **)
@@ -33,26 +42,28 @@ module HaveAPI::ClientExamples
33
42
 
34
43
  class RustAsyncClient < RustClient
35
44
  id 'rust-async'
36
- label 'Rust (async)'
45
+ label 'Rust API client (async)'
46
+ language 'rust'
47
+ language_label 'Rust'
48
+ variant 'Async'
37
49
  code :rust
38
50
  order 17
39
51
 
40
- private
41
-
42
- def rust_mode
52
+ def self.rust_mode
43
53
  :async
44
54
  end
45
55
  end
46
56
 
47
57
  class RustBlockingClient < RustClient
48
58
  id 'rust-blocking'
49
- label 'Rust (blocking)'
59
+ label 'Rust API client (blocking)'
60
+ language 'rust'
61
+ language_label 'Rust'
62
+ variant 'Blocking'
50
63
  code :rust
51
64
  order 18
52
65
 
53
- private
54
-
55
- def rust_mode
66
+ def self.rust_mode
56
67
  :blocking
57
68
  end
58
69
  end
@@ -3,10 +3,17 @@ require 'haveapi/client_examples/ts_client'
3
3
  module HaveAPI::ClientExamples
4
4
  class TsAsyncClient < TsClient
5
5
  label 'TypeScript (async)'
6
+ language 'typescript'
7
+ language_label 'TypeScript'
8
+ variant 'Async'
6
9
  id 'typescript-async'
7
10
  code :typescript
8
11
  order 13
9
12
 
13
+ def self.detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
14
+ result[:status] == :limitation ? SIMPLE_DETAIL_LEVELS : DEFAULT_DETAIL_LEVELS
15
+ end
16
+
10
17
  def asynchronous?
11
18
  true
12
19
  end
@@ -3,6 +3,9 @@ require 'haveapi/client_examples/js_client'
3
3
  module HaveAPI::ClientExamples
4
4
  class TsClient < JsClient
5
5
  label 'TypeScript (callback)'
6
+ language 'typescript'
7
+ language_label 'TypeScript'
8
+ variant 'Callback'
6
9
  id 'typescript-callback'
7
10
  code :typescript
8
11
  order 12
@@ -3,7 +3,10 @@ require 'haveapi/client_example'
3
3
  module HaveAPI::ClientExamples
4
4
  class TsGeneratedClient < HaveAPI::ClientExample
5
5
  id 'typescript-generated'
6
- label 'TypeScript (generated)'
6
+ label 'TypeScript API client'
7
+ language 'typescript'
8
+ language_label 'TypeScript'
9
+ variant 'API client'
7
10
  code :typescript
8
11
  order 19
9
12
 
@@ -11,6 +14,11 @@ module HaveAPI::ClientExamples
11
14
  context&.profile&.language == :typescript
12
15
  end
13
16
 
17
+ def self.detail_levels_for(kind, context:, result:, auth_method: nil, auth_description: nil)
18
+ adapter_detail_levels_for(adapter: context.profile.adapter, kind:, context:, result:,
19
+ mode: :async, auth_method:, auth_description:)
20
+ end
21
+
14
22
  def init
15
23
  render_documentation(:init)
16
24
  end
@@ -27,7 +35,7 @@ module HaveAPI::ClientExamples
27
35
 
28
36
  def render_documentation(kind, rendering_context: context, **)
29
37
  unless self.class.available?(rendering_context)
30
- return { status: :limitation, reason: 'A generated TypeScript SDK documentation profile is required.' }.freeze
38
+ return { status: :limitation, reason: 'A TypeScript API client documentation profile is required.' }.freeze
31
39
  end
32
40
 
33
41
  rendering_context.profile.adapter.render(kind:, context: rendering_context, mode: :async, **)
@@ -4,12 +4,12 @@ module HaveAPI
4
4
  class ClientExample
5
5
  # A request-local snapshot: never freeze or retain the server's shared help.
6
6
  class Context
7
- FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile].freeze
7
+ FIELDS = %i[version host base_url version_url description resource_path resource action_name action sample profile detail].freeze
8
8
  attr_reader(*FIELDS)
9
9
 
10
10
  def initialize(version:, host:, base_url:, version_url:, description:,
11
11
  resource_path: [], resource: nil, action_name: nil, action: nil,
12
- sample: nil, profile: nil)
12
+ sample: nil, profile: nil, detail: :simple)
13
13
  @version = normalize(:version, version)
14
14
  @host = normalize(:host, host)
15
15
  @base_url = normalize(:base_url, base_url)
@@ -21,6 +21,7 @@ module HaveAPI
21
21
  @action = normalize(:action, action)
22
22
  @sample = normalize(:sample, sample)
23
23
  @profile = normalize(:profile, profile)
24
+ @detail = normalize(:detail, detail)
24
25
  freeze
25
26
  end
26
27
 
@@ -54,6 +55,10 @@ module HaveAPI
54
55
  when :action_name
55
56
  value.nil? ? nil : value.to_s.dup.freeze
56
57
  when :profile
58
+ value
59
+ when :detail
60
+ raise ArgumentError, 'detail must be :simple or :thorough' unless %i[simple thorough].include?(value)
61
+
57
62
  value
58
63
  else
59
64
  copy(value)