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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile +1 -0
  4. data/benchmarks/Gemfile +30 -0
  5. data/benchmarks/Gemfile.lock +296 -0
  6. data/benchmarks/documentation.rb +277 -0
  7. data/benchmarks/endpoints.rb +786 -0
  8. data/benchmarks/performance_budgets.json +1354 -0
  9. data/benchmarks/performance_compare.rb +678 -0
  10. data/benchmarks/shell.nix +21 -0
  11. data/doc/client-examples.md +179 -26
  12. data/doc/performance.md +310 -0
  13. data/haveapi.gemspec +1 -1
  14. data/lib/haveapi/client_examples/ts_generated_client.rb +36 -0
  15. data/lib/haveapi/documentation_clients.rb +49 -6
  16. data/lib/haveapi/documentation_context.rb +42 -12
  17. data/lib/haveapi/version.rb +1 -1
  18. data/lib/haveapi/views/version_page.erb +2 -2
  19. data/spec/client_examples/context_performance_spec.rb +72 -0
  20. data/spec/client_examples/context_spec.rb +206 -0
  21. data/spec/client_examples/profile_mount_spec.rb +45 -0
  22. data/spec/client_examples/ts_generated_client_spec.rb +53 -0
  23. data/spec/client_examples/typescript_profiles_spec.rb +113 -0
  24. data/spec/documentation/client_examples_spec.rb +2 -2
  25. data/spec/documentation/performance_spec.rb +79 -0
  26. data/spec/performance/comparator_spec.rb +472 -0
  27. data/spec/performance/endpoint_contract_spec.rb +226 -0
  28. data/spec/performance/endpoint_scaling_spec.rb +82 -0
  29. data/spec/performance/event_baseline_spec.rb +407 -0
  30. data/spec/performance/json_activation_spec.rb +151 -0
  31. data/spec/performance/release_lineage_spec.rb +470 -0
  32. data/spec/performance/runner_spec.rb +610 -0
  33. data/spec/performance/sqlite_spec.rb +194 -0
  34. data/spec/performance/support/live_worker.rb +44 -0
  35. data/spec/performance/support/samples.rb +72 -0
  36. data/test_support/documentation_performance_fixture.rb +106 -0
  37. data/test_support/performance_endpoint_fixture.rb +350 -0
  38. data/test_support/performance_sqlite_fixture.rb +384 -0
  39. metadata +29 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d58d9e427d490d025fec95d60739ea4aa17b4e86cc3fb9978b7c00b751e6e75
4
- data.tar.gz: e0b958ac9924d4cacaf75513d79690c3ed2519d6b5d1522a0433d92285742887
3
+ metadata.gz: 92be099fb88d7c03896cb0d40205830260f93959987227c2d793a31012e6917f
4
+ data.tar.gz: 06c2e32b9c92d68ff32cfbe5f43ae9c22fb360a0d8bfb365ead9a48080001348
5
5
  SHA512:
6
- metadata.gz: 78980868da2569146dccf39ce14309e27b97d51b4a266b7709813faafb191577520f31408dd6ebf1e5310b37bd43197e9b4e838503cd60c5d8b480607e8c5f6d
7
- data.tar.gz: 5a2a6ad3e686e3d77b86fb83e7e27b5eaf5e2ac573013fa938255b203405fdabe899a605f71cea8088769e81f323b6678be00272889c56d629757356d9170d87
6
+ metadata.gz: d3cb01c43beac90a8a095eded1edb417a32090ef7149fcd65da86d6d31da0cb54a2eb79f57e2ed2a456f5fd9f1d5bc7916551e88d0856b5d09fb5a1b0e0bba95
7
+ data.tar.gz: 610e4b8bc84dc86b6a03db3bcfac4bdedf8512482e76cfe2b3b8013cfc33fdb4367055cc546ada82ac1153342c742cbfe1f42b7efb466a3239aab5595352f5b3
data/.gitignore CHANGED
@@ -2,4 +2,5 @@ doc/protocol.*
2
2
  doc/create-client.md
3
3
  doc/typed-input-validation.md
4
4
  doc/client-examples.md
5
+ doc/performance.md
5
6
  doc/json-schema.html
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ gemspec
4
4
  gem 'haveapi-client', path: '../../clients/ruby'
5
5
 
6
6
  group :test do
7
+ gem 'json', '= 2.21.2'
7
8
  gem 'rack-test'
8
9
  gem 'rackup'
9
10
  gem 'rspec'
