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
@@ -0,0 +1,192 @@
1
+ # Browsing API documentation
2
+
3
+ Open a version endpoint such as `/v7.0/` for the overview and client
4
+ initialization examples. The overview lists the available client libraries and
5
+ configured API clients. Choose **Authentication** for sign-in examples or
6
+ **Resources** for the full resource directory. Nested resources have their own pages, with
7
+ breadcrumbs linking to their parents. The sidebar identifies the current
8
+ resource by its full dotted path, such as `vps.snapshot`, above its action links.
9
+
10
+ The brand identifies the API, with its version on a separate line. The API root
11
+ lists available versions. HaveAPI internals opens the framework README; its
12
+ sidebar links to the protocol and client-development references.
13
+
14
+ Resource URLs use `?resource=vps` or `?resource=vps.snapshot`; the directory uses
15
+ `?page=resources` and authentication uses `?page=authentication`. These selectors
16
+ apply only to HTML documentation, leaving API actions and OPTIONS descriptions
17
+ unchanged. A version URL without the final slash redirects to its canonical
18
+ slash URL and retains the query. Mounted APIs retain
19
+ their prefix, for example `/api/v7.0/?resource=vps`. The unselected API root
20
+ continues to list versions; a selector there uses the default version.
21
+ Malformed or ambiguous selectors return 400. Resources absent from the current
22
+ user's description return 404. Navigation, examples and legacy anchor mappings
23
+ use the same filtered description. Ordinary documentation pages use
24
+ `private, no-store`; compact search data has a separate private-cache policy
25
+ described below.
26
+
27
+ Each resource page shows that resource's own actions and links to its children.
28
+ The sidebar lists its direct subresources. While scrolling, the current action
29
+ expands to show its Input parameters, Output parameters and Examples links.
30
+ Authentication methods provide links to their sections in the same way.
31
+ Without JavaScript, all of these section links remain visible. Reference
32
+ headings have permalink icons for linking to a particular section.
33
+ Action links retain their existing `root-...` fragments. Old overview links
34
+ with resource, action or example fragments open the corresponding resource page
35
+ when JavaScript is enabled. Ordinary resource links work without JavaScript;
36
+ the resource directory remains usable without JavaScript.
37
+
38
+ ## Search and visibility
39
+
40
+ Search is available in the sidebar on every documentation page. Press `/` to
41
+ focus it; on small screens this opens the navigation. The shortcut leaves normal
42
+ typing in editable fields alone. Search finds resource names, action
43
+ names, aliases and paths, and input or output parameter names, labels and types.
44
+ Versioned pages search their selected API version. The versions index and
45
+ HaveAPI internals search the default version, identified in the search label.
46
+ Search does not include protocol text, examples, defaults or validator values.
47
+
48
+ Results open the resource, action or parameter row. Queries use the normal
49
+ resource-directory page, such as `?page=resources&q=hostname`, and work without
50
+ JavaScript. With JavaScript enabled, using the search field loads a compact
51
+ list of searchable metadata. Further typing searches that list locally.
52
+ Resources appear before actions, followed by parameters. Resource results show
53
+ the full path, including parent resources. The list shows at most
54
+ 20 matches and asks the reader to refine queries with more results. Submitting
55
+ the form opens the results page.
56
+
57
+ The first search may take time to load. The browser can reuse its private cached
58
+ copy when navigating between freshly rendered pages with the same visible API
59
+ metadata. Search follows the documentation snapshot shown on the page; an open
60
+ page can retain that snapshot until reloaded. Opening a result checks access
61
+ again. The versions index, internal pages and pages restored from browser history
62
+ load a fresh snapshot when search is used. The search enhancement does not save
63
+ queries or results in preferences or JavaScript storage.
64
+
65
+ Anonymous documentation shows all declared resources, actions and parameters.
66
+ Signed-in documentation and search results show only those accessible to the
67
+ current account. Open **Documentation settings** beside the API name for theme
68
+ choices and the available OAuth sign-in or account controls. Signed-in readers
69
+ see their username and **Sign out**. Supported OAuth clients also offer
70
+ **Switch user**, which ends the documentation session
71
+ and starts a fresh sign-in. If that next sign-in is cancelled, the old session
72
+ remains signed out. The note below the controls states which view is shown.
73
+ Each API request still checks authorization
74
+ for its particular operation; a documentation entry does not grant permission
75
+ to execute it.
76
+
77
+ Parameter Details contain a Validation rules table with one row per advertised
78
+ rule. Allowed values appear in code beside their display labels. String choices
79
+ have no added quotation marks; other values retain their types.
80
+ Validation messages and extra options remain available; unknown rules retain
81
+ their supplied details. The Required column, defaults and API validation
82
+ behavior are unchanged. On narrow screens, parameter tables and long code blocks
83
+ scroll within their own regions.
84
+
85
+ Version-specific navigation puts Authentication before Resources and indents
86
+ authentication methods beneath it. Technical references are linked under
87
+ HaveAPI internals in the versions-index sidebar, and retain their direct URLs.
88
+
89
+ ## Themes and examples
90
+
91
+ The sidebar controls theme, language, client variant and example detail for
92
+ all examples on the page. Open **Documentation settings** and use the **Theme**
93
+ controls to select **Automatic**, **Light** or **Dark**. Automatic follows the
94
+ operating system. The versions index and shared internal pages use the same
95
+ layout and theme choices.
96
+
97
+ **Simple** examples focus on the operation and assume an initialized `api`
98
+ client, authenticated when required. **With error handling** demonstrates
99
+ additional error checks or diagnostics. Initialization and authentication
100
+ examples remain on their respective pages.
101
+
102
+ Choose a language first. The Client selector appears when that language has
103
+ multiple variants, such as Callback and Async. Examples stays visible and shows
104
+ the choices supported by the selected client on the current page. A selector
105
+ with only one choice is disabled.
106
+
107
+ A client can offer only Simple on an initialization page and both levels on an
108
+ action page. Each example uses With error handling only when that group supports
109
+ it; other groups remain Simple. A singleton choice preserves the saved detail
110
+ preference for pages and clients that support both levels. Replace the literal
111
+ credentials in authentication examples with the values for your application.
112
+
113
+ Use the copy icon inside a code block's top-right corner to copy its displayed code.
114
+ If the browser cannot copy it, select the code and copy it manually. Copying an
115
+ example does not change the selected language, detail or scroll position.
116
+
117
+ The browser stores these choices in `haveapi.docs.preferences.v1`. Preference
118
+ data contains no credentials or API responses. Choices are checked against the
119
+ clients available in the current API; an unavailable saved client uses an
120
+ available fallback.
121
+ Storage failures leave the controls usable for the current page. Without
122
+ JavaScript, native disclosure panels keep the examples readable.
123
+
124
+ ## Extending documentation
125
+
126
+ Client renderers declare `language`, `language_label` and optional `variant`
127
+ metadata alongside their stable client `id`. Unspecified presentation metadata
128
+ falls back to the renderer's ID and label. Only available renderers appear;
129
+ Go, Rust and TypeScript API clients require their configured profiles. See
130
+ [client examples](client-examples.md) for renderer and profile configuration.
131
+
132
+ Set the server's optional `documentation_name` before mounting to use a specific
133
+ API name or canonical domain in the brand and page titles. Without it, the
134
+ reference displays the request host and any nondefault port. The name is display
135
+ text; it does not change action URLs, OAuth endpoints or redirects.
136
+
137
+ Renderers can declare `detail_levels :simple` or
138
+ `detail_levels :simple, :thorough`. Declarations are inherited; undeclared custom
139
+ renderers keep both levels for compatibility. Declare a single level when the
140
+ renderer has no separate error-handling format. A renderer can refine its choices
141
+ for each group with `detail_levels_for(kind, context:, result:,
142
+ auth_method: nil, auth_description: nil)`, using the already-rendered Simple
143
+ result. The base query preserves the inherited declaration, including for
144
+ custom limitation results. Return only a frozen `[:simple]` or
145
+ `[:simple, :thorough]`. This controls presentation and does not change direct
146
+ renderer calls or Context detail values. See the
147
+ [renderer extension contract](client-examples.md#renderer-extensions) for the
148
+ built-in policies and adapter interface.
149
+
150
+ HTML selection traverses description keys rather than Ruby constants or file
151
+ paths. Keep UI navigation separate from any complete private schema used by a
152
+ client generator for name resolution. New content should retain stable action
153
+ and example IDs, escaped source and the shared current-user description.
154
+
155
+ Search metadata comes from that request's authorized description. Versioned
156
+ HTML derives one compact projection and a content key; it includes only the key
157
+ and index URL in the page. The index contains names, labels, types, paths and
158
+ parent references, with no examples, defaults, validator values or account
159
+ identity. Ruby result pages and browser suggestions match the same projection.
160
+
161
+ The keyed index representation uses `private, max-age=86400, immutable` and
162
+ varies by credentials and the configured locale header. A network request still
163
+ authenticates, describes the current view and checks the key before returning
164
+ data or a conditional success. Changed views return 409 without index data.
165
+ Bootstrap responses, errors and responses changing cookies remain no-store.
166
+ There is no server snapshot cache, background permission polling or persistent
167
+ JavaScript index. Logout does not erase private HTTP-cache files; a fresh page
168
+ can reuse an entry only when its authorized metadata has the same content key.
169
+
170
+ ## Browser sign-in
171
+
172
+ APIs can optionally configure an OAuth documentation client for browser sign-in.
173
+ The server keeps its client secret and uses a secure encrypted cookie for the
174
+ documentation session. This session applies only to HTML documentation; API
175
+ actions still use their ordinary credentials. See the
176
+ [OAuth setup guide](documentation-oauth.md) for configuration and HTTPS
177
+ requirements. APIs without that configuration show anonymous documentation and
178
+ no sign-in control. HTTP Basic, bearer headers and query credentials do not sign
179
+ readers into the documentation. Their API authentication examples remain available.
180
+
181
+ For a single-version API, the versions index uses that version's
182
+ documentation authentication and shows its actual account controls. With
183
+ multiple versions, the index hides account controls rather than assuming their
184
+ authentication is compatible. Internal articles remain neutral and may link to
185
+ the single version's authentication page without claiming a signed-in identity.
186
+
187
+ Documentation identity comes only from its valid OAuth session cookie. Cached
188
+ Basic credentials for another user do not change that identity or its controls;
189
+ an absent or invalid cookie leaves the reader anonymous. Ordinary API actions
190
+ and OPTIONS descriptions continue to use their existing authentication. See the
191
+ OAuth setup guide for enabling account selection on a dedicated documentation
192
+ client.
data/doc/index.md CHANGED
@@ -1,7 +1,4 @@
1
- HaveAPI documentation
2
- =====================
1
+ # HaveAPI internals
3
2
 
4
- - [README](doc/readme)
5
- - [Protocol definition](doc/protocol.md)
6
- - [JSON schema of the documentation protocol](doc/json-schema)
7
- - [How to create a client](doc/create-client.md)
3
+ These pages describe the HaveAPI protocol and how to develop clients.
4
+ Choose a reference from the sidebar.
data/doc/performance.md CHANGED
@@ -44,16 +44,22 @@ outside measurement. Three batches of eight renders check allocations, and a
44
44
  separate Ruby-call count checks traversal growth; each allows 32 additional
45
45
  objects or calls per render. Output must remain identical and inputs unchanged.
46
46
 
47
- Complete Rack HTML requests enable all three generated profiles. Checks compare
48
- every registered initialization/authentication/action panel with real renderer
49
- output, including successful and failed samples, and exercise admin/member/admin
50
- isolation through the same adapters. Allocation measurements use pages with
51
- 2, 4 and 8 resources, each with two actions and three admin-visible samples per
52
- action, plus one admin-only action. Member pages hide the admin sample and action.
53
- The added-resource allocation increment may grow by at most 2.5 times when the
54
- resource increment doubles. This permits linear panel/output growth while
55
- rejecting the measured whole-description-per-example copy regression. The
56
- fixture uses scalar fields so Go action panels exercise supported code paths.
47
+ Rack HTML checks enable all three generated profiles and request the overview,
48
+ authentication page, resource directory and every resource page. Each panel is
49
+ compared with the real renderer output at both detail levels, including successful and failed
50
+ samples. Admin/member/admin requests verify isolation through the same adapters;
51
+ a selected resource page must not render another resource's action examples.
52
+ The fixture uses scalar fields so Go action panels exercise supported code paths.
53
+
54
+ Allocation measurements cover two dimensions at the unchanged 2.5 increment
55
+ ratio. With 2, 4 and 8 resources, a fixed request set (overview, authentication,
56
+ directory and resource0) measures growth from the authorized description and
57
+ navigation.
58
+ Separately, one selected resource with 2, 4 and 8 ordinary actions measures
59
+ panel growth and detects repeated whole-resource or whole-description copies.
60
+ Both dimensions retain the existing samples and admin-only action. Measured
61
+ page bytes are recorded separately from complete-site correctness bytes; summing
62
+ all resource pages would also count their repeated page structure.
57
63
 
58
64
  The log retains the named registry, kinds, allocations, Ruby-call counts and
59
65
  page byte counts. These deterministic checks measure the selected candidate.
@@ -151,8 +157,25 @@ Rust panels, authorization boundaries and operation counts. They use relative
151
157
  allocation and deterministic checks without inventing fixed-reference allocation
152
158
  ceilings for the new shapes. Existing calibrated workload budgets remain intact.
153
159
 
154
- These shapes exercise complete pages with several megabytes of HTML, so a
155
- single request provides substantial measurement work. The weekly shape doubles
160
+ The HTML workloads retain one GET to the version overview for each operation,
161
+ with unchanged fixture dimensions, counters and budgets. The validator accepts
162
+ the legacy complete-page layout or the current overview as explicit, separate
163
+ shapes. It checks the overview's resource-directory link, complete authorized
164
+ resource/action anchor map and real initialization panels, and rejects action
165
+ bodies or private sample values there. The current overview intentionally omits resource examples;
166
+ its smaller response does not establish that resource-page rendering is faster.
167
+ The separate all-client checks cover the directory inventory, resource pages and
168
+ both detail levels.
169
+
170
+ Documentation HTML on sources supporting the optional documentation OAuth client
171
+ uses a real encrypted session cookie. Historical sources retain their Basic
172
+ authentication setup; API, OPTIONS and direct renderer lanes keep their original
173
+ credentials. Cookie issuance happens during fixture setup, with stable per-role
174
+ CSRF. Every HTML request still decrypts the cookie and validates its token through
175
+ the OAuth provider. Samples record the selected identity mode and role.
176
+
177
+ Historical complete-page shapes can contain several megabytes of HTML, so a
178
+ single request can provide substantial measurement work. The weekly shape doubles
156
179
  the resource count. Actual batch durations and output sizes belong in each
157
180
  run's evidence; these synthetic shapes do not define production latency targets.
158
181
 
@@ -243,6 +266,10 @@ fails during activation. Historical
243
266
  quick allocation calibration and all existing reference/budget bytes are retained
244
267
  unchanged, without claiming that the original quick calibration loaded Rust.
245
268
 
269
+ The benchmark worker activates io-console 0.9.4 before loading source or fixture
270
+ code, so its dependency graph does not depend on which examples a page renders.
271
+ Missing or conflicting versions fail before requests begin.
272
+
246
273
  The root, Ruby server and Rust generator development Gemfiles declare JSON
247
274
  2.21.2 so fresh setup installs the recorded native dependency. Comparisons
248
275
  accept only that exact declaration change in these three files. They retain
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.31.1'
18
+ s.add_dependency 'haveapi-client', '~> 0.32.0'
19
19
  s.add_dependency 'i18n', '>= 1.6', '< 2'
20
20
  s.add_dependency 'json'
21
21
  s.add_dependency 'mail'
@@ -48,6 +48,20 @@ module HaveAPI::Authentication
48
48
  nil
49
49
  end
50
50
 
51
+ # Validate an explicit documentation cookie token without API credentials.
52
+ def authenticate_oauth2_token(version, request, token)
53
+ catch(:return) do
54
+ Array(@instances[version]).each do |provider|
55
+ next unless provider.name == :oauth2 && provider.respond_to?(:authenticate_access_token)
56
+
57
+ user = provider.authenticate_access_token(request, token)
58
+ return user if user
59
+ end
60
+ end
61
+
62
+ nil
63
+ end
64
+
51
65
  def describe(context)
52
66
  ret = {}
53
67
 
@@ -71,6 +85,11 @@ module HaveAPI::Authentication
71
85
  ret
72
86
  end
73
87
 
88
+ # Metadata only: no resource traversal or authentication is performed.
89
+ def provider_descriptions(version)
90
+ Array(@instances[version]).to_h { |provider| [provider.name, provider.describe] }
91
+ end
92
+
74
93
  # Return provider list for version `v`.
75
94
  # Used for registering providers to specific version, e.g.
76
95
  # api.auth_chain[1] << MyAuthProvider
@@ -130,7 +130,13 @@ module HaveAPI::Authentication
130
130
  HaveAPI.t('haveapi.authentication.multiple_oauth2_tokens')
131
131
  end
132
132
 
133
- token && config.find_user_by_access_token(request, token)
133
+ token && authenticate_access_token(request, token)
134
+ rescue HaveAPI::AuthenticationError
135
+ nil
136
+ end
137
+
138
+ def authenticate_access_token(request, token)
139
+ config.find_user_by_access_token(request, token)
134
140
  rescue HaveAPI::AuthenticationError
135
141
  nil
136
142
  end
@@ -9,6 +9,9 @@ module HaveAPI
9
9
  # Methods `example`, `request` and `response` take one argument, the example
10
10
  # to describe.
11
11
  class ClientExample
12
+ SIMPLE_DETAIL_LEVELS = [:simple].freeze
13
+ DEFAULT_DETAIL_LEVELS = %i[simple thorough].freeze
14
+
12
15
  class << self
13
16
  # All subclasses have to call this method to set their label and be
14
17
  # registered.
@@ -46,6 +49,42 @@ module HaveAPI
46
49
  true
47
50
  end
48
51
 
52
+ def language(value = nil)
53
+ @language = value.to_s.freeze unless value.nil?
54
+ @language || id
55
+ end
56
+
57
+ def language_label(value = nil)
58
+ @language_label = value.to_s.freeze unless value.nil?
59
+ @language_label || label
60
+ end
61
+
62
+ def variant(value = nil)
63
+ @variant = value.to_s.freeze unless value.nil?
64
+ @variant
65
+ end
66
+
67
+ def detail_levels(*values)
68
+ @detail_levels = validated_detail_levels(values) unless values.empty?
69
+ return @detail_levels if instance_variable_defined?(:@detail_levels)
70
+
71
+ superclass.respond_to?(:detail_levels) ? superclass.detail_levels : DEFAULT_DETAIL_LEVELS
72
+ end
73
+
74
+ # Custom renderers retain their declaration, including for limitations.
75
+ # Built-ins refine this using the already-rendered Simple result.
76
+ def detail_levels_for(_kind, context:, result:, auth_method: nil, auth_description: nil)
77
+ detail_levels
78
+ end
79
+
80
+ # Older, explicitly pinned generator adapters only provide resolve/render.
81
+ # Their unknown detail semantics retain this wrapper's declaration.
82
+ def adapter_detail_levels_for(adapter:, kind:, context:, result:, mode:, auth_method: nil, auth_description: nil)
83
+ return detail_levels unless adapter.respond_to?(:detail_levels_for)
84
+
85
+ validated_detail_levels(adapter.detail_levels_for(kind:, context:, result:, mode:, auth_method:, auth_description:))
86
+ end
87
+
49
88
  def render(kind, context:, auth_method: nil, auth_description: nil)
50
89
  arguments = [context.host, context.base_url, context.version]
51
90
  arguments.push(context.resource_path, context.resource, context.action_name, context.action) if kind == :example
@@ -98,6 +137,16 @@ module HaveAPI
98
137
  def clients
99
138
  @clients.sort { |a, b| a.order <=> b.order }
100
139
  end
140
+
141
+ private
142
+
143
+ def validated_detail_levels(values)
144
+ unless [SIMPLE_DETAIL_LEVELS, DEFAULT_DETAIL_LEVELS].include?(values)
145
+ raise ArgumentError, 'Example detail levels must be :simple or :simple, :thorough'
146
+ end
147
+
148
+ values.frozen? ? values : values.dup.freeze
149
+ end
101
150
  end
102
151
 
103
152
  attr_reader :resource_path, :resource, :action_name, :action, :host, :base_url, :version, :context
@@ -118,31 +167,110 @@ module HaveAPI
118
167
  self
119
168
  end
120
169
 
170
+ def thorough?
171
+ context&.detail == :thorough
172
+ end
173
+
121
174
  def version_url
122
175
  File.join(base_url, "v#{version}", '/')
123
176
  end
124
177
 
125
178
  protected
126
179
 
127
- # @param password [Boolean] include password parameter
128
- # @return [Hash<String, String>] parameter => example value
129
- def auth_token_credentials(desc, password: true)
130
- passwords = %i[password pass passwd]
131
- params = desc[:resources][:token][:actions]['request'][:input][:parameters].keys - %i[lifetime interval]
180
+ class UnsupportedAuthentication < StandardError; end
181
+
182
+ def auth_field(value, name, default = nil)
183
+ return default unless value.is_a?(Hash)
132
184
 
133
- unless password
134
- params.reject! { |param| passwords.include?(param) }
185
+ value.fetch(name) { value.fetch(name.to_s, default) }
186
+ end
187
+
188
+ def auth_token_actions(description)
189
+ token = auth_field(auth_field(description, :resources, {}), :token, {})
190
+ auth_field(token, :actions, {})
191
+ end
192
+
193
+ def auth_token_parameters(description, action = :request)
194
+ info = auth_field(auth_token_actions(description), action, {})
195
+ input = auth_field(info, :input, {})
196
+ unless [nil, :hash, 'hash', :object, 'object'].include?(auth_field(input, :layout))
197
+ raise UnsupportedAuthentication, 'This token example requires object input.'
198
+ end
199
+
200
+ auth_field(input, :parameters, {})
201
+ end
202
+
203
+ # Only advertised machine types/finite constraints supply public literals.
204
+ # Defaults, prose and current-user data are deliberately not consulted.
205
+ def auth_literal(name, parameter, excluding: [])
206
+ type = auth_field(parameter, :type)
207
+ value = case type
208
+ when 'String', 'Text' then %w[password pass passwd].include?(name.to_s) ? 'secret' : name.to_s
209
+ when 'Integer', 'Resource' then 1
210
+ when 'Float' then 1.0
211
+ when 'Boolean' then excluding.include?(false)
212
+ when 'Datetime' then '2026-01-01T00:00:00Z'
213
+ when 'Custom' then {}
214
+ else raise UnsupportedAuthentication, 'This token example has an unsupported input type.'
215
+ end
216
+ validators = auth_field(parameter, :validators, {})
217
+ inclusion = auth_field(auth_field(validators, :include, {}), :values)
218
+ values = inclusion.is_a?(Hash) ? inclusion.keys : inclusion
219
+ acceptance = auth_field(validators, :accept)
220
+ if acceptance.is_a?(Hash) && (acceptance.has_key?(:value) || acceptance.has_key?('value'))
221
+ value = auth_field(acceptance, :value)
222
+ elsif values.is_a?(Array)
223
+ raise UnsupportedAuthentication, 'This token example has no advertised input choice.' if values.empty?
224
+
225
+ value = values.find { |choice| !excluding.include?(choice) }
226
+ end
227
+ value = 'fixed' if name.to_s == 'lifetime'
228
+ value = value.to_s if value.is_a?(Symbol) && %w[String Text].include?(type)
229
+ if excluding.include?(value)
230
+ raise UnsupportedAuthentication, 'This token client cannot send the advertised login input value.'
231
+ end
232
+
233
+ compatible = case type
234
+ when 'String', 'Text', 'Datetime' then value.is_a?(String)
235
+ when 'Integer' then value.is_a?(Integer)
236
+ when 'Resource' then value.is_a?(Integer) || value.is_a?(String)
237
+ when 'Float' then value.is_a?(Numeric) && value.finite?
238
+ when 'Boolean' then [true, false].include?(value)
239
+ when 'Custom' then value.is_a?(Hash) || value.is_a?(Array) || value.is_a?(String) || value.is_a?(Numeric) || [true, false, nil].include?(value)
240
+ end
241
+ compatible &&= values.map { |item| item.is_a?(Symbol) ? item.to_s : item }.include?(value) if values.is_a?(Array)
242
+ compatible &&= value == auth_field(acceptance, :value) if acceptance.is_a?(Hash)
243
+ raise UnsupportedAuthentication, 'This token example cannot represent its advertised input choice.' unless compatible
244
+
245
+ value
246
+ end
247
+
248
+ def auth_login_literal(_name, _parameter, value)
249
+ value
250
+ end
251
+
252
+ # @param password [Boolean] include an interactive password parameter
253
+ def auth_token_credentials(description, password: true, action: :request)
254
+ auth_token_parameters(description, action).each_with_object({}) do |(name, parameter), input|
255
+ next if name.to_s == 'token' || (!password && %w[password pass passwd].include?(name.to_s))
256
+ next if name.to_s == 'interval' && !auth_field(parameter, :required)
257
+
258
+ begin
259
+ value = auth_literal(name, parameter)
260
+ rescue UnsupportedAuthentication
261
+ raise if auth_field(parameter, :required) || name.to_s == 'lifetime'
262
+
263
+ next
264
+ end
265
+ input[name.to_s] = action.to_s == 'request' ? auth_login_literal(name, parameter, value) : value
135
266
  end
267
+ end
136
268
 
137
- params.to_h do |param|
138
- value =
139
- if passwords.include?(param)
140
- 'secret'
141
- else
142
- param
143
- end
269
+ def auth_token_continuations(description)
270
+ auth_token_actions(description).each_with_object({}) do |(name, _action), result|
271
+ next if %w[request renew revoke].include?(name.to_s)
144
272
 
145
- [param, value]
273
+ result[name.to_s] = auth_token_credentials(description, action: name)
146
274
  end
147
275
  end
148
276
  end
@@ -1,14 +1,16 @@
1
- require 'pp'
1
+ require 'shellwords'
2
2
  require 'haveapi/client_examples/http'
3
3
 
4
4
  module HaveAPI::ClientExamples
5
5
  class Curl < Http
6
6
  label 'curl'
7
+ language 'curl'
8
+ language_label 'curl'
7
9
  code :bash
8
10
  order 90
9
11
 
10
12
  def init
11
- "$ curl --request OPTIONS '#{version_url}'"
13
+ "$ curl --request OPTIONS #{Shellwords.escape(version_url)}"
12
14
  end
13
15
 
14
16
  def auth(method, desc)
@@ -17,35 +19,41 @@ module HaveAPI::ClientExamples
17
19
  <<~END
18
20
  # Password is asked on standard input
19
21
  $ curl --request OPTIONS \\
20
- --user username \\
21
- '#{base_url}'
22
- Password: secret
22
+ --user user \\
23
+ #{Shellwords.escape(base_url)}
24
+ # Password: secret
23
25
 
24
26
  # Password given on the command line
25
27
  $ curl --request OPTIONS \\
26
- --user username:secret \\
27
- '#{base_url}'
28
+ --user user:secret \\
29
+ #{Shellwords.escape(base_url)}
28
30
  END
29
31
 
30
32
  when :token
31
- login = auth_token_credentials(desc).merge(lifetime: 'fixed')
33
+ login = auth_token_credentials(desc)
32
34
 
33
35
  <<~END
34
36
  # Acquire the token
35
37
  $ curl --request POST \\
36
38
  --header 'Content-Type: application/json' \\
37
- --data-binary "#{format_data(token: login)}" \\
38
- '#{File.join(base_url, '_auth', 'token', 'tokens')}'
39
+ --data-binary #{Shellwords.escape(JSON.generate(token: login))} \\
40
+ #{Shellwords.escape(File.join(base_url, '_auth', 'token', 'tokens'))}
39
41
 
40
42
  # Use a previously acquired token
41
43
  $ curl --request OPTIONS \\
42
- --header '#{desc[:http_header]}: thetoken' \\
43
- '#{base_url}'
44
+ --header #{Shellwords.escape("#{desc[:http_header]}: example-token")} \\
45
+ #{Shellwords.escape(base_url)}
44
46
  END
45
47
 
46
48
  when :oauth2
47
49
  '# See RFC 6749 for authorization process and RFC 6750 for access token usage.'
48
50
  end
51
+ rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
52
+ { status: :limitation, reason: e.message }.freeze
53
+ end
54
+
55
+ def example(sample)
56
+ "#{request(sample)}\n#{comment_response(response(sample))}"
49
57
  end
50
58
 
51
59
  def request(sample)
@@ -64,9 +72,9 @@ module HaveAPI::ClientExamples
64
72
  })
