haveapi 0.30.1 → 0.31.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 +1 -0
- data/Gemfile +1 -0
- data/benchmarks/Gemfile +30 -0
- data/benchmarks/Gemfile.lock +296 -0
- data/benchmarks/documentation.rb +277 -0
- data/benchmarks/endpoints.rb +786 -0
- data/benchmarks/performance_budgets.json +1354 -0
- data/benchmarks/performance_compare.rb +678 -0
- data/benchmarks/shell.nix +21 -0
- data/doc/client-examples.md +179 -26
- data/doc/performance.md +310 -0
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/client_examples/ts_generated_client.rb +36 -0
- data/lib/haveapi/documentation_clients.rb +49 -6
- data/lib/haveapi/documentation_context.rb +42 -12
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/version_page.erb +2 -2
- data/spec/client_examples/context_performance_spec.rb +72 -0
- data/spec/client_examples/context_spec.rb +206 -0
- data/spec/client_examples/profile_mount_spec.rb +45 -0
- data/spec/client_examples/ts_generated_client_spec.rb +53 -0
- data/spec/client_examples/typescript_profiles_spec.rb +113 -0
- data/spec/documentation/client_examples_spec.rb +2 -2
- data/spec/documentation/performance_spec.rb +79 -0
- data/spec/performance/comparator_spec.rb +472 -0
- data/spec/performance/endpoint_contract_spec.rb +226 -0
- data/spec/performance/endpoint_scaling_spec.rb +82 -0
- data/spec/performance/event_baseline_spec.rb +407 -0
- data/spec/performance/json_activation_spec.rb +151 -0
- data/spec/performance/release_lineage_spec.rb +470 -0
- data/spec/performance/runner_spec.rb +610 -0
- data/spec/performance/sqlite_spec.rb +194 -0
- data/spec/performance/support/live_worker.rb +44 -0
- data/spec/performance/support/samples.rb +72 -0
- data/test_support/documentation_performance_fixture.rb +106 -0
- data/test_support/performance_endpoint_fixture.rb +350 -0
- data/test_support/performance_sqlite_fixture.rb +384 -0
- metadata +29 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
describe HaveAPI::ClientExample::Profiles do
|
|
5
|
+
let(:profiles) { described_class.new }
|
|
6
|
+
let(:server) { instance_double(HaveAPI::Server, versions: ['1.0'], root: '/mounted/', describe_version: description) }
|
|
7
|
+
let(:adapter) do
|
|
8
|
+
Class.new do
|
|
9
|
+
def resolve(**); end
|
|
10
|
+
def render(**); end
|
|
11
|
+
end.new
|
|
12
|
+
end
|
|
13
|
+
let(:documentation) { Class.new }
|
|
14
|
+
let(:directory) { Dir.mktmpdir('haveapi-typescript-profile-') }
|
|
15
|
+
|
|
16
|
+
def snapshot_path
|
|
17
|
+
File.join(directory, 'snapshot.json')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def description
|
|
21
|
+
{ resources: { widget: { actions: { show: { input: { parameters: { hidden: { default: 'PRIVATE_DEFAULT' } } }, examples: ['PRIVATE_SAMPLE'] } } } },
|
|
22
|
+
authentication: {}, meta: { namespace: '_meta' } }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
generator = Module.new
|
|
27
|
+
generator.const_set(:VERSION, HaveAPI::VERSION)
|
|
28
|
+
generator.const_set(:Documentation, documentation)
|
|
29
|
+
stub_const('HaveAPI::TypeScriptClient', generator)
|
|
30
|
+
allow(profiles).to receive(:require).with('haveapi/typescript_client/documentation').and_return(true)
|
|
31
|
+
allow(documentation).to receive_messages(from_description: adapter, from_snapshot: adapter)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
after { FileUtils.remove_entry(directory) }
|
|
35
|
+
|
|
36
|
+
def mount
|
|
37
|
+
profiles.mount(['1.0'])
|
|
38
|
+
profiles.finalize(server)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'defers one frozen private structural description and preserves omitted package version' do
|
|
42
|
+
config = { package_name: '@example/widget-client' }
|
|
43
|
+
profiles.register('1.0', typescript: config)
|
|
44
|
+
config[:package_name].replace('changed-client')
|
|
45
|
+
expect(documentation).not_to have_received(:from_description)
|
|
46
|
+
mount
|
|
47
|
+
expect(documentation).to have_received(:from_description).once do |**arguments|
|
|
48
|
+
expect(arguments).to include(api_version: '1.0', protocol_version: HaveAPI::PROTOCOL_VERSION, mount_path: '/mounted/',
|
|
49
|
+
package_name: '@example/widget-client', package_version: nil)
|
|
50
|
+
expect(arguments[:description]).to be_frozen
|
|
51
|
+
expect(JSON.generate(arguments[:description])).not_to include('PRIVATE_DEFAULT', 'PRIVATE_SAMPLE')
|
|
52
|
+
expect(arguments[:description].dig('resources', 'widget', 'actions', 'show', 'input', 'parameters')).to have_key('hidden')
|
|
53
|
+
end
|
|
54
|
+
profile = profiles.for_version('1.0').fetch(:typescript)
|
|
55
|
+
expect(profile).to have_attributes(language: :typescript, package_name: '@example/widget-client', package_version: nil)
|
|
56
|
+
expect(profile).to be_frozen
|
|
57
|
+
expect(profile.adapter).to be_frozen
|
|
58
|
+
expect(server).to have_received(:describe_version).once
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'loads a saved SDK once at registration with explicit package and generator versions' do
|
|
62
|
+
File.write(snapshot_path, JSON.generate(api_version: '1.0'))
|
|
63
|
+
profiles.register('1.0', typescript: { package_name: 'widget-client', package_version: '1.2.3-beta.1+build.4',
|
|
64
|
+
snapshot_path:, generator_version: HaveAPI::VERSION })
|
|
65
|
+
File.unlink(snapshot_path)
|
|
66
|
+
mount
|
|
67
|
+
expect(documentation).to have_received(:from_snapshot).once.with(snapshot: { 'api_version' => '1.0' },
|
|
68
|
+
package_name: 'widget-client', package_version: '1.2.3-beta.1+build.4')
|
|
69
|
+
expect(server).not_to have_received(:describe_version)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'rejects unsafe names, nonconcrete versions and unexpected fields before loading the generator' do
|
|
73
|
+
['../client', '@scope', 'https://example.test/client', 'UPPERCASE', 'client name', 'node_modules'].each do |name|
|
|
74
|
+
expect { profiles.register('1.0', typescript: { package_name: name }) }.to raise_error(described_class::Invalid, /package/)
|
|
75
|
+
end
|
|
76
|
+
['latest', '^1.2.3', '1.2', '01.2.3', '1.2.3-01', false].each do |version|
|
|
77
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client', package_version: version }) }.to raise_error(described_class::Invalid, /package/)
|
|
78
|
+
end
|
|
79
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client', factory: 'custom' }) }.to raise_error(described_class::Invalid, /fields/)
|
|
80
|
+
expect(profiles).not_to have_received(:require)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'rejects missing generators, version mismatch, missing snapshots and incompatible factories' do
|
|
84
|
+
allow(profiles).to receive(:require).and_raise(LoadError)
|
|
85
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client' }) }.to raise_error(described_class::Invalid, /not installed/)
|
|
86
|
+
allow(profiles).to receive(:require).and_return(true)
|
|
87
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client', generator_version: '0.0.0' }) }.to raise_error(described_class::Invalid, /version mismatch/)
|
|
88
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client', snapshot_path: '/missing/schema.json' }) }.to raise_error(described_class::Invalid, /absolute local/)
|
|
89
|
+
allow(documentation).to receive(:respond_to?).and_call_original
|
|
90
|
+
allow(documentation).to receive(:respond_to?).with(:from_snapshot).and_return(false)
|
|
91
|
+
expect { profiles.register('1.0', typescript: { package_name: 'client' }) }.to raise_error(described_class::Invalid, /incompatible profile/)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'rejects a different saved API version without publishing any profile' do
|
|
95
|
+
File.write(snapshot_path, JSON.generate(api_version: '2.0'))
|
|
96
|
+
profiles.register('1.0', typescript: { package_name: 'client', snapshot_path: })
|
|
97
|
+
expect { profiles.mount(['1.0']) }.to raise_error(described_class::Invalid, /TypeScript snapshot API version mismatch/)
|
|
98
|
+
expect(profiles.for_version('1.0')).to be_empty
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'fails atomically and hides private diagnostics for invalid schemas or adapters' do
|
|
102
|
+
allow(documentation).to receive(:from_description).and_raise('PRIVATE_SCHEMA_DIAGNOSTIC')
|
|
103
|
+
profiles.register('1.0', typescript: { package_name: 'client' })
|
|
104
|
+
expect { mount }.to raise_error(described_class::Invalid, 'Configured documentation client schema or configuration is invalid')
|
|
105
|
+
expect(profiles.for_version('1.0')).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'rejects an adapter missing render or resolve during initialization' do
|
|
109
|
+
allow(documentation).to receive(:from_description).and_return(Object.new)
|
|
110
|
+
profiles.register('1.0', typescript: { package_name: 'client' })
|
|
111
|
+
expect { mount }.to raise_error(described_class::Invalid, /adapter interface/)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -42,10 +42,10 @@ describe HaveAPI::ClientExample do
|
|
|
42
42
|
expect(last_response.body).to include('Initialization', '<h4>Ruby</h4>', '<h4>PHP</h4>', '<h4>curl</h4>')
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
it 'hides unconfigured
|
|
45
|
+
it 'hides unconfigured generated SDKs in overview, initialization, authentication and actions' do
|
|
46
46
|
get '/v1/'
|
|
47
47
|
expect(last_response.status).to eq(200)
|
|
48
|
-
expect(last_response.body).not_to include('clients/go', 'clients/rust', 'init-go', 'init-rust', '>Go</h', '>Rust (')
|
|
48
|
+
expect(last_response.body).not_to include('clients/go', 'clients/rust', 'init-go', 'init-rust', 'typescript-generated', '>Go</h', '>Rust (')
|
|
49
49
|
expect(last_response.body).to include('init-javascript-callback', 'init-typescript-async', 'init-python-sync')
|
|
50
50
|
expect(last_response.body).to include('example-widget-show-0-javascript-callback')
|
|
51
51
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'base64'
|
|
3
|
+
require_relative '../../test_support/documentation_performance_fixture'
|
|
4
|
+
|
|
5
|
+
describe HaveAPI::Server, '#mount' do
|
|
6
|
+
let(:server) do
|
|
7
|
+
HaveAPI::DocumentationPerformanceFixture.build(resource_count: 3, actions_per_resource: 2)
|
|
8
|
+
end
|
|
9
|
+
let(:contexts) { [] }
|
|
10
|
+
let(:help_inputs) { [] }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
HaveAPI::ClientExample.clients.each do |client|
|
|
14
|
+
allow(client).to receive(:render).and_wrap_original do |original, kind, context:, **arguments|
|
|
15
|
+
contexts << context
|
|
16
|
+
original.call(kind, context:, **arguments)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
allow(server).to receive(:describe_version).and_wrap_original do |original, *arguments|
|
|
20
|
+
original.call(*arguments).tap do |help|
|
|
21
|
+
help_inputs << [help, Marshal.dump(help)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def request_as(user)
|
|
27
|
+
contexts.clear
|
|
28
|
+
Rack::MockRequest.new(server.app).get(
|
|
29
|
+
'/benchmark/api/v1/', 'HTTP_HOST' => 'example.test',
|
|
30
|
+
'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{user}:fixture-only")}"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def expect_shared_request(response)
|
|
35
|
+
expect(response.status).to eq(200)
|
|
36
|
+
expect(response.body).to include('PUBLIC_SAMPLE_0', 'example-resource0-action0-0',
|
|
37
|
+
'init-javascript-callback', 'init-javascript-async',
|
|
38
|
+
'init-typescript-callback', 'init-typescript-async',
|
|
39
|
+
'init-python-sync', 'init-python-async')
|
|
40
|
+
expect(contexts.length).to be > 20
|
|
41
|
+
description = contexts.first.description
|
|
42
|
+
contexts.each { |context| expect(context.description).to equal(description) }
|
|
43
|
+
expect(description).to be_frozen
|
|
44
|
+
description
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'shares one description across real panels and isolates sequential admin/member/admin requests' do
|
|
48
|
+
first_admin = request_as('admin')
|
|
49
|
+
admin_description = expect_shared_request(first_admin)
|
|
50
|
+
privileged_markers = %w[ADMIN_ONLY_ACTION ADMIN_ONLY_DEFAULT ADMIN_ONLY_REQUEST ADMIN_ONLY_RESPONSE
|
|
51
|
+
ADMIN_ONLY_ACTION_SAMPLE admin_field admin_only]
|
|
52
|
+
privileged_markers.each { |marker| expect(first_admin.body).to include(marker) }
|
|
53
|
+
|
|
54
|
+
member = request_as('member')
|
|
55
|
+
member_description = expect_shared_request(member)
|
|
56
|
+
expect(member_description).not_to equal(admin_description)
|
|
57
|
+
privileged_markers.each { |marker| expect(member.body).not_to include(marker) }
|
|
58
|
+
expect(JSON.generate(contexts.map { |context| [context.description, context.action, context.sample] }))
|
|
59
|
+
.not_to match(/ADMIN_ONLY_|admin_field|admin_only/)
|
|
60
|
+
expect(member_description[:resources]['resource0'][:actions]).not_to have_key('admin_only')
|
|
61
|
+
selected = contexts.find { |context| context.action_name == 'action0' }
|
|
62
|
+
expect(selected.action[:input][:parameters]).not_to have_key(:admin_field)
|
|
63
|
+
expect(selected.sample[:request]).to include(enabled: false, count: 0, nullable: nil, value0: '')
|
|
64
|
+
|
|
65
|
+
last_admin = request_as('admin')
|
|
66
|
+
restored_description = expect_shared_request(last_admin)
|
|
67
|
+
expect(restored_description).not_to equal(admin_description)
|
|
68
|
+
expect(restored_description).not_to equal(member_description)
|
|
69
|
+
expect(restored_description).to eq(admin_description)
|
|
70
|
+
privileged_markers.each { |marker| expect(last_admin.body).to include(marker) }
|
|
71
|
+
|
|
72
|
+
help_inputs.each do |help, original|
|
|
73
|
+
expect(Marshal.dump(help)).to eq(original)
|
|
74
|
+
expect(help).not_to be_frozen
|
|
75
|
+
expect(help[:resources]).not_to be_frozen
|
|
76
|
+
expect(help[:resources]['resource0'][:actions]['action0'][:examples].first[:request]).not_to be_frozen
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|