haveapi 0.30.0 → 0.30.2
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/documentation.rb +277 -0
- data/benchmarks/endpoints.rb +730 -0
- data/benchmarks/performance_budgets.json +1354 -0
- data/benchmarks/performance_compare.rb +579 -0
- data/doc/client-examples.md +233 -24
- data/doc/performance.md +272 -0
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/documentation_clients.rb +158 -35
- data/lib/haveapi/documentation_context.rb +42 -12
- data/lib/haveapi/documentation_schema.rb +73 -0
- data/lib/haveapi/server.rb +5 -0
- 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/dynamic_js_spec.rb +1 -0
- data/spec/client_examples/profile_mount_spec.rb +215 -0
- data/spec/client_examples/profiles_spec.rb +202 -32
- data/spec/client_examples/structural_schema_spec.rb +48 -0
- data/spec/documentation/client_examples_spec.rb +10 -0
- data/spec/documentation/performance_spec.rb +79 -0
- data/spec/performance/comparator_spec.rb +342 -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 +279 -0
- data/spec/performance/json_activation_spec.rb +151 -0
- data/spec/performance/release_lineage_spec.rb +431 -0
- data/spec/performance/runner_spec.rb +476 -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 +26 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'haveapi/documentation_context'
|
|
3
|
+
require 'haveapi/documentation_schema'
|
|
3
4
|
|
|
4
5
|
module HaveAPI
|
|
5
6
|
class ClientExample
|
|
@@ -9,7 +10,7 @@ module HaveAPI
|
|
|
9
10
|
def initialize(language:, package_name:, package_version:, adapter:)
|
|
10
11
|
@language = language
|
|
11
12
|
@package_name = package_name.dup.freeze
|
|
12
|
-
@package_version = package_version
|
|
13
|
+
@package_version = package_version&.dup&.freeze
|
|
13
14
|
@adapter = adapter.freeze
|
|
14
15
|
freeze
|
|
15
16
|
end
|
|
@@ -20,64 +21,116 @@ module HaveAPI
|
|
|
20
21
|
|
|
21
22
|
KEYS = {
|
|
22
23
|
go: %i[import_path module_version description_path generator_version],
|
|
23
|
-
rust: %i[snapshot_path generator_config_path generator_version]
|
|
24
|
+
rust: %i[crate_name crate_version source snapshot_path generator_config_path generator_version]
|
|
24
25
|
}.freeze
|
|
25
26
|
|
|
26
27
|
def initialize
|
|
27
|
-
@
|
|
28
|
-
@
|
|
28
|
+
@registrations = {}
|
|
29
|
+
@profiles = {}.freeze
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def register(version, **languages)
|
|
32
|
-
raise Invalid, 'Documentation client profiles are already mounted' if @
|
|
33
|
+
raise Invalid, 'Documentation client profiles are already mounted' if @closed
|
|
33
34
|
raise Invalid, 'Unknown documentation client language' unless (languages.keys - KEYS.keys).empty?
|
|
34
35
|
|
|
35
|
-
selected = @
|
|
36
|
+
selected = @registrations.fetch(version.to_s, {})
|
|
37
|
+
additions = {}
|
|
36
38
|
languages.each do |language, config|
|
|
37
39
|
next if config.nil?
|
|
38
40
|
raise Invalid, 'Duplicate documentation client profile' if selected.has_key?(language)
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
additions[language] = load_profile(language, config)
|
|
41
43
|
end
|
|
44
|
+
@registrations[version.to_s] = selected.merge(additions)
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
def mount(versions)
|
|
45
|
-
|
|
48
|
+
raise Invalid, 'Documentation client profiles are already mounted' if @closed
|
|
49
|
+
|
|
50
|
+
@closed = true
|
|
51
|
+
unknown = @registrations.keys - Array(versions).map(&:to_s)
|
|
46
52
|
raise Invalid, 'Documentation client profile selects an unavailable API version' unless unknown.empty?
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
|
|
54
|
+
@registrations.each do |version, languages|
|
|
55
|
+
snapshot = languages.dig(:rust, :snapshot)
|
|
56
|
+
if snapshot && snapshot.fetch('api_version').to_s != version
|
|
57
|
+
raise Invalid, 'Documentation Rust snapshot API version mismatch'
|
|
58
|
+
end
|
|
49
59
|
end
|
|
60
|
+
end
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@
|
|
62
|
+
# Publish only after every adapter succeeds, using the complete mounted DSL.
|
|
63
|
+
def finalize(server)
|
|
64
|
+
raise Invalid, 'Documentation client profiles are not ready to finalize' unless @closed && !@finalized
|
|
65
|
+
|
|
66
|
+
completed = @registrations.to_h do |version, languages|
|
|
67
|
+
description = if languages.any? { |_language, config| !config[:profile] }
|
|
68
|
+
safely do
|
|
69
|
+
context = HaveAPI::Context.new(
|
|
70
|
+
server, version: server.versions.find { |v| v.to_s == version }, user: nil, endpoint: false, doc: true
|
|
71
|
+
)
|
|
72
|
+
StructuralSchema.copy(server.describe_version(context))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
profiles = languages.to_h do |language, config|
|
|
76
|
+
profile = config[:profile] || safely { build_profile(language, config, description, server, version) }
|
|
77
|
+
[language, profile]
|
|
78
|
+
end
|
|
79
|
+
[version, profiles.freeze]
|
|
80
|
+
end
|
|
81
|
+
@profiles = completed.freeze
|
|
82
|
+
@registrations = nil
|
|
83
|
+
@finalized = true
|
|
84
|
+
rescue StandardError
|
|
85
|
+
abort_mount
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def abort_mount
|
|
90
|
+
@profiles = {}.freeze
|
|
91
|
+
@registrations = nil
|
|
92
|
+
@closed = true
|
|
93
|
+
@finalized = true
|
|
54
94
|
end
|
|
55
95
|
|
|
56
96
|
def for_version(version)
|
|
57
|
-
@
|
|
97
|
+
@profiles.fetch(version.to_s, {}).dup.freeze
|
|
58
98
|
end
|
|
59
99
|
|
|
60
100
|
private
|
|
61
101
|
|
|
62
102
|
def validate_config(language, config)
|
|
63
|
-
unless config.is_a?(Hash) && config.keys
|
|
64
|
-
config.
|
|
103
|
+
unless config.is_a?(Hash) && (config.keys - KEYS.fetch(language)).empty? &&
|
|
104
|
+
config.all? { |key, value| %i[source module_version crate_version].include?(key) || nonempty_string?(value) }
|
|
65
105
|
raise Invalid, 'Invalid documentation client profile fields'
|
|
66
106
|
end
|
|
67
|
-
|
|
107
|
+
|
|
108
|
+
config = config.dup
|
|
109
|
+
config[:generator_version] = config.fetch(:generator_version, HaveAPI::VERSION)
|
|
110
|
+
return config if config[:generator_version].match?(/\A\d+\.\d+\.\d+(?:[-+][\w.-]+)?\z/)
|
|
68
111
|
|
|
69
112
|
raise Invalid, 'Invalid documentation generator version'
|
|
70
113
|
end
|
|
71
114
|
|
|
115
|
+
def nonempty_string?(value)
|
|
116
|
+
value.is_a?(String) && !value.strip.empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
72
119
|
def local_file(path)
|
|
73
120
|
raise Invalid, 'Documentation profiles require absolute local files' unless path.start_with?('/') && File.file?(path)
|
|
74
121
|
|
|
75
122
|
path
|
|
76
123
|
end
|
|
77
124
|
|
|
78
|
-
def load_profile(language, config
|
|
79
|
-
|
|
80
|
-
|
|
125
|
+
def load_profile(language, config)
|
|
126
|
+
safely do
|
|
127
|
+
config = validate_config(language, config)
|
|
128
|
+
language == :go ? load_go(config) : load_rust(config)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def safely
|
|
133
|
+
yield
|
|
81
134
|
rescue LoadError
|
|
82
135
|
raise Invalid, 'Configured documentation client generator is not installed'
|
|
83
136
|
rescue JSON::ParserError, SystemCallError
|
|
@@ -89,11 +142,11 @@ module HaveAPI
|
|
|
89
142
|
end
|
|
90
143
|
|
|
91
144
|
def load_go(config)
|
|
92
|
-
name = config
|
|
93
|
-
version = config
|
|
94
|
-
unless name.match?(%r{\A[a-zA-Z0-9][a-zA-Z0-9._~/-]*\z}) &&
|
|
145
|
+
name = config[:import_path]
|
|
146
|
+
version = config[:module_version]
|
|
147
|
+
unless nonempty_string?(name) && name.match?(%r{\A[a-zA-Z0-9][a-zA-Z0-9._~/-]*\z}) &&
|
|
95
148
|
name.split('/').none? { |part| part.empty? || %w[. ..].include?(part) } &&
|
|
96
|
-
version.match?(/\Av\d+\.\d+\.\d+(?:[-+][\w.-]+)?\z/)
|
|
149
|
+
(version.nil? || (version.is_a?(String) && version.match?(/\Av\d+\.\d+\.\d+(?:[-+][\w.-]+)?\z/)))
|
|
97
150
|
raise Invalid, 'Invalid Go documentation client package'
|
|
98
151
|
end
|
|
99
152
|
|
|
@@ -102,24 +155,94 @@ module HaveAPI
|
|
|
102
155
|
raise Invalid, 'Documentation Go generator version mismatch'
|
|
103
156
|
end
|
|
104
157
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
158
|
+
prepared = { name: name.dup.freeze, version: version&.dup&.freeze }.freeze
|
|
159
|
+
return prepared unless config.has_key?(:description_path)
|
|
160
|
+
|
|
161
|
+
description = JSON.parse(File.read(local_file(config[:description_path])))
|
|
162
|
+
{ profile: go_profile(prepared, description) }.freeze
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def go_profile(config, description)
|
|
166
|
+
adapter = HaveAPI::GoClient::Documentation.new(description:, import_path: config[:name], module_version: config[:version])
|
|
167
|
+
profile(:go, config, adapter)
|
|
108
168
|
end
|
|
109
169
|
|
|
110
|
-
def load_rust(config
|
|
170
|
+
def load_rust(config)
|
|
111
171
|
require 'haveapi/rust_client/documentation'
|
|
112
172
|
unless config.fetch(:generator_version) == HaveAPI::RustClient::VERSION
|
|
113
173
|
raise Invalid, 'Documentation Rust generator version mismatch'
|
|
114
174
|
end
|
|
115
175
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
legacy = config.has_key?(:generator_config_path)
|
|
177
|
+
if legacy && (config.has_key?(:crate_name) || config.has_key?(:crate_version))
|
|
178
|
+
raise Invalid, 'Conflicting Rust documentation client identities'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
documentation = HaveAPI::RustClient::Documentation
|
|
182
|
+
snapshot = JSON.parse(File.read(local_file(config[:snapshot_path]))) if config.has_key?(:snapshot_path)
|
|
183
|
+
if (!legacy || !snapshot) && !(documentation.respond_to?(:from_description) && documentation.respond_to?(:from_snapshot))
|
|
184
|
+
raise Invalid, 'Documentation Rust generator does not support automatic or inline profiles'
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
source = if config.has_key?(:source)
|
|
188
|
+
raise Invalid, 'Documentation Rust generator does not support source metadata' unless documentation.respond_to?(:validate_source)
|
|
189
|
+
|
|
190
|
+
documentation.validate_source(config[:source])
|
|
191
|
+
end
|
|
192
|
+
generator_config = Tomlrb.load_file(local_file(config[:generator_config_path])) if legacy
|
|
193
|
+
if legacy
|
|
194
|
+
HaveAPI::RustClient::Generator.validate_config(generator_config) unless snapshot
|
|
195
|
+
name = generator_config.fetch('crate_name')
|
|
196
|
+
version = generator_config.fetch('crate_version')
|
|
197
|
+
else
|
|
198
|
+
name = config[:crate_name]
|
|
199
|
+
version = config[:crate_version]
|
|
200
|
+
HaveAPI::RustClient::Model.validate_package(crate_name: name, crate_version: version)
|
|
201
|
+
end
|
|
202
|
+
prepared = { name: name.dup.freeze, version: version&.dup&.freeze,
|
|
203
|
+
snapshot:, generator_config:, source: }.freeze
|
|
204
|
+
return prepared unless snapshot
|
|
205
|
+
|
|
206
|
+
prepared.merge(profile: rust_profile(prepared, snapshot:)).freeze
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_profile(language, config, description, server, version)
|
|
210
|
+
return go_profile(config, description) if language == :go
|
|
211
|
+
|
|
212
|
+
arguments = { description:, api_version: version, protocol_version: HaveAPI::PROTOCOL_VERSION,
|
|
213
|
+
mount_path: server.root }
|
|
214
|
+
if config[:generator_config]
|
|
215
|
+
snapshot = HaveAPI::RustClient::Snapshot.from_description(
|
|
216
|
+
**arguments, source_url: "https://haveapi.invalid#{server.root}v#{version}/", profile: 'server-documentation'
|
|
217
|
+
)
|
|
218
|
+
return rust_profile(config, snapshot:)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
adapter = HaveAPI::RustClient::Documentation.from_description(
|
|
222
|
+
**arguments, crate_name: config[:name], crate_version: config[:version], source: config[:source]
|
|
223
|
+
)
|
|
224
|
+
profile(:rust, config, adapter)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def rust_profile(config, snapshot:)
|
|
228
|
+
adapter = if config[:generator_config]
|
|
229
|
+
arguments = { snapshot:, config: config[:generator_config] }
|
|
230
|
+
arguments[:source] = config[:source] if config[:source]
|
|
231
|
+
HaveAPI::RustClient::Documentation.new(**arguments)
|
|
232
|
+
else
|
|
233
|
+
HaveAPI::RustClient::Documentation.from_snapshot(
|
|
234
|
+
snapshot:, crate_name: config[:name], crate_version: config[:version], source: config[:source]
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
profile(:rust, config, adapter)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def profile(language, config, adapter)
|
|
241
|
+
unless adapter.respond_to?(:resolve) && adapter.respond_to?(:render)
|
|
242
|
+
raise Invalid, 'Documentation client generator has an incompatible adapter interface'
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
Profile.new(language:, package_name: config[:name], package_version: config[:version], adapter:)
|
|
123
246
|
end
|
|
124
247
|
end
|
|
125
248
|
end
|
|
@@ -10,26 +10,56 @@ module HaveAPI
|
|
|
10
10
|
def initialize(version:, host:, base_url:, version_url:, description:,
|
|
11
11
|
resource_path: [], resource: nil, action_name: nil, action: nil,
|
|
12
12
|
sample: nil, profile: nil)
|
|
13
|
-
@version = version
|
|
14
|
-
@host = host
|
|
15
|
-
@base_url = base_url
|
|
16
|
-
@version_url = version_url
|
|
17
|
-
@description =
|
|
18
|
-
@resource_path =
|
|
19
|
-
@resource =
|
|
20
|
-
@action_name = action_name
|
|
21
|
-
@action =
|
|
22
|
-
@sample =
|
|
23
|
-
@profile = profile
|
|
13
|
+
@version = normalize(:version, version)
|
|
14
|
+
@host = normalize(:host, host)
|
|
15
|
+
@base_url = normalize(:base_url, base_url)
|
|
16
|
+
@version_url = normalize(:version_url, version_url)
|
|
17
|
+
@description = normalize(:description, description)
|
|
18
|
+
@resource_path = normalize(:resource_path, resource_path)
|
|
19
|
+
@resource = normalize(:resource, resource)
|
|
20
|
+
@action_name = normalize(:action_name, action_name)
|
|
21
|
+
@action = normalize(:action, action)
|
|
22
|
+
@sample = normalize(:sample, sample)
|
|
23
|
+
@profile = normalize(:profile, profile)
|
|
24
24
|
freeze
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def with(**changes)
|
|
28
|
-
|
|
28
|
+
unless instance_of?(Context)
|
|
29
|
+
return self.class.new(**FIELDS.to_h { |field| [field, public_send(field)] }, **changes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unknown = changes.keys - FIELDS
|
|
33
|
+
unless unknown.empty?
|
|
34
|
+
raise ArgumentError, "unknown keyword#{'s' if unknown.length > 1}: #{unknown.map(&:inspect).join(', ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Unchanged fields are already owned snapshots; caller-supplied values
|
|
38
|
+
# must still cross the same ownership boundary as initial construction.
|
|
39
|
+
derived = dup
|
|
40
|
+
changes.each do |field, value|
|
|
41
|
+
derived.instance_variable_set("@#{field}", normalize(field, value))
|
|
42
|
+
end
|
|
43
|
+
derived.freeze
|
|
29
44
|
end
|
|
30
45
|
|
|
31
46
|
private
|
|
32
47
|
|
|
48
|
+
def normalize(field, value)
|
|
49
|
+
case field
|
|
50
|
+
when :version, :host, :base_url, :version_url
|
|
51
|
+
value.to_s.dup.freeze
|
|
52
|
+
when :resource_path
|
|
53
|
+
copy(value.map(&:to_s))
|
|
54
|
+
when :action_name
|
|
55
|
+
value.nil? ? nil : value.to_s.dup.freeze
|
|
56
|
+
when :profile
|
|
57
|
+
value
|
|
58
|
+
else
|
|
59
|
+
copy(value)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
33
63
|
def copy(value)
|
|
34
64
|
case value
|
|
35
65
|
when Hash
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module HaveAPI
|
|
4
|
+
class ClientExample
|
|
5
|
+
# Private naming/type input, never a request's authorized help or samples.
|
|
6
|
+
module StructuralSchema
|
|
7
|
+
BUILTIN_VALIDATORS = %w[present number include exclude format length confirm accept].freeze
|
|
8
|
+
|
|
9
|
+
def self.copy(description)
|
|
10
|
+
result = JSON.parse(JSON.generate(description))
|
|
11
|
+
resources(result['resources'])
|
|
12
|
+
result.fetch('authentication', {}).each_value do |provider|
|
|
13
|
+
prose(provider)
|
|
14
|
+
resources(provider['resources'])
|
|
15
|
+
end
|
|
16
|
+
freeze_data(result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.resources(map)
|
|
20
|
+
return unless map
|
|
21
|
+
|
|
22
|
+
map.each_value do |resource|
|
|
23
|
+
prose(resource)
|
|
24
|
+
resource.fetch('actions', {}).each_value do |action|
|
|
25
|
+
prose(action)
|
|
26
|
+
action.delete('examples')
|
|
27
|
+
parameters(action['input'])
|
|
28
|
+
parameters(action['output'])
|
|
29
|
+
action.fetch('meta', nil)&.each_value do |metadata|
|
|
30
|
+
next unless metadata
|
|
31
|
+
|
|
32
|
+
parameters(metadata['input'])
|
|
33
|
+
parameters(metadata['output'])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
resources(resource['resources'])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.parameters(group)
|
|
41
|
+
return unless group
|
|
42
|
+
|
|
43
|
+
group.fetch('parameters', {}).each_value do |parameter|
|
|
44
|
+
prose(parameter)
|
|
45
|
+
parameter.delete('default')
|
|
46
|
+
parameter.fetch('validators', {}).each do |name, validator|
|
|
47
|
+
prose(validator) if BUILTIN_VALIDATORS.include?(name) && validator.is_a?(Hash)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.prose(object)
|
|
53
|
+
%w[label description message].each { |key| object.delete(key) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.freeze_data(value)
|
|
57
|
+
case value
|
|
58
|
+
when Hash
|
|
59
|
+
value.each do |key, item|
|
|
60
|
+
freeze_data(key)
|
|
61
|
+
freeze_data(item)
|
|
62
|
+
end
|
|
63
|
+
when Array
|
|
64
|
+
value.each { |item| freeze_data(item) }
|
|
65
|
+
end
|
|
66
|
+
value.freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private_class_method :resources, :parameters, :prose, :freeze_data
|
|
70
|
+
end
|
|
71
|
+
private_constant :StructuralSchema
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/haveapi/server.rb
CHANGED
|
@@ -622,6 +622,11 @@ module HaveAPI
|
|
|
622
622
|
end
|
|
623
623
|
|
|
624
624
|
call_hooks_for(:post_mount, args: [self, @sinatra])
|
|
625
|
+
@documentation_clients.finalize(self)
|
|
626
|
+
rescue StandardError
|
|
627
|
+
@documentation_clients.abort_mount
|
|
628
|
+
@sinatra = nil
|
|
629
|
+
raise
|
|
625
630
|
end
|
|
626
631
|
|
|
627
632
|
def mount_version(prefix, v)
|
data/lib/haveapi/version.rb
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<li><a href="https://github.com/vpsfreecz/haveapi/tree/master/clients/js" target="_blank" rel="noopener">JavaScript and TypeScript (callback and async)</a></li>
|
|
21
21
|
<li><a href="https://github.com/vpsfreecz/haveapi/tree/master/clients/python" target="_blank" rel="noopener">Python (sync and async)</a></li>
|
|
22
22
|
<% settings.api_server.documentation_client_profiles(@v).each do |language, profile| %>
|
|
23
|
-
<li><a href="https://github.com/vpsfreecz/haveapi/tree/master/clients/<%= escape_html(language) %>" target="_blank" rel="noopener"><%= language == :go ? 'Go' : 'Rust (async and blocking)' %></a>: <code><%= escape_html(profile.package_name) %></code
|
|
23
|
+
<li><a href="https://github.com/vpsfreecz/haveapi/tree/master/clients/<%= escape_html(language) %>" target="_blank" rel="noopener"><%= language == :go ? 'Go' : 'Rust (async and blocking)' %></a>: <code><%= escape_html(profile.package_name) %></code><% if profile.package_version %> <%= escape_html(profile.package_version) %><% end %></li>
|
|
24
24
|
<% end %>
|
|
25
25
|
<li><a href="https://github.com/vpsfreecz/haveapi/tree/master/clients/php" target="_blank" rel="noopener">PHP</a></li>
|
|
26
26
|
<li>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<p>
|
|
33
33
|
These examples target API v<%= escape_html(@v) %>. The framework and dynamic
|
|
34
34
|
clients share HaveAPI package versions. Generated Go and Rust SDKs use their
|
|
35
|
-
own package versions
|
|
35
|
+
own package versions. Their configured SDKs are expected to match this API.
|
|
36
36
|
</p>
|
|
37
37
|
|
|
38
38
|
<h2>Initialization</h2>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe HaveAPI::ClientExample::Context, '#with' do
|
|
4
|
+
let(:sample) { { request: { payload: { 'items' => [0, false, '', nil] } } } }
|
|
5
|
+
let(:profile) { Object.new.freeze }
|
|
6
|
+
|
|
7
|
+
def parent_context(resource_count)
|
|
8
|
+
action = { input: { parameters: { payload: { type: 'Custom' } } }, examples: [sample] }
|
|
9
|
+
description = {
|
|
10
|
+
resources: resource_count.times.to_h do |index|
|
|
11
|
+
["resource#{index}", { description: "Resource #{index}", actions: { 'show' => action } }]
|
|
12
|
+
end
|
|
13
|
+
}
|
|
14
|
+
described_class.new(version: '1', host: 'example.test', base_url: 'http://example.test/benchmark/api/',
|
|
15
|
+
version_url: 'http://example.test/benchmark/api/v1/', description:,
|
|
16
|
+
resource_path: ['resource0'], resource: description[:resources]['resource0'],
|
|
17
|
+
action_name: 'show', action:, sample:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def allocated_derivations(parent, changes, count: 40)
|
|
21
|
+
# Taking the minimum of repeated batches avoids incidental VM bookkeeping
|
|
22
|
+
# spikes. Expectations and fixture construction stay outside this loop.
|
|
23
|
+
Array.new(3) do
|
|
24
|
+
before = GC.stat(:total_allocated_objects)
|
|
25
|
+
count.times { parent.with(**changes) }
|
|
26
|
+
GC.stat(:total_allocated_objects) - before
|
|
27
|
+
end.min
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'shares every unchanged field and owns a different snapshot for a new request' do
|
|
31
|
+
parent = parent_context(8)
|
|
32
|
+
other_request = parent_context(8)
|
|
33
|
+
children = [parent.with(profile:), parent.with(sample:)]
|
|
34
|
+
fields = %i[version host base_url version_url description resource_path resource action_name action sample profile]
|
|
35
|
+
|
|
36
|
+
children.each_with_index do |child, index|
|
|
37
|
+
changed = index == 0 ? :profile : :sample
|
|
38
|
+
(fields - [changed]).each do |field|
|
|
39
|
+
expect(child.public_send(field)).to equal(parent.public_send(field))
|
|
40
|
+
end
|
|
41
|
+
expect(child).to be_frozen
|
|
42
|
+
end
|
|
43
|
+
expect(other_request.description).to eq(parent.description)
|
|
44
|
+
expect(other_request.description).not_to equal(parent.description)
|
|
45
|
+
expect(other_request.description[:resources]).not_to equal(parent.description[:resources])
|
|
46
|
+
replacement = parent.with(description: parent.description)
|
|
47
|
+
expect(replacement.description).to eq(parent.description)
|
|
48
|
+
expect(replacement.description).not_to equal(parent.description)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'keeps allocations independent of unrelated description size for small changes' do
|
|
52
|
+
small = parent_context(8)
|
|
53
|
+
large = parent_context(128)
|
|
54
|
+
changes = [{ profile: }, { sample: }]
|
|
55
|
+
[small, large].each do |parent|
|
|
56
|
+
changes.each { |change| 80.times { parent.with(**change) } }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
changes.each do |change|
|
|
60
|
+
small_allocations = allocated_derivations(small, change)
|
|
61
|
+
large_allocations = allocated_derivations(large, change)
|
|
62
|
+
# CRuby 3.4 allocates 200/200 objects for profile changes and 560/560 for
|
|
63
|
+
# samples at these sizes. The original constructor path grows from 12,560
|
|
64
|
+
# to 151,760. Allow 32 extra objects per derivation for VM bookkeeping;
|
|
65
|
+
# identity assertions independently forbid whole-description copies.
|
|
66
|
+
expect(large_allocations).to be <= small_allocations + (40 * 32)
|
|
67
|
+
[small, large].each do |parent|
|
|
68
|
+
expect(parent.with(**change).description).to equal(parent.description)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|