haveapi 0.31.2 → 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,247 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
require_relative 'dynamic_fixture'
|
|
4
|
+
|
|
5
|
+
RSpec.describe HaveAPI::ClientExample do
|
|
6
|
+
include DynamicExampleFixture
|
|
7
|
+
|
|
8
|
+
it 'retains all registered direct lanes while keeping authored Ruby and PHP assembly separators clean' do
|
|
9
|
+
expect(described_class.clients.length).to eq(15)
|
|
10
|
+
sample = { path_params: [7], request: { name: 'widget' }, response: { id: 7 }, status: true }
|
|
11
|
+
action = dynamic_action.merge(path: '/mount/v2/widgets/{widget_id}')
|
|
12
|
+
base = dynamic_context(resource_path: ['widget'], action_name: 'show', action:, sample:)
|
|
13
|
+
lanes = [[:init, base, nil], [:auth, base, :basic], [:auth, base, :token], [:auth, base, :oauth2],
|
|
14
|
+
[:example, base, nil], [:example, base.with(action: action.merge(auth: true)), nil],
|
|
15
|
+
[:example, base.with(sample: sample.merge(status: false)), nil]]
|
|
16
|
+
described_class.clients.each do |renderer|
|
|
17
|
+
%i[simple thorough].each do |detail|
|
|
18
|
+
lanes.each do |kind, context, provider|
|
|
19
|
+
result = renderer.render(kind, context: context.with(detail:), auth_method: provider, auth_description: token_description)
|
|
20
|
+
expect(result).to include(status: satisfy { |status| %i[code limitation].include?(status) })
|
|
21
|
+
next unless result[:status] == :code && %w[ruby php].include?(renderer.language)
|
|
22
|
+
|
|
23
|
+
source = result.fetch(:code)
|
|
24
|
+
expect(source).not_to match(/\A\n|\n\n\z|\n[ \t]+\n|\n\n\n/)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'preserves custom renderer whitespace and literal content exactly' do
|
|
31
|
+
source = "\ncustom literal\n\n\n content\n\n"
|
|
32
|
+
renderer = Class.new(described_class) { define_method(:init) { source } }
|
|
33
|
+
expect(renderer.render(:init, context: dynamic_context).fetch(:code)).to eq(source)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'keeps built-in client IDs, order and language metadata together' do
|
|
37
|
+
expected = [
|
|
38
|
+
['0', 'ruby', 'Ruby', nil],
|
|
39
|
+
%w[javascript-callback javascript JavaScript Callback],
|
|
40
|
+
%w[javascript-async javascript JavaScript Async],
|
|
41
|
+
%w[typescript-callback typescript TypeScript Callback],
|
|
42
|
+
%w[typescript-async typescript TypeScript Async],
|
|
43
|
+
%w[python-sync python Python Sync],
|
|
44
|
+
%w[python-async python Python Async],
|
|
45
|
+
['go', 'go', 'Go', nil],
|
|
46
|
+
%w[rust-async rust Rust Async],
|
|
47
|
+
%w[rust-blocking rust Rust Blocking],
|
|
48
|
+
['typescript-generated', 'typescript', 'TypeScript', 'API client'],
|
|
49
|
+
['2', 'php', 'PHP', nil], ['3', 'cli', 'CLI', nil],
|
|
50
|
+
['5', 'curl', 'curl', nil], ['6', 'http', 'HTTP', nil]
|
|
51
|
+
]
|
|
52
|
+
actual = described_class.clients.map { |renderer| [renderer.id, renderer.language, renderer.language_label, renderer.variant] }
|
|
53
|
+
expect(actual).to eq(expected)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'defaults unspecified third-party metadata to its own ID and label without inheriting a built-in group' do
|
|
57
|
+
renderer = Class.new(HaveAPI::ClientExamples::PythonClient) do
|
|
58
|
+
def self.label
|
|
59
|
+
'Custom client'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
stub_const('CustomDocumentationExample', renderer)
|
|
63
|
+
|
|
64
|
+
expect(renderer.language).to eq(renderer.id)
|
|
65
|
+
expect(renderer.language_label).to eq('Custom client')
|
|
66
|
+
expect(renderer.variant).to be_nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'defaults undeclared custom renderers to both frozen detail levels' do
|
|
70
|
+
renderer = Class.new(described_class)
|
|
71
|
+
expect(renderer.detail_levels).to eq(%i[simple thorough])
|
|
72
|
+
expect(renderer.detail_levels).to be_frozen
|
|
73
|
+
expect { renderer.detail_levels << :other }.to raise_error(FrozenError)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'inherits the nearest detail declaration and freezes an independently owned override' do
|
|
77
|
+
parent = Class.new(described_class)
|
|
78
|
+
levels = [:simple]
|
|
79
|
+
parent.detail_levels(*levels)
|
|
80
|
+
levels << :thorough
|
|
81
|
+
child = Class.new(parent)
|
|
82
|
+
grandchild = Class.new(child)
|
|
83
|
+
expect(child.detail_levels).to equal(parent.detail_levels)
|
|
84
|
+
expect(grandchild.detail_levels).to eq([:simple])
|
|
85
|
+
child.detail_levels :simple, :thorough
|
|
86
|
+
expect(child.detail_levels).to be_frozen
|
|
87
|
+
expect(grandchild.detail_levels).to equal(child.detail_levels)
|
|
88
|
+
expect(parent.detail_levels).to eq([:simple])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'rejects unsupported, duplicate, reversed and non-symbol detail declarations without changing metadata' do
|
|
92
|
+
renderer = Class.new(described_class)
|
|
93
|
+
renderer.detail_levels :simple
|
|
94
|
+
[[:thorough], %i[thorough simple], %i[simple simple], %i[simple thorough thorough],
|
|
95
|
+
[:other], ['simple'], [nil], [[:simple]]].each do |values|
|
|
96
|
+
expect { renderer.detail_levels(*values) }.to raise_error(ArgumentError)
|
|
97
|
+
expect(renderer.detail_levels).to eq([:simple])
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'declares one format for HTTP, inherited curl and CLI without changing registry IDs or other clients' do
|
|
102
|
+
single_level = [HaveAPI::ClientExamples::Http, HaveAPI::ClientExamples::Curl, HaveAPI::ClientExamples::RubyCli]
|
|
103
|
+
registry = described_class.clients
|
|
104
|
+
single_level.each do |renderer|
|
|
105
|
+
expect(renderer.detail_levels).to eq([:simple])
|
|
106
|
+
expect(renderer.detail_levels).to be_frozen
|
|
107
|
+
end
|
|
108
|
+
expect(HaveAPI::ClientExamples::Curl.detail_levels).to equal(HaveAPI::ClientExamples::Http.detail_levels)
|
|
109
|
+
(registry - single_level).each { |renderer| expect(renderer.detail_levels).to eq(%i[simple thorough]) }
|
|
110
|
+
expect(described_class.clients).to eq(registry)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'uses simple for legacy positional instances and reads thorough only from its context' do
|
|
114
|
+
renderer = described_class.new('api.test', 'https://api.test/', '1')
|
|
115
|
+
context = described_class::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
116
|
+
version_url: 'https://api.test/v1/', description: {})
|
|
117
|
+
expect(renderer.thorough?).to be(false)
|
|
118
|
+
expect(renderer.with_context(context).thorough?).to be(false)
|
|
119
|
+
expect(renderer.with_context(context.with(detail: :thorough)).thorough?).to be(true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'keeps a custom Simple limitation and its distinct thorough result under the legacy declaration' do
|
|
123
|
+
renderer = Class.new(described_class) do
|
|
124
|
+
def init
|
|
125
|
+
thorough? ? 'custom recovery instructions' : { status: :limitation, reason: 'custom Simple limitation' }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
context = described_class::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
129
|
+
version_url: 'https://api.test/v1/', description: {})
|
|
130
|
+
result = renderer.render(:init, context:)
|
|
131
|
+
expect(result).to include(status: :limitation)
|
|
132
|
+
expect(renderer.detail_levels_for(:init, context:, result:)).to equal(renderer.detail_levels)
|
|
133
|
+
expect(renderer.render(:init, context: context.with(detail: :thorough))).to include(code: 'custom recovery instructions')
|
|
134
|
+
renderer.detail_levels :simple
|
|
135
|
+
expect(renderer.detail_levels_for(:init, context:, result:)).to equal(renderer.detail_levels)
|
|
136
|
+
expect(context.detail).to eq(:simple)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'retains inherited legacy declarations for every wrapper without invoking the adapter' do
|
|
140
|
+
calls = []
|
|
141
|
+
adapter = Class.new do
|
|
142
|
+
define_method(:resolve) { |**| calls << :resolve }
|
|
143
|
+
define_method(:render) { |**| calls << :render }
|
|
144
|
+
end.new.freeze
|
|
145
|
+
limitation = { status: :limitation, reason: 'Simple format unavailable' }.freeze
|
|
146
|
+
[[HaveAPI::ClientExamples::GoClient, :go],
|
|
147
|
+
[HaveAPI::ClientExamples::RustAsyncClient, :rust],
|
|
148
|
+
[HaveAPI::ClientExamples::RustBlockingClient, :rust],
|
|
149
|
+
[HaveAPI::ClientExamples::TsGeneratedClient, :typescript]].each do |wrapper, language|
|
|
150
|
+
profile = described_class::Profile.new(language:, package_name: 'fixture-client', package_version: nil, adapter:)
|
|
151
|
+
context = described_class::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
152
|
+
version_url: 'https://api.test/v1/', description: {}, profile:)
|
|
153
|
+
child = Class.new(wrapper)
|
|
154
|
+
expect(child.detail_levels_for(:init, context:, result: limitation)).to equal(wrapper.detail_levels)
|
|
155
|
+
expect(child.detail_levels_for(:init, context:, result: limitation)).to eq(%i[simple thorough])
|
|
156
|
+
child.detail_levels :simple
|
|
157
|
+
expect(child.detail_levels_for(:init, context:, result: limitation)).to equal(child.detail_levels)
|
|
158
|
+
expect(child.detail_levels_for(:init, context:, result: limitation)).to be_frozen
|
|
159
|
+
expect(context.profile).to equal(profile)
|
|
160
|
+
expect(context.profile.adapter).to equal(adapter)
|
|
161
|
+
end
|
|
162
|
+
expect(calls).to be_empty
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'keeps a pinned saved-profile adapter with only resolve/render through actual wrapper selection' do
|
|
166
|
+
calls = []
|
|
167
|
+
adapter = Class.new do
|
|
168
|
+
define_method(:resolve) { |**| calls << :resolve }
|
|
169
|
+
define_method(:render) do |kind:, context:, mode:, **|
|
|
170
|
+
calls << [kind, context.detail, mode]
|
|
171
|
+
if context.detail == :thorough
|
|
172
|
+
{ status: :code, code: 'legacy recovery instructions' }.freeze
|
|
173
|
+
else
|
|
174
|
+
{ status: :limitation, reason: 'legacy Simple limitation' }.freeze
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end.new
|
|
178
|
+
profiles = described_class::Profiles.new
|
|
179
|
+
generator = Module.new
|
|
180
|
+
generator.const_set(:VERSION, '0.29.0')
|
|
181
|
+
documentation = Class.new
|
|
182
|
+
generator.const_set(:Documentation, documentation)
|
|
183
|
+
stub_const('HaveAPI::GoClient', generator)
|
|
184
|
+
allow(profiles).to receive(:require).with('haveapi/go_client/documentation').and_return(true)
|
|
185
|
+
allow(documentation).to receive(:new).and_return(adapter)
|
|
186
|
+
Dir.mktmpdir('haveapi-legacy-detail-') do |directory|
|
|
187
|
+
path = File.join(directory, 'description.json')
|
|
188
|
+
File.write(path, JSON.generate(resources: {}, authentication: {}, meta: { namespace: '_meta' }))
|
|
189
|
+
profiles.register(1, go: { import_path: 'example.test/client', description_path: path,
|
|
190
|
+
generator_version: '0.29.0' })
|
|
191
|
+
profiles.mount([1])
|
|
192
|
+
server = instance_double(HaveAPI::Server)
|
|
193
|
+
profiles.finalize(server)
|
|
194
|
+
profile = profiles.for_version(1).fetch(:go)
|
|
195
|
+
context = described_class::Context.new(version: '1', host: 'api.test', base_url: 'https://api.test/',
|
|
196
|
+
version_url: 'https://api.test/v1/', description: {}, profile:)
|
|
197
|
+
wrapper = HaveAPI::ClientExamples::GoClient
|
|
198
|
+
simple = wrapper.render(:init, context:)
|
|
199
|
+
levels = wrapper.detail_levels_for(:init, context:, result: simple)
|
|
200
|
+
expect(levels).to eq(%i[simple thorough])
|
|
201
|
+
expect(levels).to be_frozen
|
|
202
|
+
expect(calls).to eq([%i[init simple sync]])
|
|
203
|
+
panels = levels.map do |detail|
|
|
204
|
+
[detail, detail == :simple ? simple : wrapper.render(:init, context: context.with(detail:))]
|
|
205
|
+
end
|
|
206
|
+
expect(panels).to eq([
|
|
207
|
+
[:simple, { status: :limitation, reason: 'legacy Simple limitation' }],
|
|
208
|
+
[:thorough, { status: :code, code: 'legacy recovery instructions' }]
|
|
209
|
+
])
|
|
210
|
+
expect(calls).to eq([%i[init simple sync], %i[init thorough sync]])
|
|
211
|
+
expect(profile.adapter).to equal(adapter)
|
|
212
|
+
expect(profile).to be_frozen
|
|
213
|
+
expect(adapter).to be_frozen
|
|
214
|
+
expect(adapter).not_to respond_to(:detail_levels_for)
|
|
215
|
+
expect(context.detail).to eq(:simple)
|
|
216
|
+
expect(documentation).to have_received(:new).once
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'delegates modern limitation queries exactly once and propagates exceptions and invalid levels' do
|
|
221
|
+
wrapper = HaveAPI::ClientExamples::GoClient
|
|
222
|
+
adapter = Object.new
|
|
223
|
+
def adapter.detail_levels_for(**); end
|
|
224
|
+
allow(adapter).to receive(:detail_levels_for).and_return([:simple])
|
|
225
|
+
context = Object.new.freeze
|
|
226
|
+
result = { status: :limitation, reason: 'current limitation' }.freeze
|
|
227
|
+
auth_description = { http_header: 'X-Token' }.freeze
|
|
228
|
+
levels = wrapper.adapter_detail_levels_for(adapter:, kind: :auth, context:, result:, mode: :sync,
|
|
229
|
+
auth_method: :token, auth_description:)
|
|
230
|
+
expect(levels).to eq([:simple])
|
|
231
|
+
expect(levels).to be_frozen
|
|
232
|
+
expect(adapter).to have_received(:detail_levels_for).once.with(
|
|
233
|
+
kind: :auth, context:, result:, mode: :sync, auth_method: :token, auth_description:
|
|
234
|
+
)
|
|
235
|
+
[nil, [], [:thorough], %i[thorough simple], %i[simple simple], ['simple']].each do |invalid|
|
|
236
|
+
allow(adapter).to receive(:detail_levels_for).and_return(invalid)
|
|
237
|
+
expect do
|
|
238
|
+
wrapper.adapter_detail_levels_for(adapter:, kind: :init, context:, result:, mode: :sync)
|
|
239
|
+
end.to raise_error(ArgumentError)
|
|
240
|
+
end
|
|
241
|
+
failure = RuntimeError.new('modern query failure')
|
|
242
|
+
allow(adapter).to receive(:detail_levels_for).and_raise(failure)
|
|
243
|
+
expect do
|
|
244
|
+
wrapper.adapter_detail_levels_for(adapter:, kind: :init, context:, result:, mode: :sync)
|
|
245
|
+
end.to(raise_error { |error| expect(error).to equal(failure) })
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
-
|
|
2
|
+
require_relative '../../test_support/documentation_oauth_fixture'
|
|
3
3
|
|
|
4
4
|
module ProfileMountSpec
|
|
5
5
|
User = Struct.new(:login)
|
|
@@ -132,10 +132,21 @@ describe HaveAPI::Server, '#mount' do
|
|
|
132
132
|
server.documentation_clients(version, go: { import_path: 'example.test/widget/client' })
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
-
def
|
|
135
|
+
def configure_documentation_identity
|
|
136
|
+
fixture = DocumentationOAuthFixture.configure(
|
|
137
|
+
server, users: { 1 => %w[member admin].to_h { |role| [role, ProfileMountSpec::User.new(role)] } },
|
|
138
|
+
mount_path: '/mounted/api/'
|
|
139
|
+
)
|
|
140
|
+
server.define_singleton_method(:documentation_fixture) { fixture }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def request_as(user, locale, resource: nil)
|
|
144
|
+
fixture = server.documentation_fixture
|
|
145
|
+
path = '/mounted/api/v1/'
|
|
146
|
+
path += "?resource=#{CGI.escape(resource)}" if resource
|
|
136
147
|
Rack::MockRequest.new(server.app).get(
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
"#{fixture.origin}#{path}", 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT_LANGUAGE' => locale,
|
|
149
|
+
'HTTP_COOKIE' => fixture.header_for(server, version: 1, token: user)
|
|
139
150
|
)
|
|
140
151
|
end
|
|
141
152
|
|
|
@@ -179,6 +190,7 @@ describe HaveAPI::Server, '#mount' do
|
|
|
179
190
|
it 'captures each configured version once and does not initialize from requests' do
|
|
180
191
|
register(1)
|
|
181
192
|
register(2)
|
|
193
|
+
configure_documentation_identity
|
|
182
194
|
server.mount('/mounted/api/')
|
|
183
195
|
expect(descriptions.map { |description| description['help'] }).to eq(['/mounted/api/v1/', '/mounted/api/v2/'])
|
|
184
196
|
request_as('member', 'en')
|
|
@@ -189,24 +201,32 @@ describe HaveAPI::Server, '#mount' do
|
|
|
189
201
|
|
|
190
202
|
it 'keeps private schema separate from sequential users, locales, HTML and samples' do
|
|
191
203
|
register
|
|
204
|
+
configure_documentation_identity
|
|
192
205
|
server.available_locales = %i[en cs]
|
|
193
206
|
::I18n.backend.store_translations(:en, profile_mount_spec: { action: 'Visible English action' })
|
|
194
207
|
::I18n.backend.store_translations(:cs, profile_mount_spec: { action: 'Viditelna ceska akce' })
|
|
195
208
|
server.mount('/mounted/api/')
|
|
196
|
-
privileged = request_as('admin', 'en')
|
|
209
|
+
privileged = request_as('admin', 'en', resource: 'widget')
|
|
197
210
|
expect(privileged.status).to eq(200)
|
|
198
211
|
expect(privileged.body).to include('PRIVATE_SAMPLE', 'Visible English action')
|
|
212
|
+
expect(contexts.select { |context| context.sample&.dig(:request, :secret) == 'PRIVATE_SAMPLE' }.map(&:detail))
|
|
213
|
+
.to contain_exactly(:simple, :thorough)
|
|
214
|
+
public_overview = request_as('member', 'cs')
|
|
215
|
+
expect(public_overview.status).to eq(200)
|
|
216
|
+
expect(public_overview.body).to include('init-go', 'example.test/widget/client')
|
|
217
|
+
expect(public_overview.body).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE')
|
|
199
218
|
contexts.clear
|
|
200
|
-
public_response = request_as('member', 'cs')
|
|
219
|
+
public_response = request_as('member', 'cs', resource: 'widget')
|
|
201
220
|
expect(public_response.status).to eq(200)
|
|
202
|
-
expect(public_response.body).to include('Viditelna ceska akce', '
|
|
221
|
+
expect(public_response.body).to include('Viditelna ceska akce', 'example-widget-show-0-go', 'example-widget-show-0-go-thorough')
|
|
203
222
|
expect(public_response.body).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE', 'admin_only', 'haveapi.invalid', 'StructuralSchema')
|
|
204
223
|
selected = contexts.find { |context| context.action_name == 'show' }
|
|
205
224
|
expect(selected.action.dig(:input, :parameters).keys.map(&:to_s)).to eq(['visible'])
|
|
206
225
|
expect(selected.description[:resources]['widget'][:actions]).not_to have_key('secret')
|
|
207
226
|
expect(selected.sample[:request]).to eq(visible: 'public sample')
|
|
208
227
|
expect(selected.action[:description]).to eq('Viditelna ceska akce')
|
|
209
|
-
expect(
|
|
228
|
+
expect(contexts.select { |context| context.action_name == 'show' }.map(&:detail)).to contain_exactly(:simple, :thorough)
|
|
229
|
+
expect(request_as('member', 'en', resource: 'widget').body).to include('Visible English action')
|
|
210
230
|
expect(descriptions.size).to eq(1)
|
|
211
231
|
end
|
|
212
232
|
|
|
@@ -229,23 +249,30 @@ describe HaveAPI::Server, '#mount' do
|
|
|
229
249
|
|
|
230
250
|
it 'mounts a generated TypeScript panel beside dynamic tabs using only authorized request samples' do
|
|
231
251
|
register_typescript
|
|
252
|
+
configure_documentation_identity
|
|
232
253
|
server.mount('/mounted/api/')
|
|
233
254
|
expect(descriptions.size).to eq(1)
|
|
234
255
|
private_schema = descriptions.first
|
|
235
256
|
expect(private_schema).to be_frozen
|
|
236
257
|
expect(JSON.generate(private_schema)).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE')
|
|
237
258
|
expect(private_schema.dig('resources', 'widget', 'actions')).to have_key('secret')
|
|
238
|
-
expect(request_as('admin', 'en').body).to include('PRIVATE_SAMPLE')
|
|
259
|
+
expect(request_as('admin', 'en', resource: 'widget').body).to include('PRIVATE_SAMPLE')
|
|
260
|
+
overview = request_as('member', 'en')
|
|
261
|
+
expect(overview.status).to eq(200)
|
|
262
|
+
expect(overview.body).to include('init-typescript-generated', 'init-typescript-callback',
|
|
263
|
+
'init-typescript-async', '@example/widget-client')
|
|
264
|
+
expect(overview.body).not_to include('PRIVATE_SAMPLE', 'example-widget-show-0-typescript-generated')
|
|
239
265
|
contexts.clear
|
|
240
|
-
response = request_as('member', 'en')
|
|
266
|
+
response = request_as('member', 'en', resource: 'widget')
|
|
241
267
|
expect(response.status).to eq(200)
|
|
242
|
-
expect(response.body).to include('TypeScript
|
|
243
|
-
'
|
|
268
|
+
expect(response.body).to include('TypeScript API client', 'example-widget-show-0-typescript-generated',
|
|
269
|
+
'example-widget-show-0-typescript-generated-thorough')
|
|
244
270
|
expect(response.body).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE', 'PRIVATE_RESPONSE', 'admin_only')
|
|
245
271
|
selected = contexts.find { |context| context.action_name == 'show' }
|
|
246
|
-
expect(selected.version_url).to eq('
|
|
272
|
+
expect(selected.version_url).to eq('https://example.org/mounted/api/v1/')
|
|
247
273
|
expect(selected.action.dig(:input, :parameters).keys).to eq([:visible])
|
|
248
274
|
expect(selected.sample[:request]).to eq(visible: 'public sample')
|
|
275
|
+
expect(contexts.select { |context| context.action_name == 'show' }.map(&:detail)).to contain_exactly(:simple, :thorough)
|
|
249
276
|
expect(documentation).to have_received(:from_description).once
|
|
250
277
|
end
|
|
251
278
|
|
|
@@ -3,6 +3,8 @@ require 'haveapi/documentation_context'
|
|
|
3
3
|
require 'haveapi/client_examples/rust_client'
|
|
4
4
|
|
|
5
5
|
class RustDocumentationAdapterFixture
|
|
6
|
+
def detail_levels_for(kind:, context:, result:, mode:, auth_method: nil, auth_description: nil); end
|
|
7
|
+
|
|
6
8
|
def render(kind:, context:, mode:, auth_method: nil, auth_description: nil); end
|
|
7
9
|
end
|
|
8
10
|
|
|
@@ -61,7 +63,7 @@ describe HaveAPI::ClientExamples::RustClient do
|
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
it 'preserves a concise adapter limitation without fabricating source' do
|
|
64
|
-
result = { status: :limitation, reason: 'This sample needs an unsupported
|
|
66
|
+
result = { status: :limitation, reason: 'This sample needs an unsupported API client capability.' }.freeze
|
|
65
67
|
allow(adapter).to receive(:render).and_return(result)
|
|
66
68
|
expect(HaveAPI::ClientExamples::RustAsyncClient.render(:example, context:)).to eq(result)
|
|
67
69
|
end
|
|
@@ -72,4 +74,36 @@ describe HaveAPI::ClientExamples::RustClient do
|
|
|
72
74
|
expect(renderer.auth(:basic, {})).to include(status: :limitation)
|
|
73
75
|
expect(renderer.example({})).to include(status: :limitation)
|
|
74
76
|
end
|
|
77
|
+
|
|
78
|
+
it 'delegates each mode capability with the exact Simple result without rerendering or mutating context' do
|
|
79
|
+
description = { http_header: 'X-Token' }.freeze
|
|
80
|
+
[HaveAPI::ClientExamples::RustAsyncClient, HaveAPI::ClientExamples::RustBlockingClient].each do |renderer|
|
|
81
|
+
allow(adapter).to receive(:detail_levels_for).and_return(%i[simple thorough].freeze)
|
|
82
|
+
levels = renderer.detail_levels_for(:auth, context:, result: code,
|
|
83
|
+
auth_method: :token, auth_description: description)
|
|
84
|
+
expect(levels).to eq(%i[simple thorough])
|
|
85
|
+
expect(levels).to be_frozen
|
|
86
|
+
expect(adapter).to have_received(:detail_levels_for).with(
|
|
87
|
+
kind: :auth, context:, result: code, mode: renderer.rust_mode,
|
|
88
|
+
auth_method: :token, auth_description: description
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
expect(adapter).not_to have_received(:render)
|
|
92
|
+
expect(context.detail).to eq(:simple)
|
|
93
|
+
expect(context.profile.adapter).to equal(adapter)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'delegates limitation policy and rejects malformed modern levels instead of falling back' do
|
|
97
|
+
renderer = HaveAPI::ClientExamples::RustAsyncClient
|
|
98
|
+
limitation = { status: :limitation, reason: 'Unsupported sample' }.freeze
|
|
99
|
+
allow(adapter).to receive(:detail_levels_for).and_return([:simple].freeze)
|
|
100
|
+
expect(renderer.detail_levels_for(:example, context:, result: limitation)).to eq([:simple])
|
|
101
|
+
expect(adapter).to have_received(:detail_levels_for).with(
|
|
102
|
+
kind: :example, context:, result: limitation, mode: :async, auth_method: nil, auth_description: nil
|
|
103
|
+
)
|
|
104
|
+
[nil, [], [:thorough], %i[thorough simple], %i[simple simple], ['simple']].each do |invalid|
|
|
105
|
+
allow(adapter).to receive(:detail_levels_for).and_return(invalid)
|
|
106
|
+
expect { renderer.detail_levels_for(:example, context:, result: code) }.to raise_error(ArgumentError)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
75
109
|
end
|