haveapi 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/benchmarks/documentation.rb +29 -6
- data/benchmarks/endpoints.rb +153 -24
- data/benchmarks/spec/client_examples_spec.rb +109 -37
- data/doc/client-examples.md +218 -45
- data/doc/documentation-oauth.md +157 -0
- data/doc/documentation-ui.md +192 -0
- data/doc/index.md +3 -6
- data/doc/performance.md +39 -12
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/authentication/chain.rb +19 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
- data/lib/haveapi/client_example.rb +143 -15
- data/lib/haveapi/client_examples/curl.rb +30 -20
- data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
- data/lib/haveapi/client_examples/go_client.rb +9 -2
- data/lib/haveapi/client_examples/http.rb +9 -4
- data/lib/haveapi/client_examples/js_async_client.rb +7 -0
- data/lib/haveapi/client_examples/js_client.rb +47 -36
- data/lib/haveapi/client_examples/php_client.rb +161 -75
- data/lib/haveapi/client_examples/python_async_client.rb +3 -0
- data/lib/haveapi/client_examples/python_client.rb +61 -47
- data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
- data/lib/haveapi/client_examples/ruby_client.rb +80 -31
- data/lib/haveapi/client_examples/rust_client.rb +20 -9
- data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
- data/lib/haveapi/client_examples/ts_client.rb +3 -0
- data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
- data/lib/haveapi/documentation_context.rb +7 -2
- data/lib/haveapi/documentation_helpers.rb +223 -0
- data/lib/haveapi/documentation_oauth.rb +399 -0
- data/lib/haveapi/documentation_search.rb +225 -0
- data/lib/haveapi/documentation_search_index.rb +79 -0
- data/lib/haveapi/documentation_validators.rb +179 -0
- data/lib/haveapi/public/css/documentation.css +343 -0
- data/lib/haveapi/public/js/documentation.js +992 -0
- data/lib/haveapi/server.rb +144 -86
- data/lib/haveapi/shared_documentation_helpers.rb +54 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +16 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
- data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
- data/lib/haveapi/views/documentation/search_form.erb +10 -0
- data/lib/haveapi/views/documentation/search_index.erb +1 -0
- data/lib/haveapi/views/documentation/search_results.erb +20 -0
- data/lib/haveapi/views/index.erb +2 -4
- data/lib/haveapi/views/main_layout.erb +12 -6
- data/lib/haveapi/views/shared_documentation.erb +12 -0
- data/lib/haveapi/views/shared_sidebar.erb +25 -0
- data/lib/haveapi/views/version_page/auth_body.erb +12 -33
- data/lib/haveapi/views/version_page/client_overview.erb +40 -0
- data/lib/haveapi/views/version_page/example_panels.erb +18 -0
- data/lib/haveapi/views/version_page/resource_body.erb +91 -132
- data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
- data/lib/haveapi/views/version_page/usage.erb +7 -0
- data/lib/haveapi/views/version_page.erb +36 -117
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
- data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
- data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
- data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
- data/lib/haveapi/views/version_sidebar.erb +59 -48
- data/spec/authentication/basic_spec.rb +10 -10
- data/spec/authentication/oauth2_chain_spec.rb +72 -0
- data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
- data/spec/client_examples/assets/documentation_test.cjs +817 -0
- data/spec/client_examples/context_performance_spec.rb +5 -4
- data/spec/client_examples/context_spec.rb +23 -2
- data/spec/client_examples/detail_spec.rb +175 -0
- data/spec/client_examples/dynamic_acceptance.md +59 -26
- data/spec/client_examples/dynamic_acceptance.rb +164 -44
- data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
- data/spec/client_examples/dynamic_fixture.rb +4 -3
- data/spec/client_examples/dynamic_js_spec.rb +20 -11
- data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
- data/spec/client_examples/dynamic_python_spec.rb +62 -11
- data/spec/client_examples/go_spec.rb +14 -1
- data/spec/client_examples/presentation_spec.rb +247 -0
- data/spec/client_examples/profile_mount_spec.rb +40 -13
- data/spec/client_examples/rust_client_spec.rb +35 -1
- data/spec/client_examples/shell_spec.rb +268 -0
- data/spec/client_examples/syntax_spec.rb +192 -0
- data/spec/client_examples/ts_generated_client_spec.rb +15 -2
- data/spec/client_examples/typescript_profiles_spec.rb +1 -1
- data/spec/documentation/auth_filtering_spec.rb +9 -4
- data/spec/documentation/canonical_routes_spec.rb +89 -0
- data/spec/documentation/client_examples_spec.rb +39 -5
- data/spec/documentation/client_overview_spec.rb +145 -0
- data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
- data/spec/documentation/example_variants_spec.rb +93 -0
- data/spec/documentation/fixtures/search_index.json +476 -0
- data/spec/documentation/identity_and_guides_spec.rb +130 -0
- data/spec/documentation/identity_context_spec.rb +180 -0
- data/spec/documentation/navigation_spec.rb +296 -0
- data/spec/documentation/oauth_fixture_spec.rb +39 -0
- data/spec/documentation/oauth_spec.rb +912 -0
- data/spec/documentation/oauth_transport_spec.rb +45 -0
- data/spec/documentation/performance_spec.rb +47 -6
- data/spec/documentation/reference_headings_spec.rb +190 -0
- data/spec/documentation/search_index_routes_spec.rb +202 -0
- data/spec/documentation/search_index_spec.rb +85 -0
- data/spec/documentation/search_routes_spec.rb +199 -0
- data/spec/documentation/search_spec.rb +134 -0
- data/spec/documentation/shared_pages_spec.rb +240 -0
- data/spec/documentation/validators_spec.rb +123 -0
- data/spec/documentation_spec.rb +3 -3
- data/spec/packaging_spec.rb +21 -2
- data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
- data/spec/performance/documentation_shapes_spec.rb +97 -0
- data/spec/performance/json_activation_spec.rb +78 -1
- data/spec/performance/release_lineage_spec.rb +4 -3
- data/spec/performance/runner_spec.rb +1 -0
- data/test_support/all_client_performance_fixture.rb +6 -10
- data/test_support/documentation_oauth_fixture.rb +54 -0
- data/test_support/documentation_performance_fixture.rb +19 -1
- data/test_support/dynamic_documentation_api.rb +35 -10
- data/test_support/dynamic_python_login_spec.rb +3 -11
- metadata +52 -3
|
@@ -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
|
|
2
|
-
=====================
|
|
1
|
+
# HaveAPI internals
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
48
|
-
every
|
|
49
|
-
output, including successful and failed
|
|
50
|
-
isolation through the same adapters
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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.
|
|
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 &&
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
def
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
[
|
|
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 '
|
|
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
|
|
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
|
|
21
|
-
|
|
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
|
|
27
|
-
|
|
28
|
+
--user user:secret \\
|
|
29
|
+
#{Shellwords.escape(base_url)}
|
|
28
30
|
END
|
|
29
31
|
|
|
30
32
|
when :token
|
|
31
|
-
login = auth_token_credentials(desc)
|
|
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
|
|
38
|
-
|
|
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
|
|
43
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|