@@ -0,0 +1,30 @@
1
+ source 'https://rubygems.org'
2
+
3
+ ruby '3.4.11'
4
+
5
+ # Third-party benchmark tools only; measured HaveAPI comes from exported roots.
6
+ gem 'activerecord', '= 8.1.4'
7
+ gem 'activesupport', '= 8.1.4'
8
+ gem 'faraday', '= 2.15.0'
9
+ gem 'fiddle', '= 1.1.8'
10
+ gem 'highline', '= 3.1.2'
11
+ gem 'i18n', '= 1.15.2'
12
+ gem 'json', '= 2.21.2'
13
+ gem 'mail', '= 2.9.1'
14
+ gem 'nesty', '= 1.0.2'
15
+ gem 'rack-oauth2', '= 2.2.1'
16
+ gem 'rack-test', '= 2.3.0'
17
+ gem 'rackup', '= 2.3.1'
18
+ gem 'rake', '= 13.4.2'
19
+ gem 'redcarpet', '= 3.6.1'
20
+ gem 'require_all', '= 2.0.0'
21
+ gem 'rest-client', '= 2.1.0'
22
+ gem 'rspec', '= 3.13.2'
23
+ gem 'ruby-progressbar', '= 1.13.0'
24
+ gem 'sinatra', '= 4.2.1'
25
+ gem 'sinatra-activerecord', '= 2.0.28'
26
+ gem 'sinatra-contrib', '= 4.2.1'
27
+ gem 'sqlite3', '= 2.10.1'
28
+ gem 'tilt', '= 2.9.0'
29
+ gem 'tomlrb', '= 2.0.5'
30
+ gem 'webrick', '= 1.9.2'
@@ -0,0 +1,296 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activemodel (8.1.4)
5
+ activesupport (= 8.1.4)
6
+ activerecord (8.1.4)
7
+ activemodel (= 8.1.4)
8
+ activesupport (= 8.1.4)
9
+ timeout (>= 0.4.0)
10
+ activesupport (8.1.4)
11
+ base64
12
+ bigdecimal
13
+ concurrent-ruby (~> 1.0, >= 1.3.1)
14
+ connection_pool (>= 2.2.5)
15
+ drb
16
+ i18n (>= 1.6, < 2)
17
+ json
18
+ logger (>= 1.4.2)
19
+ minitest (>= 5.1)
20
+ securerandom (>= 0.3)
21
+ tzinfo (~> 2.0, >= 2.0.5)
22
+ uri (>= 0.13.1)
23
+ aes_key_wrap (1.1.0)
24
+ attr_required (1.0.2)
25
+ base64 (0.3.0)
26
+ bigdecimal (4.1.3)
27
+ bindata (3.0.1)
28
+ concurrent-ruby (1.3.8)
29
+ connection_pool (3.0.2)
30
+ date (3.6.0)
31
+ diff-lcs (1.6.2)
32
+ domain_name (0.7.20261007)
33
+ drb (2.2.3)
34
+ faraday (2.15.0)
35
+ faraday-net_http (>= 2.0, < 3.6)
36
+ json
37
+ logger
38
+ faraday-follow_redirects (0.5.0)
39
+ faraday (>= 1, < 3)
40
+ faraday-net_http (3.5.0)
41
+ net-http (~> 0.5)
42
+ fiddle (1.1.8)
43
+ highline (3.1.2)
44
+ reline
45
+ http-accept (1.7.0)
46
+ http-cookie (1.1.6)
47
+ domain_name (~> 0.5)
48
+ i18n (1.15.2)
49
+ concurrent-ruby (~> 1.0)
50
+ io-console (0.9.4)
51
+ json (2.21.2)
52
+ json-jwt (1.17.2)
53
+ activesupport (>= 4.2)
54
+ aes_key_wrap
55
+ base64
56
+ bindata
57
+ faraday (~> 2.0)
58
+ faraday-follow_redirects
59
+ logger (1.7.0)
60
+ mail (2.9.1)
61
+ logger
62
+ mini_mime (>= 0.1.1)
63
+ net-imap
64
+ net-pop
65
+ net-smtp
66
+ mime-types (3.7.0)
67
+ logger
68
+ mime-types-data (~> 3.2025, >= 3.2025.0507)
69
+ mime-types-data (3.2026.1020)
70
+ mini_mime (1.1.5)
71
+ minitest (6.0.6)
72
+ drb (~> 2.0)
73
+ prism (~> 1.5)
74
+ multi_json (1.21.2)
75
+ mustermann (3.1.1)
76
+ nesty (1.0.2)
77
+ net-http (0.10.0)
78
+ net-protocol (>= 0.3.0)
79
+ uri (>= 0.11.1)
80
+ net-imap (0.6.8)
81
+ date
82
+ net-protocol
83
+ net-pop (0.1.2)
84
+ net-protocol
85
+ net-protocol (0.4.0)
86
+ timeout
87
+ net-smtp (0.5.2)
88
+ net-protocol
89
+ netrc (0.11.0)
90
+ prism (1.9.0)
91
+ racc (1.8.1)
92
+ rack (3.2.7)
93
+ rack-oauth2 (2.2.1)
94
+ activesupport
95
+ attr_required
96
+ faraday (~> 2.0)
97
+ faraday-follow_redirects
98
+ json-jwt (>= 1.11.0)
99
+ rack (>= 2.1.0)
100
+ rack-protection (4.2.1)
101
+ base64 (>= 0.1.0)
102
+ logger (>= 1.6.0)
103
+ rack (>= 3.0.0, < 4)
104
+ rack-session (2.1.2)
105
+ base64 (>= 0.1.0)
106
+ rack (>= 3.0.0)
107
+ rack-test (2.3.0)
108
+ rack (>= 1.3)
109
+ rackup (2.3.1)
110
+ rack (>= 3)
111
+ rake (13.4.2)
112
+ redcarpet (3.6.1)
113
+ reline (0.7.0)
114
+ io-console (~> 0.5)
115
+ require_all (2.0.0)
116
+ rest-client (2.1.0)
117
+ http-accept (>= 1.7.0, < 2.0)
118
+ http-cookie (>= 1.0.2, < 2.0)
119
+ mime-types (>= 1.16, < 4.0)
120
+ netrc (~> 0.8)
121
+ rspec (3.13.2)
122
+ rspec-core (~> 3.13.0)
123
+ rspec-expectations (~> 3.13.0)
124
+ rspec-mocks (~> 3.13.0)
125
+ rspec-core (3.13.6)
126
+ rspec-support (~> 3.13.0)
127
+ rspec-expectations (3.13.5)
128
+ diff-lcs (>= 1.2.0, < 2.0)
129
+ rspec-support (~> 3.13.0)
130
+ rspec-mocks (3.13.8)
131
+ diff-lcs (>= 1.2.0, < 2.0)
132
+ rspec-support (~> 3.13.0)
133
+ rspec-support (3.13.7)
134
+ ruby-progressbar (1.13.0)
135
+ securerandom (0.4.1)
136
+ sinatra (4.2.1)
137
+ logger (>= 1.6.0)
138
+ mustermann (~> 3.0)
139
+ rack (>= 3.0.0, < 4)
140
+ rack-protection (= 4.2.1)
141
+ rack-session (>= 2.0.0, < 3)
142
+ tilt (~> 2.0)
143
+ sinatra-activerecord (2.0.28)
144
+ activerecord (>= 4.1)
145
+ sinatra (>= 1.0)
146
+ sinatra-contrib (4.2.1)
147
+ multi_json (>= 0.0.2)
148
+ mustermann (~> 3.0)
149
+ rack-protection (= 4.2.1)
150
+ sinatra (= 4.2.1)
151
+ tilt (~> 2.0)
152
+ sqlite3 (2.10.1-aarch64-linux-gnu)
153
+ sqlite3 (2.10.1-aarch64-linux-musl)
154
+ sqlite3 (2.10.1-arm-linux-gnu)
155
+ sqlite3 (2.10.1-arm-linux-musl)
156
+ sqlite3 (2.10.1-arm64-darwin)
157
+ sqlite3 (2.10.1-x86-linux-gnu)
158
+ sqlite3 (2.10.1-x86-linux-musl)
159
+ sqlite3 (2.10.1-x86_64-darwin)
160
+ sqlite3 (2.10.1-x86_64-linux-gnu)
161
+ sqlite3 (2.10.1-x86_64-linux-musl)
162
+ tilt (2.9.0)
163
+ timeout (0.6.1)
164
+ tomlrb (2.0.5)
165
+ racc (~> 1.4)
166
+ tzinfo (2.0.6)
167
+ concurrent-ruby (~> 1.0)
168
+ uri (1.1.2)
169
+ webrick (1.9.2)
170
+
171
+ PLATFORMS
172
+ aarch64-linux-gnu
173
+ aarch64-linux-musl
174
+ arm-linux-gnu
175
+ arm-linux-musl
176
+ arm64-darwin
177
+ x86-linux-gnu
178
+ x86-linux-musl
179
+ x86_64-darwin
180
+ x86_64-linux-gnu
181
+ x86_64-linux-musl
182
+
183
+ DEPENDENCIES
184
+ activerecord (= 8.1.4)
185
+ activesupport (= 8.1.4)
186
+ faraday (= 2.15.0)
187
+ fiddle (= 1.1.8)
188
+ highline (= 3.1.2)
189
+ i18n (= 1.15.2)
190
+ json (= 2.21.2)
191
+ mail (= 2.9.1)
192
+ nesty (= 1.0.2)
193
+ rack-oauth2 (= 2.2.1)
194
+ rack-test (= 2.3.0)
195
+ rackup (= 2.3.1)
196
+ rake (= 13.4.2)
197
+ redcarpet (= 3.6.1)
198
+ require_all (= 2.0.0)
199
+ rest-client (= 2.1.0)
200
+ rspec (= 3.13.2)
201
+ ruby-progressbar (= 1.13.0)
202
+ sinatra (= 4.2.1)
203
+ sinatra-activerecord (= 2.0.28)
204
+ sinatra-contrib (= 4.2.1)
205
+ sqlite3 (= 2.10.1)
206
+ tilt (= 2.9.0)
207
+ tomlrb (= 2.0.5)
208
+ webrick (= 1.9.2)
209
+
210
+ CHECKSUMS
211
+ activemodel (8.1.4) sha256=9a0906c127bd118aa5f06157db12b8f750da83dccee72db2f658547a0ae7a0e8
212
+ activerecord (8.1.4) sha256=3b4957ea7f246c9231bd7faa909312eac03a9101aaf87977b6c52733ff00b04f
213
+ activesupport (8.1.4) sha256=891dd3fe43c4fc8391934b237b1189bb1f661c932a84a840043db9d46a4e80df
214
+ aes_key_wrap (1.1.0) sha256=b935f4756b37375895db45669e79dfcdc0f7901e12d4e08974d5540c8e0776a5
215
+ attr_required (1.0.2) sha256=f0ebfc56b35e874f4d0ae799066dbc1f81efefe2364ca3803dc9ea6a4de6cb99
216
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
217
+ bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
218
+ bindata (3.0.1) sha256=e97a826a12f6e7c5847e983e5826f277d61035317d352ec339b079537f2fa466
219
+ bundler (4.0.23) sha256=407ba8a1d928f412847f819aa5f8b6aef7c59fb51feb517112af8a165487287b
220
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
221
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
222
+ date (3.6.0) sha256=18ac945c4e24ebd93534f8eab8c2fd8c5604b3e977105b3798d62a1e8d27b580
223
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
224
+ domain_name (0.7.20261007) sha256=0976c6ab837dbde991b433d03acc38fdb91fa8839df31a1ed75ce0318dc0441c
225
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
226
+ faraday (2.15.0) sha256=3be0eff0187ed913a1b4f679e9fcfe845b542cdd4f75b76cd6181a4ae1335fff
227
+ faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
228
+ faraday-net_http (3.5.0) sha256=2e7094c34e38754ee048cc3df47376f8657005d9375f8473bce1c3b526a22e36
229
+ fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
230
+ highline (3.1.2) sha256=67cbd34d19f6ef11a7ee1d82ffab5d36dfd5b3be861f450fc1716c7125f4bb4a
231
+ http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
232
+ http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9
233
+ i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
234
+ io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
235
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
236
+ json-jwt (1.17.2) sha256=97e37c1c3e22e9e1d74a1c0d77319c5cdc8a636226e5dbb1664b0b34eddcf1c9
237
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
238
+ mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8
239
+ mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
240
+ mime-types-data (3.2026.1020) sha256=6b3bcc776c1e3591267727700467e02fb9c12b503fcce4e0e964516d7c1e0d06
241
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
242
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
243
+ multi_json (1.21.2) sha256=245531eaf54bdba57aba515ff08b4503505c5e09911daa89c5a561e2f85bea8e
244
+ mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
245
+ nesty (1.0.2) sha256=1c6ee90b96d46d6dfad9ca1b91104e57c0f46dd24f2a186406026eb75833498b
246
+ net-http (0.10.0) sha256=1a2ff6e37a05724973c43f2bf7bf105f59c36a4cd0a7e9b1c021a0a566987c27
247
+ net-imap (0.6.8) sha256=eb30bb62227b64a3af6b518b04b2375113d69f68f15de59b0193b07d98acf6d9
248
+ net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
249
+ net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853
250
+ net-smtp (0.5.2) sha256=35103cc04d6b2d94632f9928ede07483c24b70b9c0db1f231fea43909b678f3c
251
+ netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
252
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
253
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
254
+ rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
255
+ rack-oauth2 (2.2.1) sha256=c73aa87c508043e2258f02b4fb110cacba9b37d2ccf884e22487d014a120d1a5
256
+ rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
257
+ rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
258
+ rack-test (2.3.0) sha256=b118d177cc002c572fc1b6fd8d19c9ed5881a36e8d6f689f9e618f9f861241e8
259
+ rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
260
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
261
+ redcarpet (3.6.1) sha256=d444910e6aa55480c6bcdc0cdb057626e8a32c054c29e793fa642ba2f155f445
262
+ reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
263
+ require_all (2.0.0) sha256=68d7b944b0f1c9898a16f9f102b448897c06fd2781d30efec19cbcf6e2164e6a
264
+ rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3
265
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
266
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
267
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
268
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
269
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
270
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
271
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
272
+ sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
273
+ sinatra-activerecord (2.0.28) sha256=99f352c2dfa244d02b4f877efbe00135360b758390b8bb7bc2d4d91171c93811
274
+ sinatra-contrib (4.2.1) sha256=10d091c944d268aa910c618ea40a3c3ebe0533e6e32990d84af92235a3d26b4a
275
+ sqlite3 (2.10.1-aarch64-linux-gnu) sha256=48390cc2b809acd1280f5c9daaf3c2013833adb8a5ece9b0634f708651080ccd
276
+ sqlite3 (2.10.1-aarch64-linux-musl) sha256=db1bcddf98e4eeea80ca896f56aa3f9468364fa1ee8e042ddc6b628b5092fabb
277
+ sqlite3 (2.10.1-arm-linux-gnu) sha256=34a67a3fe84f705a0fad6f7aa4541579300644b9acdf414797e59929a6e3528b
278
+ sqlite3 (2.10.1-arm-linux-musl) sha256=e317adb8979b57dafd999b54410b4050296aa5d00caa664e3f75d876e2ae90e8
279
+ sqlite3 (2.10.1-arm64-darwin) sha256=45f74dad0a8ee57ae5fadfac39bf160d1f09fb0484a442223310d869cd957d92
280
+ sqlite3 (2.10.1-x86-linux-gnu) sha256=7b3f9eff7c5c26551800649312796203ded8a505bcdaf30cd915dd9ae5e9575c
281
+ sqlite3 (2.10.1-x86-linux-musl) sha256=8f193eb1b4a66156baaaedafac0de091533cf840f4b78cf3fa2019cde0388f10
282
+ sqlite3 (2.10.1-x86_64-darwin) sha256=00d1cd2e8ecff5ad9fffa0c97de59d4f85c5fc54eada2587d53ca0bd933b7a5d
283
+ sqlite3 (2.10.1-x86_64-linux-gnu) sha256=661ecede31e23a412059cd8c684ed84e332f64f167e4a90902cf99e8ea2c8718
284
+ sqlite3 (2.10.1-x86_64-linux-musl) sha256=67c7c5443a930157b4e148f0f4cb9ab998ee7ecfa8ce56879a6b56d1d742b2be
285
+ tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5
286
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
287
+ tomlrb (2.0.5) sha256=6e9a5e70a94a723842e770fcd14c49867703500d07cd03ebb6c343138a7b4477
288
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
289
+ uri (1.1.2) sha256=89292e98eabbef6248b0a0ed5e7e398e152f83f17560023a6d94a7f492b169fb
290
+ webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
291
+
292
+ RUBY VERSION
293
+ ruby 3.4.11
294
+
295
+ BUNDLED WITH
296
+ 4.0.23
@@ -0,0 +1,277 @@
1
+ require 'base64'
2
+ require 'digest'
3
+ require 'json'
4
+ require 'open3'
5
+ require 'optparse'
6
+ require 'rbconfig'
7
+ require 'socket'
8
+
9
+ module HaveAPI
10
+ # Opt-in measurements; deterministic performance assertions live in the specs.
11
+ class DocumentationBenchmark
12
+ ROOT = File.expand_path('../../..', __dir__)
13
+ FIXTURE = File.expand_path('../test_support/documentation_performance_fixture.rb', __dir__)
14
+ URL = 'http://example.test/benchmark/api/v1/'.freeze
15
+ METRICS = %i[wall_seconds cpu_seconds allocated_objects].freeze
16
+
17
+ class InvalidOutput < StandardError
18
+ attr_reader :output
19
+
20
+ def initialize(message, output)
21
+ @output = output
22
+ super(message)
23
+ end
24
+ end
25
+
26
+ def initialize(argv)
27
+ @options = { shape: 'both', size: 8, resources: 8, actions: 1, samples: 1, parameters: 2,
28
+ warmup: 1, trials: 3, iterations: 100, lanes: %w[context html], rust: false,
29
+ user: 'admin', source_root: ROOT }
30
+ parser.parse!(argv)
31
+ raise OptionParser::InvalidArgument, argv.join(' ') unless argv.empty?
32
+
33
+ validate_options
34
+ end
35
+
36
+ def run
37
+ @git_head = git('rev-parse', 'HEAD')
38
+ unless @git_head || @options[:source_revision]
39
+ raise ArgumentError, 'Source has no Git HEAD; supply --source-revision for an exported tree'
40
+ end
41
+
42
+ source_root = @options.fetch(:source_root)
43
+ %w[servers/ruby clients/ruby clients/rust].reverse_each do |component|
44
+ $:.unshift(File.join(source_root, component, 'lib'))
45
+ end
46
+ require 'haveapi'
47
+ require 'rack/mock'
48
+ require FIXTURE
49
+ expected = File.join(source_root, 'servers/ruby/lib/haveapi/documentation_context.rb')
50
+ actual = ClientExample::Context.instance_method(:with).source_location.first
51
+ raise "Context loaded from #{actual}, expected #{expected}" unless File.identical?(actual, expected)
52
+
53
+ if @options[:rust]
54
+ require 'haveapi/rust_client/documentation'
55
+ expected_rust = File.join(source_root, 'clients/rust/lib/haveapi/rust_client/documentation.rb')
56
+ actual_rust = RustClient::Documentation.instance_method(:render).source_location.first
57
+ unless File.identical?(actual_rust, expected_rust)
58
+ raise "Rust adapter loaded from #{actual_rust}, expected #{expected_rust}"
59
+ end
60
+ end
61
+
62
+ cases = shapes.map { |shape, dimensions| run_case(shape, dimensions) }
63
+ {
64
+ schema_version: 1, status: 'ok', runtime: { description: RUBY_DESCRIPTION, executable: RbConfig.ruby, engine: RUBY_ENGINE,
65
+ version: RUBY_VERSION, platform: RUBY_PLATFORM },
66
+ host: { hostname: Socket.gethostname, os: RbConfig::CONFIG['host_os'] },
67
+ source: source_identity, inputs: input_identity, options: @options, cases:
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def parser
74
+ OptionParser.new do |opts|
75
+ opts.banner = 'Usage: documentation.rb [options] (JSON on stdout)'
76
+ opts.on('--shape SHAPE', %w[resources actions both custom], 'Default: both') { |v| @options[:shape] = v }
77
+ opts.on('--size N', Integer, 'Resources or concentrated actions (default: 8)') { |v| @options[:size] = v }
78
+ opts.on('--resources N', Integer, 'Resource count for custom shape (default: 8)') { |v| @options[:resources] = v }
79
+ opts.on('--actions N', Integer, 'Actions per resource for custom shape (default: 1)') { |v| @options[:actions] = v }
80
+ opts.on('--samples N', Integer, 'Public samples per action (default: 1)') { |v| @options[:samples] = v }
81
+ opts.on('--parameters N', Integer, 'Additional string parameters per action (default: 2)') { |v| @options[:parameters] = v }
82
+ opts.on('--warmup N', Integer, 'Warmup batches per lane (default: 1)') { |v| @options[:warmup] = v }
83
+ opts.on('--trials N', Integer, 'Measured batches per lane (default: 3)') { |v| @options[:trials] = v }
84
+ opts.on('--iterations N', Integer, 'Derivations or adapter renders per batch (default: 100)') { |v| @options[:iterations] = v }
85
+ opts.on('--lanes LIST', Array, 'context,html,rust (default: context,html)') { |v| @options[:lanes] = v }
86
+ opts.on('--rust', 'Enable Rust profile and add the adapter lane; requires generator dependencies') do
87
+ @options[:rust] = true
88
+ @options[:lanes] |= ['rust']
89
+ end
90
+ opts.on('--user USER', %w[admin member], 'Synthetic documentation user (default: admin)') { |v| @options[:user] = v }
91
+ opts.on('--source-root PATH', 'Load implementation from another checkout using this harness/fixture') do |v|
92
+ @options[:source_root] = File.expand_path(v)
93
+ end
94
+ opts.on('--source-revision LABEL', 'Explicit source identity in addition to detected Git HEAD') { |v| @options[:source_revision] = v }
95
+ opts.on('-h', '--help', 'Show options without loading dependencies') do
96
+ puts opts
97
+ exit
98
+ end
99
+ end
100
+ end
101
+
102
+ def validate_options
103
+ %i[size resources actions samples parameters trials iterations].each do |name|
104
+ raise OptionParser::InvalidArgument, "#{name} must be positive" unless @options.fetch(name) > 0
105
+ end
106
+ raise OptionParser::InvalidArgument, 'warmup must be nonnegative' if @options.fetch(:warmup) < 0
107
+ unless @options[:lanes].any? && (@options[:lanes] - %w[context html rust]).empty?
108
+ raise OptionParser::InvalidArgument, 'lanes must select context,html,rust'
109
+ end
110
+
111
+ @options[:lanes].uniq!
112
+ @options[:rust] = true if @options[:lanes].include?('rust')
113
+ end
114
+
115
+ def shapes
116
+ selected = @options[:shape] == 'both' ? %w[resources actions] : [@options[:shape]]
117
+ selected.map do |shape|
118
+ resources, actions = case shape
119
+ when 'resources' then [@options[:size], 1]
120
+ when 'actions' then [1, @options[:size]]
121
+ else @options.values_at(:resources, :actions)
122
+ end
123
+ [shape, { resource_count: resources, actions_per_resource: actions,
124
+ samples_per_action: @options[:samples], parameters_per_action: @options[:parameters] }]
125
+ end
126
+ end
127
+
128
+ def run_case(shape, dimensions)
129
+ server, setup = measure { DocumentationPerformanceFixture.build(**dimensions, rust: @options[:rust]) }
130
+ request = Rack::MockRequest.new(server.app)
131
+ headers = { 'HTTP_AUTHORIZATION' => "Basic #{Base64.strict_encode64("#{@options[:user]}:fixture-only")}",
132
+ 'HTTP_HOST' => 'example.test', 'HTTP_ACCEPT_LANGUAGE' => 'en', 'HTTP_ACCEPT' => 'text/html' }
133
+ response = request.options(URL, **headers, 'HTTP_ACCEPT' => 'application/json')
134
+ raise "OPTIONS returned #{response.status}" unless response.status == 200
135
+
136
+ description = JSON.parse(response.body, symbolize_names: true).fetch(:response)
137
+ profile = server.documentation_client_profiles(1)[:rust]
138
+ parent = ClientExample::Context.new(version: 1, host: 'example.test',
139
+ base_url: 'http://example.test/benchmark/api/', version_url: URL,
140
+ description:, profile:)
141
+ renderers = ClientExample.clients.select { |client| client.available?(parent) }.map(&:id)
142
+ lanes = []
143
+ if @options[:lanes].include?('context')
144
+ replacement = Object.new.freeze
145
+ sample = { request: { value: 0, enabled: false }, response: { value: '' } }
146
+ { profile: { profile: replacement }, sample: { sample: } }.each do |name, changes|
147
+ lanes << trial_lane("context_#{name}", @options[:iterations], ->(child) { context_output(child, parent) }) do
148
+ child = nil
149
+ @options[:iterations].times { child = parent.with(**changes) }
150
+ child
151
+ end
152
+ end
153
+ end
154
+ if @options[:lanes].include?('html')
155
+ lanes << trial_lane('html', 1, method(:html_output)) { request.get(URL, **headers) }
156
+ end
157
+ lanes.concat(rust_lanes(parent, profile)) if @options[:lanes].include?('rust')
158
+ { shape:, dimensions:, profile_mode: @options[:rust] ? 'rust' : 'dynamic', renderers:,
159
+ description: body_identity(JSON.generate(description)), setup:, lanes: }
160
+ end
161
+
162
+ def rust_lanes(parent, profile)
163
+ raise 'Rust profile is unavailable' unless profile
164
+
165
+ resource = parent.description.fetch(:resources).fetch(:resource0)
166
+ action = resource.fetch(:actions).fetch(:action0)
167
+ context = parent.with(resource_path: ['resource0'], resource:, action_name: 'action0', action:,
168
+ sample: action.fetch(:examples).first)
169
+ auth = parent.description.fetch(:authentication).fetch(:basic)
170
+ %i[async blocking].flat_map do |mode|
171
+ %i[example auth].map do |kind|
172
+ trial_lane("rust_#{mode}_#{kind}", @options[:iterations], method(:rust_output)) do
173
+ result = nil
174
+ @options[:iterations].times do
175
+ result = profile.adapter.render(kind:, context:, mode:, auth_method: :basic, auth_description: auth)
176
+ end
177
+ result
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def measure
184
+ GC.start
185
+ wall = Process.clock_gettime(Process::CLOCK_MONOTONIC)
186
+ cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
187
+ allocated = GC.stat(:total_allocated_objects)
188
+ result = yield
189
+ allocations = GC.stat(:total_allocated_objects) - allocated
190
+ cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) - cpu
191
+ wall = Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall
192
+ [result, { wall_seconds: wall, cpu_seconds: cpu, allocated_objects: allocations }]
193
+ end
194
+
195
+ def trial_lane(name, operations, summarize, &work)
196
+ @options[:warmup].times { summarize.call(work.call) }
197
+ trials = Array.new(@options[:trials]) do |index|
198
+ result, metrics = measure(&work)
199
+ { trial: index + 1, **metrics, output: summarize.call(result) }
200
+ end
201
+ outputs = trials.map { |trial| trial[:output] }.uniq
202
+ raise "#{name} output changed between trials" unless outputs.length == 1
203
+
204
+ median = METRICS.to_h do |metric|
205
+ sorted = trials.map { |trial| trial.fetch(metric) }.sort
206
+ middle = sorted.length / 2
207
+ [metric, sorted.length.odd? ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2.0]
208
+ end
209
+ { name:, operations_per_trial: operations, trials:, median: }
210
+ end
211
+
212
+ def body_identity(body)
213
+ { body_bytes: body.bytesize, body_sha256: Digest::SHA256.hexdigest(body) }
214
+ end
215
+
216
+ def context_output(child, parent)
217
+ { status: 'ok', description_shared: child.description.equal?(parent.description), frozen: child.frozen?,
218
+ **body_identity(JSON.generate(description: child.description, sample: child.sample)) }
219
+ end
220
+
221
+ def html_output(response)
222
+ output = { status: response.status, **body_identity(response.body) }
223
+ unless response.status == 200 && response.body.include?('PUBLIC_SAMPLE_0') && response.body.include?('init-examples')
224
+ raise InvalidOutput.new('HTML request failed or missing sample panels', output)
225
+ end
226
+
227
+ output
228
+ end
229
+
230
+ def rust_output(result)
231
+ unless result[:status] == :code && !result[:code].empty?
232
+ raise InvalidOutput.new('Rust render failed', result)
233
+ end
234
+
235
+ { status: result[:status], **body_identity(result[:code]) }
236
+ end
237
+
238
+ def git(*)
239
+ output, status = Open3.capture2('git', '-C', @options[:source_root], *, err: File::NULL)
240
+ status.success? ? output.strip : nil
241
+ end
242
+
243
+ def source_identity
244
+ { root: @options[:source_root], supplied_revision: @options[:source_revision],
245
+ git_head: @git_head, git_status: git('status', '--porcelain', '--untracked-files=normal'),
246
+ git_diff_sha256: git('diff', 'HEAD', '--binary')&.then { |diff| Digest::SHA256.hexdigest(diff) },
247
+ loaded_source_sha256: $".grep(%r{/haveapi(?:/|\.rb)}).sort.to_h do |path|
248
+ [path, Digest::SHA256.file(path).hexdigest]
249
+ end }
250
+ end
251
+
252
+ def input_identity
253
+ paths = [__FILE__, FIXTURE, File.join(ROOT, 'flake.lock'),
254
+ ENV['BUNDLE_GEMFILE'] || File.join(ROOT, 'servers/ruby/Gemfile')]
255
+ paths << "#{paths.last}.lock"
256
+ paths << File.join(@options[:source_root], 'flake.lock')
257
+ if @options[:rust]
258
+ paths.concat(%w[clients/rust/Gemfile clients/rust/Gemfile.lock].map { |path| File.join(ROOT, path) })
259
+ end
260
+ { files_sha256: paths.select { |path| File.file?(path) }.to_h { |path| [path, Digest::SHA256.file(path).hexdigest] },
261
+ gem_paths: Gem.path,
262
+ gems: Gem.loaded_specs.sort.to_h.transform_values { |spec| spec.version.to_s } }
263
+ end
264
+ end
265
+ end
266
+
267
+ if $0 == __FILE__
268
+ begin
269
+ puts JSON.pretty_generate(HaveAPI::DocumentationBenchmark.new(ARGV).run)
270
+ rescue StandardError, LoadError => e
271
+ puts JSON.pretty_generate(schema_version: 1, status: 'failed',
272
+ error: { class: e.class.name, message: e.message,
273
+ output: e.respond_to?(:output) ? e.output : nil })
274
+ warn "#{e.class}: #{e.message}"
275
+ exit 1
276
+ end
277
+ end