65
73
 
66
74
  <<~END
67
- $ curl --request #{action[:method]} \\
68
- --data-binary "#{data}" \\
69
- '#{url}'
75
+ $ curl --request #{Shellwords.escape(action[:method].to_s)} \\
76
+ --data-binary #{data} \\
77
+ #{Shellwords.escape(url)}
70
78
  END
71
79
  end
72
80
 
@@ -80,11 +88,13 @@ module HaveAPI::ClientExamples
80
88
  end
81
89
 
82
90
  def format_data(data)
83
- json = JSON.pretty_generate(data)
84
- json.split("\n").map do |line|
85
- out = ''
86
- PP.pp(line, out).strip[1..-2]
87
- end.join("\n")
91
+ Shellwords.escape(JSON.generate(data))
92
+ end
93
+
94
+ private
95
+
96
+ def comment_response(fragment)
97
+ fragment.gsub(/\r\n?/, "\n").lines.map { |line| "# #{line.delete_suffix("\n")}\n" }.join
88
98
  end
89
99
  end
90
100
  end
@@ -13,7 +13,7 @@ module HaveAPI::ClientExamples
13
13
 
14
14
  def code_result
15
15
  { status: :code, code: yield.freeze }.freeze
16
- rescue Unsupported, JSON::GeneratorError => e
16
+ rescue Unsupported, HaveAPI::ClientExample::UnsupportedAuthentication, JSON::GeneratorError => e
17
17
  { status: :limitation, reason: e.message.freeze }.freeze
18
18
  end
19
19