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,384 @@
|
|
|
1
|
+
require 'haveapi'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
require 'sqlite3'
|
|
4
|
+
require 'haveapi/model_adapters/active_record'
|
|
5
|
+
require 'rack/mock'
|
|
6
|
+
require 'base64'
|
|
7
|
+
require 'digest'
|
|
8
|
+
require 'json'
|
|
9
|
+
require_relative '../benchmarks/performance_compare'
|
|
10
|
+
|
|
11
|
+
module HaveAPI
|
|
12
|
+
module PerformanceFixtures
|
|
13
|
+
# Neutral Rack scenarios; database setup and validation stay outside timing.
|
|
14
|
+
module SQLite
|
|
15
|
+
User = Struct.new(:login)
|
|
16
|
+
|
|
17
|
+
class BasicProvider < HaveAPI::Authentication::Basic::Provider
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def find_user(_request, username, password)
|
|
21
|
+
User.new(username) if %w[admin member].include?(username) && password == 'fixture-only'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.cases(profile: 'quick')
|
|
26
|
+
case profile
|
|
27
|
+
when 'quick'
|
|
28
|
+
[{ id: 'sqlite.page', dimensions: { rows: 128, page_size: 32 } },
|
|
29
|
+
{ id: 'sqlite.includes', dimensions: { rows: 32, include_depth: 2 } }]
|
|
30
|
+
when 'weekly'
|
|
31
|
+
cases + [{ id: 'sqlite.page', dimensions: { rows: 10_000, page_size: 32 } }] +
|
|
32
|
+
[32, 128, 1024].product(%w[admin member]).map do |rows, user|
|
|
33
|
+
{ id: 'sqlite.includes', dimensions: { rows:, include_depth: 2, user: } }
|
|
34
|
+
end
|
|
35
|
+
else raise ArgumentError, 'Unknown SQLite performance profile'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.build(case_spec)
|
|
40
|
+
Scenario.new(case_spec)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.database_namespace
|
|
44
|
+
@database_sequence = (@database_sequence || 0) + 1
|
|
45
|
+
const_set("Database#{@database_sequence}", Module.new)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Scenario
|
|
49
|
+
attr_reader :case_spec
|
|
50
|
+
|
|
51
|
+
def initialize(case_spec)
|
|
52
|
+
@case_spec = case_spec.transform_keys(&:to_sym)
|
|
53
|
+
@dimensions = @case_spec.fetch(:dimensions).transform_keys(&:to_sym)
|
|
54
|
+
@rows = @dimensions.fetch(:rows)
|
|
55
|
+
@user = @dimensions.fetch(:user, 'admin')
|
|
56
|
+
@depth = @dimensions.fetch(:include_depth, 0)
|
|
57
|
+
validate_case!
|
|
58
|
+
@namespace = SQLite.database_namespace
|
|
59
|
+
build_database
|
|
60
|
+
build_api
|
|
61
|
+
@request = Rack::MockRequest.new(@server.app)
|
|
62
|
+
@path = request_path
|
|
63
|
+
@headers = { 'HTTP_HOST' => 'example.test', 'HTTP_ACCEPT' => 'application/json',
|
|
64
|
+
'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{@user}:fixture-only")}" }
|
|
65
|
+
@subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
|
|
66
|
+
record_query(args.last)
|
|
67
|
+
end
|
|
68
|
+
reset!(iterations: 1)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
close
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset!(iterations:)
|
|
75
|
+
raise ArgumentError, 'Iterations must be positive' unless iterations.is_a?(Integer) && iterations > 0
|
|
76
|
+
raise 'SQLite scenario is closed' if @closed
|
|
77
|
+
|
|
78
|
+
@operations = Array.new(iterations) { { queries: 0, authorizations: 0, executions: 0 } }
|
|
79
|
+
@index = 0
|
|
80
|
+
@active_operation = nil
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def call
|
|
85
|
+
raise 'SQLite scenario is closed' if @closed
|
|
86
|
+
raise 'SQLite scenario called beyond reset iterations' if @index >= @operations.length
|
|
87
|
+
|
|
88
|
+
@active_operation = @operations.fetch(@index)
|
|
89
|
+
@index += 1
|
|
90
|
+
@base.uncached { @request.get(@path, @headers) }
|
|
91
|
+
ensure
|
|
92
|
+
@active_operation = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate!(response, index:)
|
|
96
|
+
unless index.is_a?(Integer) && index >= 0 && index < @index
|
|
97
|
+
raise ArgumentError, 'Invalid or uncalled SQLite operation index'
|
|
98
|
+
end
|
|
99
|
+
unless response.status == 200 && response['content-type']&.start_with?('application/json')
|
|
100
|
+
raise 'Unexpected SQLite HTTP/envelope status'
|
|
101
|
+
end
|
|
102
|
+
raise 'Unexpected SQLite operation counters' unless @operations.fetch(index) == expected_counters
|
|
103
|
+
|
|
104
|
+
return yield(-> { validate_body!(response) }) if block_given?
|
|
105
|
+
|
|
106
|
+
validate_body!(response)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_body!(response)
|
|
110
|
+
identity = HaveAPI::Performance::Identity.json(response)
|
|
111
|
+
body = identity.fetch(:canonical_body)
|
|
112
|
+
unless body['status'] == true && body['message'].nil? && body['errors'].nil? &&
|
|
113
|
+
body.keys.sort == %w[errors message response status]
|
|
114
|
+
raise 'Unexpected SQLite HTTP/envelope status'
|
|
115
|
+
end
|
|
116
|
+
raise 'Unexpected SQLite response rows or values' unless body['response'] == expected_response
|
|
117
|
+
|
|
118
|
+
identity
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def counters
|
|
122
|
+
raise 'SQLite operation records are incomplete' unless @index == @operations.length
|
|
123
|
+
|
|
124
|
+
unless @operations.all? { |operation| operation == expected_counters }
|
|
125
|
+
raise 'Unexpected SQLite operation counters'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
{ operations: @operations.map(&:dup) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def close
|
|
132
|
+
return if @closed
|
|
133
|
+
|
|
134
|
+
ActiveSupport::Notifications.unsubscribe(@subscription) if @subscription
|
|
135
|
+
@base.remove_connection if @base
|
|
136
|
+
if @namespace
|
|
137
|
+
name = @namespace.name.split('::').last
|
|
138
|
+
SQLite.send(:remove_const, name) if SQLite.const_defined?(name, false) && SQLite.const_get(name).equal?(@namespace)
|
|
139
|
+
end
|
|
140
|
+
@closed = true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def authorize_record
|
|
144
|
+
@active_operation[:authorizations] += 1 if @active_operation
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def execute_record
|
|
148
|
+
@active_operation[:executions] += 1 if @active_operation
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def validate_case!
|
|
154
|
+
unless %w[sqlite.page sqlite.includes].include?(@case_spec.fetch(:id)) &&
|
|
155
|
+
@rows.is_a?(Integer) && @rows > 0 && %w[admin member].include?(@user)
|
|
156
|
+
raise ArgumentError, 'Invalid SQLite case'
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
allowed = @case_spec[:id] == 'sqlite.page' ? %i[rows page_size user] : %i[rows include_depth user]
|
|
160
|
+
raise ArgumentError, 'Unknown SQLite dimensions' unless (@dimensions.keys - allowed).empty?
|
|
161
|
+
|
|
162
|
+
if @case_spec[:id] == 'sqlite.page'
|
|
163
|
+
unless @dimensions.fetch(:page_size) == 32 && @rows >= 32
|
|
164
|
+
raise ArgumentError, 'SQLite pagination requires a fixed page of 32'
|
|
165
|
+
end
|
|
166
|
+
elsif ![1, 2].include?(@depth)
|
|
167
|
+
raise ArgumentError, 'SQLite include depth must be 1 or 2'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build_database
|
|
172
|
+
@base = @namespace.const_set(:Base, Class.new(ActiveRecord::Base))
|
|
173
|
+
@base.abstract_class = true
|
|
174
|
+
@base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
|
175
|
+
connection = @base.connection
|
|
176
|
+
connection.create_table(:performance_accounts) do |table|
|
|
177
|
+
table.string :label
|
|
178
|
+
table.string :secret
|
|
179
|
+
end
|
|
180
|
+
connection.create_table(:performance_groups) do |table|
|
|
181
|
+
table.string :label
|
|
182
|
+
table.string :secret
|
|
183
|
+
table.integer :account_id
|
|
184
|
+
end
|
|
185
|
+
connection.create_table(:performance_items) do |table|
|
|
186
|
+
table.string :label
|
|
187
|
+
table.boolean :enabled
|
|
188
|
+
table.integer :count
|
|
189
|
+
table.string :note
|
|
190
|
+
table.integer :group_id
|
|
191
|
+
end
|
|
192
|
+
@account = model(:Account, 'performance_accounts')
|
|
193
|
+
@group = model(:Group, 'performance_groups')
|
|
194
|
+
@item = model(:Item, 'performance_items')
|
|
195
|
+
@group.belongs_to(:account, class_name: @account.name)
|
|
196
|
+
@item.belongs_to(:group, class_name: @group.name)
|
|
197
|
+
@account.insert_all!((1..@rows).map { |id| { id:, label: "account#{id}", secret: "ACCOUNT_SECRET_#{id}" } })
|
|
198
|
+
@group.insert_all!((1..@rows).map { |id| { id:, label: "group#{id}", secret: "GROUP_SECRET_#{id}", account_id: id } })
|
|
199
|
+
@item.insert_all!((1..@rows).map { |id| { id:, label: '', enabled: false, count: 0, note: nil, group_id: id } })
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def model(name, table)
|
|
203
|
+
klass = @namespace.const_set(name, Class.new(@base))
|
|
204
|
+
klass.table_name = table
|
|
205
|
+
klass.columns
|
|
206
|
+
klass
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_api
|
|
210
|
+
scenario = self
|
|
211
|
+
depth = @depth
|
|
212
|
+
resources = @namespace.const_set(:API, Module.new)
|
|
213
|
+
account_resource = resource(resources, :Account, @account)
|
|
214
|
+
account_resource.define_action(:Show, superclass: HaveAPI::Actions::Default::Show) do
|
|
215
|
+
authorize do |user, params|
|
|
216
|
+
scenario.authorize_record
|
|
217
|
+
deny if user.login == 'member' && (params['account_id'].to_i % 3) == 0
|
|
218
|
+
output blacklist: [:secret] if user.login == 'member'
|
|
219
|
+
allow
|
|
220
|
+
end
|
|
221
|
+
output(:object) do
|
|
222
|
+
integer :id
|
|
223
|
+
string :label
|
|
224
|
+
string :secret
|
|
225
|
+
end
|
|
226
|
+
define_method(:exec) do
|
|
227
|
+
scenario.execute_record
|
|
228
|
+
raise 'Associated Show must not execute'
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
group_resource = resource(resources, :Group, @group)
|
|
232
|
+
group_resource.define_action(:Show, superclass: HaveAPI::Actions::Default::Show) do
|
|
233
|
+
authorize do |user, params|
|
|
234
|
+
scenario.authorize_record
|
|
235
|
+
deny if user.login == 'member' && params['group_id'].to_i.even?
|
|
236
|
+
output blacklist: [:secret] if user.login == 'member'
|
|
237
|
+
allow
|
|
238
|
+
end
|
|
239
|
+
output(:object) do
|
|
240
|
+
integer :id
|
|
241
|
+
string :label
|
|
242
|
+
string :secret
|
|
243
|
+
# The direct lane's selected output is scalar; it does not rely on
|
|
244
|
+
# the adapter's known missing default preload for nested resources.
|
|
245
|
+
resource account_resource if depth == 2
|
|
246
|
+
end
|
|
247
|
+
define_method(:exec) do
|
|
248
|
+
scenario.execute_record
|
|
249
|
+
raise 'Associated Show must not execute'
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
item_resource = resource(resources, :Item, @item)
|
|
253
|
+
add_index(item_resource, scenario)
|
|
254
|
+
add_includes(item_resource, group_resource, scenario)
|
|
255
|
+
@server = HaveAPI::Server.new(resources)
|
|
256
|
+
@server.use_version(1)
|
|
257
|
+
@server.default_version = 1
|
|
258
|
+
@server.auth_chain << BasicProvider
|
|
259
|
+
@server.mount('/performance/api/')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def resource(resources, name, model)
|
|
263
|
+
klass = resources.const_set(name, Class.new(HaveAPI::Resource))
|
|
264
|
+
klass.resource_name = name.to_s
|
|
265
|
+
klass.version(1)
|
|
266
|
+
klass.auth(true)
|
|
267
|
+
klass.model(model)
|
|
268
|
+
unless name == :Item
|
|
269
|
+
klass.define_action(:Index, superclass: HaveAPI::Actions::Default::Index) do
|
|
270
|
+
authorize { deny }
|
|
271
|
+
output(:object_list) do
|
|
272
|
+
integer :id
|
|
273
|
+
string :label
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
klass
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def add_index(resource, scenario)
|
|
281
|
+
resource.define_action(:Index, superclass: HaveAPI::Actions::Default::Index) do
|
|
282
|
+
authorize do
|
|
283
|
+
scenario.authorize_record
|
|
284
|
+
allow
|
|
285
|
+
end
|
|
286
|
+
output(:object_list) do
|
|
287
|
+
integer :id
|
|
288
|
+
string :label
|
|
289
|
+
bool :enabled
|
|
290
|
+
integer :count
|
|
291
|
+
string :note, nullable: true
|
|
292
|
+
end
|
|
293
|
+
define_method(:exec) do
|
|
294
|
+
scenario.execute_record
|
|
295
|
+
with_pagination(self.class.model.order(id: :asc)).to_a
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def add_includes(resource, group_resource, scenario)
|
|
301
|
+
resource.define_action(:Included, superclass: HaveAPI::Actions::Default::Index) do
|
|
302
|
+
route 'included'
|
|
303
|
+
authorize do
|
|
304
|
+
scenario.authorize_record
|
|
305
|
+
allow
|
|
306
|
+
end
|
|
307
|
+
output(:object_list) do
|
|
308
|
+
integer :id
|
|
309
|
+
string :label
|
|
310
|
+
resource group_resource
|
|
311
|
+
end
|
|
312
|
+
define_method(:exec) do
|
|
313
|
+
scenario.execute_record
|
|
314
|
+
with_includes(self.class.model.order(id: :asc)).to_a
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def request_path
|
|
320
|
+
query = if @case_spec[:id] == 'sqlite.page'
|
|
321
|
+
{ item: { from_id: 0, limit: 32 } }
|
|
322
|
+
else
|
|
323
|
+
{ _meta: { includes: @depth == 1 ? 'group' : 'group__account' } }
|
|
324
|
+
end
|
|
325
|
+
route = @case_spec[:id] == 'sqlite.page' ? 'items' : 'items/included'
|
|
326
|
+
"/performance/api/v1/#{route}?#{Rack::Utils.build_nested_query(query)}"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def record_query(payload)
|
|
330
|
+
return unless @active_operation && payload[:connection]&.pool.equal?(@base.connection_pool)
|
|
331
|
+
return if %w[SCHEMA TRANSACTION].include?(payload[:name])
|
|
332
|
+
return unless payload[:sql].match?(/\A\s*(?:SELECT|INSERT|UPDATE|DELETE)\b/i)
|
|
333
|
+
raise 'SQLite request unexpectedly used query cache' if payload[:cached]
|
|
334
|
+
|
|
335
|
+
@active_operation[:queries] += 1
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def expected_counters
|
|
339
|
+
return { queries: 1, authorizations: 1, executions: 1 } if @case_spec[:id] == 'sqlite.page'
|
|
340
|
+
|
|
341
|
+
nested = if @depth == 2
|
|
342
|
+
@user == 'admin' ? @rows : (@rows + 1) / 2
|
|
343
|
+
else
|
|
344
|
+
0
|
|
345
|
+
end
|
|
346
|
+
{ queries: @depth + 1, authorizations: 1 + @rows + nested, executions: 1 }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def expected_response
|
|
350
|
+
length = @case_spec[:id] == 'sqlite.page' ? 32 : @rows
|
|
351
|
+
items = (1..length).map do |id|
|
|
352
|
+
value = if @case_spec[:id] == 'sqlite.page'
|
|
353
|
+
{ 'id' => id, 'label' => '', 'enabled' => false, 'count' => 0, 'note' => nil }
|
|
354
|
+
else
|
|
355
|
+
{ 'id' => id, 'label' => '', 'group' => expected_group(id) }
|
|
356
|
+
end
|
|
357
|
+
value['_meta'] = { 'path_params' => [id], 'resolved' => true }
|
|
358
|
+
value
|
|
359
|
+
end
|
|
360
|
+
{ 'items' => items, '_meta' => {} }
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def expected_group(id)
|
|
364
|
+
denied = { '_meta' => { 'resolved' => false, 'authorized' => false } }
|
|
365
|
+
return denied if @user == 'member' && id.even?
|
|
366
|
+
|
|
367
|
+
result = { 'id' => id, 'label' => "group#{id}", '_meta' => { 'path_params' => [id], 'resolved' => true } }
|
|
368
|
+
result['secret'] = "GROUP_SECRET_#{id}" if @user == 'admin'
|
|
369
|
+
return result if @depth == 1
|
|
370
|
+
|
|
371
|
+
result['account'] = if @user == 'member' && (id % 3) == 0
|
|
372
|
+
denied
|
|
373
|
+
else
|
|
374
|
+
account = { 'id' => id, 'label' => "account#{id}",
|
|
375
|
+
'_meta' => { 'path_params' => [id], 'resolved' => @depth == 2 } }
|
|
376
|
+
account['secret'] = "ACCOUNT_SECRET_#{id}" if @user == 'admin' && @depth == 2
|
|
377
|
+
account
|
|
378
|
+
end
|
|
379
|
+
result
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: haveapi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jakub Skokan
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.31.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.31.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: i18n
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -210,11 +210,19 @@ files:
|
|
|
210
210
|
- LICENSE.txt
|
|
211
211
|
- README.md
|
|
212
212
|
- Rakefile
|
|
213
|
+
- benchmarks/Gemfile
|
|
214
|
+
- benchmarks/Gemfile.lock
|
|
215
|
+
- benchmarks/documentation.rb
|
|
216
|
+
- benchmarks/endpoints.rb
|
|
217
|
+
- benchmarks/performance_budgets.json
|
|
218
|
+
- benchmarks/performance_compare.rb
|
|
219
|
+
- benchmarks/shell.nix
|
|
213
220
|
- doc/client-examples.md
|
|
214
221
|
- doc/create-client.md
|
|
215
222
|
- doc/hooks.erb
|
|
216
223
|
- doc/index.md
|
|
217
224
|
- doc/json-schema.html
|
|
225
|
+
- doc/performance.md
|
|
218
226
|
- doc/protocol.md
|
|
219
227
|
- doc/protocol.png
|
|
220
228
|
- doc/typed-input-validation.md
|
|
@@ -254,6 +262,7 @@ files:
|
|
|
254
262
|
- lib/haveapi/client_examples/rust_client.rb
|
|
255
263
|
- lib/haveapi/client_examples/ts_async_client.rb
|
|
256
264
|
- lib/haveapi/client_examples/ts_client.rb
|
|
265
|
+
- lib/haveapi/client_examples/ts_generated_client.rb
|
|
257
266
|
- lib/haveapi/common.rb
|
|
258
267
|
- lib/haveapi/context.rb
|
|
259
268
|
- lib/haveapi/documentation_clients.rb
|
|
@@ -345,6 +354,7 @@ files:
|
|
|
345
354
|
- spec/authorization_spec.rb
|
|
346
355
|
- spec/client_examples/assets/highlighter_test.cjs
|
|
347
356
|
- spec/client_examples/assets/tabs_test.cjs
|
|
357
|
+
- spec/client_examples/context_performance_spec.rb
|
|
348
358
|
- spec/client_examples/context_spec.rb
|
|
349
359
|
- spec/client_examples/dynamic_acceptance.md
|
|
350
360
|
- spec/client_examples/dynamic_acceptance.rb
|
|
@@ -360,12 +370,15 @@ files:
|
|
|
360
370
|
- spec/client_examples/rust_client_spec.rb
|
|
361
371
|
- spec/client_examples/structural_schema_spec.rb
|
|
362
372
|
- spec/client_examples/syntax_spec.rb
|
|
373
|
+
- spec/client_examples/ts_generated_client_spec.rb
|
|
374
|
+
- spec/client_examples/typescript_profiles_spec.rb
|
|
363
375
|
- spec/common_spec.rb
|
|
364
376
|
- spec/documentation/auth_filtering_spec.rb
|
|
365
377
|
- spec/documentation/client_examples_spec.rb
|
|
366
378
|
- spec/documentation/current_user_html_escaping_spec.rb
|
|
367
379
|
- spec/documentation/examples_spec.rb
|
|
368
380
|
- spec/documentation/host_html_escaping_spec.rb
|
|
381
|
+
- spec/documentation/performance_spec.rb
|
|
369
382
|
- spec/documentation_spec.rb
|
|
370
383
|
- spec/envelope_spec.rb
|
|
371
384
|
- spec/extensions/action_exceptions_spec.rb
|
|
@@ -376,6 +389,16 @@ files:
|
|
|
376
389
|
- spec/packaging_spec.rb
|
|
377
390
|
- spec/parameters/typed_spec.rb
|
|
378
391
|
- spec/params_spec.rb
|
|
392
|
+
- spec/performance/comparator_spec.rb
|
|
393
|
+
- spec/performance/endpoint_contract_spec.rb
|
|
394
|
+
- spec/performance/endpoint_scaling_spec.rb
|
|
395
|
+
- spec/performance/event_baseline_spec.rb
|
|
396
|
+
- spec/performance/json_activation_spec.rb
|
|
397
|
+
- spec/performance/release_lineage_spec.rb
|
|
398
|
+
- spec/performance/runner_spec.rb
|
|
399
|
+
- spec/performance/sqlite_spec.rb
|
|
400
|
+
- spec/performance/support/live_worker.rb
|
|
401
|
+
- spec/performance/support/samples.rb
|
|
379
402
|
- spec/resource_spec.rb
|
|
380
403
|
- spec/server/integration_spec.rb
|
|
381
404
|
- spec/spec_helper.rb
|
|
@@ -391,9 +414,12 @@ files:
|
|
|
391
414
|
- spec/validators/presence_spec.rb
|
|
392
415
|
- test_support/client_test_api.rb
|
|
393
416
|
- test_support/client_test_server.rb
|
|
417
|
+
- test_support/documentation_performance_fixture.rb
|
|
394
418
|
- test_support/dynamic_documentation_api.rb
|
|
395
419
|
- test_support/dynamic_protocol_spec.rb
|
|
396
420
|
- test_support/dynamic_python_login_spec.rb
|
|
421
|
+
- test_support/performance_endpoint_fixture.rb
|
|
422
|
+
- test_support/performance_sqlite_fixture.rb
|
|
397
423
|
licenses:
|
|
398
424
|
- MIT
|
|
399
425
|
metadata: {}
|