mockserver-client 7.6.0 → 9.0.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/README.md +69 -3
- data/lib/mockserver/binary_launcher.rb +48 -3
- data/lib/mockserver/client.rb +197 -5
- data/lib/mockserver/models.rb +857 -60
- data/lib/mockserver/version.rb +1 -1
- metadata +2 -2
data/lib/mockserver/models.rb
CHANGED
|
@@ -131,6 +131,44 @@ module MockServer
|
|
|
131
131
|
hash.reject { |_k, v| v.nil? }
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# @api private
|
|
135
|
+
def self.symbol_keys?(value)
|
|
136
|
+
case value
|
|
137
|
+
when Hash then value.any? { |k, v| k.is_a?(Symbol) || symbol_keys?(v) }
|
|
138
|
+
when Array then value.any? { |v| symbol_keys?(v) }
|
|
139
|
+
else false
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @api private
|
|
144
|
+
# Returns a copy of +value+ with every Symbol hash key, at any depth, turned
|
|
145
|
+
# into a String; values and other keys are unchanged.
|
|
146
|
+
def self.stringify_keys(value)
|
|
147
|
+
case value
|
|
148
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.is_a?(Symbol) ? k.to_s : k] = stringify_keys(v) }
|
|
149
|
+
when Array then value.map { |v| stringify_keys(v) }
|
|
150
|
+
else value
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @api private
|
|
155
|
+
# Prepended to every model's from_hash. A hash literal written with JSON-style
|
|
156
|
+
# keys ({ "httpRequest": {...} }) has Symbol keys in Ruby, while the models read
|
|
157
|
+
# the String keys JSON.parse produces, so normalise before reading. Only the
|
|
158
|
+
# outermost call scans: nested calls receive parts of the normalised hash.
|
|
159
|
+
module SymbolKeyTolerantFromHash
|
|
160
|
+
def from_hash(data)
|
|
161
|
+
return super if Thread.current[:mockserver_from_hash_normalised]
|
|
162
|
+
|
|
163
|
+
Thread.current[:mockserver_from_hash_normalised] = true
|
|
164
|
+
begin
|
|
165
|
+
super(MockServer.symbol_keys?(data) ? MockServer.stringify_keys(data) : data)
|
|
166
|
+
ensure
|
|
167
|
+
Thread.current[:mockserver_from_hash_normalised] = false
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
134
172
|
# @api private
|
|
135
173
|
# Coerce a class-callback value into an {HttpClassCallback}. Accepts:
|
|
136
174
|
# * +nil+ -> +nil+
|
|
@@ -199,12 +237,62 @@ module MockServer
|
|
|
199
237
|
end
|
|
200
238
|
|
|
201
239
|
# @api private
|
|
202
|
-
|
|
240
|
+
# With a key_match_style (MATCHING_KEY or SUB_SET) the collection is written in
|
|
241
|
+
# the object form, the only form that carries keyMatchStyle.
|
|
242
|
+
def self.serialize_key_multi_values(items, key_match_style = nil)
|
|
243
|
+
if key_match_style
|
|
244
|
+
map = { 'keyMatchStyle' => key_match_style }
|
|
245
|
+
(items || []).each { |item| (map[item.name] ||= []).concat(Array(item.values)) }
|
|
246
|
+
return map
|
|
247
|
+
end
|
|
203
248
|
return nil if items.nil?
|
|
204
249
|
|
|
205
250
|
items.map(&:to_h)
|
|
206
251
|
end
|
|
207
252
|
|
|
253
|
+
# @api private
|
|
254
|
+
# An httpRequest holding if is a conditional matcher, one naming an OpenAPI
|
|
255
|
+
# spec (specUrlOrPayload) is an OpenAPI matcher; anything else is an HTTP (or
|
|
256
|
+
# DNS) request matcher.
|
|
257
|
+
def self.deserialize_request_definition(data)
|
|
258
|
+
return ConditionalRequestDefinition.from_hash(data) if data.is_a?(Hash) && data.key?('if')
|
|
259
|
+
return OpenAPIDefinition.from_hash(data) if data.is_a?(Hash) && data.key?('specUrlOrPayload')
|
|
260
|
+
|
|
261
|
+
HttpRequest.from_hash(data)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @api private
|
|
265
|
+
def self.key_match_style(data)
|
|
266
|
+
data['keyMatchStyle'] || data[:keyMatchStyle] if data.is_a?(Hash)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# @api private
|
|
270
|
+
# Accept a keyToMultiValue collection as the server's JSON does, so headers,
|
|
271
|
+
# query parameters and trailers can be given as { name => values }, as
|
|
272
|
+
# [{ name:, values: }] or as KeyToMultiValue objects.
|
|
273
|
+
def self.coerce_key_multi_values(value)
|
|
274
|
+
return value if value.nil?
|
|
275
|
+
return deserialize_key_multi_values(stringify_keys(value)) if value.is_a?(Hash)
|
|
276
|
+
raise TypeError, "Expected a Hash or an Array of KeyToMultiValue, got #{value.class.name}" unless value.is_a?(Array)
|
|
277
|
+
|
|
278
|
+
value.map { |item| item.is_a?(Hash) ? KeyToMultiValue.from_hash(stringify_keys(item)) : item }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# @api private
|
|
282
|
+
# Cookies as { name => value }, [{ name:, value: }] or KeyToMultiValue objects.
|
|
283
|
+
def self.coerce_cookies(value)
|
|
284
|
+
return value if value.nil?
|
|
285
|
+
return deserialize_cookies(stringify_keys(value)) if value.is_a?(Hash)
|
|
286
|
+
raise TypeError, "Expected a Hash or an Array of KeyToMultiValue, got #{value.class.name}" unless value.is_a?(Array)
|
|
287
|
+
|
|
288
|
+
value.map do |item|
|
|
289
|
+
next item unless item.is_a?(Hash)
|
|
290
|
+
|
|
291
|
+
item = stringify_keys(item)
|
|
292
|
+
KeyToMultiValue.new(name: item.fetch('name', ''), values: item.key?('value') ? [item['value']] : item.fetch('values', []))
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
208
296
|
# @api private
|
|
209
297
|
# Serialise a keyToMultiValue collection in the { name => [values] } object form
|
|
210
298
|
# (rather than the [{name, values}] array form). MockServer accepts both wire
|
|
@@ -222,7 +310,8 @@ module MockServer
|
|
|
222
310
|
return nil if data.nil?
|
|
223
311
|
|
|
224
312
|
if data.is_a?(Hash)
|
|
225
|
-
return data.
|
|
313
|
+
return data.reject { |k, _v| k == 'keyMatchStyle' }
|
|
314
|
+
.map { |k, v| KeyToMultiValue.new(name: k, values: v.is_a?(Array) ? v : [v]) }
|
|
226
315
|
end
|
|
227
316
|
|
|
228
317
|
data.map do |item|
|
|
@@ -304,19 +393,25 @@ module MockServer
|
|
|
304
393
|
end
|
|
305
394
|
|
|
306
395
|
class Delay
|
|
307
|
-
|
|
396
|
+
# template is rendered against the request and read as milliseconds (falling
|
|
397
|
+
# back to time_unit/value); template_type is VELOCITY or MUSTACHE.
|
|
398
|
+
attr_accessor :time_unit, :value, :distribution, :template, :template_type
|
|
308
399
|
|
|
309
|
-
def initialize(time_unit: 'MILLISECONDS', value: 0, distribution: nil)
|
|
400
|
+
def initialize(time_unit: 'MILLISECONDS', value: 0, distribution: nil, template: nil, template_type: nil)
|
|
310
401
|
@time_unit = time_unit
|
|
311
402
|
@value = value
|
|
312
403
|
@distribution = distribution
|
|
404
|
+
@template = template
|
|
405
|
+
@template_type = template_type
|
|
313
406
|
end
|
|
314
407
|
|
|
315
408
|
def to_h
|
|
316
409
|
MockServer.strip_none({
|
|
317
410
|
'timeUnit' => @time_unit,
|
|
318
411
|
'value' => @value,
|
|
319
|
-
'distribution' => @distribution&.to_h
|
|
412
|
+
'distribution' => @distribution&.to_h,
|
|
413
|
+
'template' => @template,
|
|
414
|
+
'templateType' => @template_type
|
|
320
415
|
})
|
|
321
416
|
end
|
|
322
417
|
|
|
@@ -325,9 +420,11 @@ module MockServer
|
|
|
325
420
|
|
|
326
421
|
dist_data = data['distribution']
|
|
327
422
|
new(
|
|
328
|
-
time_unit:
|
|
329
|
-
value:
|
|
330
|
-
distribution:
|
|
423
|
+
time_unit: data.fetch('timeUnit', 'MILLISECONDS'),
|
|
424
|
+
value: data.fetch('value', 0),
|
|
425
|
+
distribution: dist_data ? DelayDistribution.from_hash(dist_data) : nil,
|
|
426
|
+
template: data['template'],
|
|
427
|
+
template_type: data['templateType']
|
|
331
428
|
)
|
|
332
429
|
end
|
|
333
430
|
end
|
|
@@ -786,46 +883,85 @@ module MockServer
|
|
|
786
883
|
# protocol pins the transport the matcher requires
|
|
787
884
|
# (org.mockserver.model.Protocol: HTTP_1_1 / HTTP_2 / HTTP_3 / SOCKS / ...),
|
|
788
885
|
# round-tripping the httpRequest +protocol+ wire key.
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
886
|
+
# headers, query_string_parameters, path_parameters and cookies also accept a
|
|
887
|
+
# Hash ({ name => values }) or an Array of Hashes, as in the server's JSON.
|
|
888
|
+
# *_key_match_style is SUB_SET (the server's default: one value of a key
|
|
889
|
+
# must match) or MATCHING_KEY (every value must); a Hash with a
|
|
890
|
+
# 'keyMatchStyle' key sets it too.
|
|
891
|
+
attr_accessor :method, :path, :body, :secure, :protocol, :keep_alive, :respond_before_body,
|
|
892
|
+
:socket_address, :jwt, :dns_name, :dns_type, :dns_class,
|
|
893
|
+
:headers_key_match_style, :query_string_parameters_key_match_style,
|
|
894
|
+
:path_parameters_key_match_style
|
|
895
|
+
attr_reader :query_string_parameters, :headers, :cookies, :path_parameters
|
|
793
896
|
|
|
794
897
|
def initialize(method: nil, path: nil, query_string_parameters: nil, headers: nil,
|
|
795
898
|
cookies: nil, body: nil, secure: nil, protocol: nil, keep_alive: nil,
|
|
796
899
|
respond_before_body: nil, path_parameters: nil, socket_address: nil,
|
|
797
|
-
jwt: nil, dns_name: nil, dns_type: nil, dns_class: nil
|
|
900
|
+
jwt: nil, dns_name: nil, dns_type: nil, dns_class: nil,
|
|
901
|
+
headers_key_match_style: nil, query_string_parameters_key_match_style: nil,
|
|
902
|
+
path_parameters_key_match_style: nil)
|
|
798
903
|
@method = method
|
|
799
904
|
@path = path
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
905
|
+
self.query_string_parameters = query_string_parameters
|
|
906
|
+
self.headers = headers
|
|
907
|
+
self.cookies = cookies
|
|
803
908
|
@body = body
|
|
804
909
|
@secure = secure
|
|
805
910
|
@protocol = protocol
|
|
806
911
|
@keep_alive = keep_alive
|
|
807
912
|
@respond_before_body = respond_before_body
|
|
808
|
-
|
|
913
|
+
self.path_parameters = path_parameters
|
|
809
914
|
@socket_address = socket_address
|
|
810
915
|
@jwt = jwt
|
|
811
916
|
@dns_name = dns_name
|
|
812
917
|
@dns_type = dns_type
|
|
813
918
|
@dns_class = dns_class
|
|
919
|
+
@headers_key_match_style = headers_key_match_style if headers_key_match_style
|
|
920
|
+
@query_string_parameters_key_match_style = query_string_parameters_key_match_style if query_string_parameters_key_match_style
|
|
921
|
+
@path_parameters_key_match_style = path_parameters_key_match_style if path_parameters_key_match_style
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def headers=(value)
|
|
925
|
+
style = MockServer.key_match_style(value)
|
|
926
|
+
@headers_key_match_style = style if style
|
|
927
|
+
@headers = MockServer.coerce_key_multi_values(value)
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def query_string_parameters=(value)
|
|
931
|
+
style = MockServer.key_match_style(value)
|
|
932
|
+
@query_string_parameters_key_match_style = style if style
|
|
933
|
+
@query_string_parameters = MockServer.coerce_key_multi_values(value)
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
def path_parameters=(value)
|
|
937
|
+
style = MockServer.key_match_style(value)
|
|
938
|
+
@path_parameters_key_match_style = style if style
|
|
939
|
+
@path_parameters = MockServer.coerce_key_multi_values(value)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def cookies=(value)
|
|
943
|
+
@cookies = MockServer.coerce_cookies(value)
|
|
814
944
|
end
|
|
815
945
|
|
|
816
946
|
def to_h
|
|
947
|
+
path_parameters = if @path_parameters_key_match_style
|
|
948
|
+
MockServer.serialize_key_multi_values(@path_parameters, @path_parameters_key_match_style)
|
|
949
|
+
else
|
|
950
|
+
MockServer.serialize_key_multi_values_object(@path_parameters)
|
|
951
|
+
end
|
|
817
952
|
MockServer.strip_none({
|
|
818
953
|
'method' => @method,
|
|
819
954
|
'path' => @path,
|
|
820
|
-
'queryStringParameters' => MockServer.serialize_key_multi_values(@query_string_parameters
|
|
821
|
-
|
|
955
|
+
'queryStringParameters' => MockServer.serialize_key_multi_values(@query_string_parameters,
|
|
956
|
+
@query_string_parameters_key_match_style),
|
|
957
|
+
'headers' => MockServer.serialize_key_multi_values(@headers, @headers_key_match_style),
|
|
822
958
|
'cookies' => MockServer.serialize_cookies(@cookies),
|
|
823
959
|
'body' => MockServer.serialize_body(@body),
|
|
824
960
|
'secure' => @secure,
|
|
825
961
|
'protocol' => @protocol,
|
|
826
962
|
'keepAlive' => @keep_alive,
|
|
827
963
|
'respondBeforeBody' => @respond_before_body,
|
|
828
|
-
'pathParameters' =>
|
|
964
|
+
'pathParameters' => path_parameters,
|
|
829
965
|
'socketAddress' => @socket_address&.to_h,
|
|
830
966
|
'jwt' => @jwt&.to_h,
|
|
831
967
|
'dnsName' => @dns_name,
|
|
@@ -853,7 +989,10 @@ module MockServer
|
|
|
853
989
|
jwt: Jwt.from_hash(data['jwt']),
|
|
854
990
|
dns_name: data['dnsName'],
|
|
855
991
|
dns_type: data['dnsType'],
|
|
856
|
-
dns_class: data['dnsClass']
|
|
992
|
+
dns_class: data['dnsClass'],
|
|
993
|
+
headers_key_match_style: MockServer.key_match_style(data['headers']),
|
|
994
|
+
query_string_parameters_key_match_style: MockServer.key_match_style(data['queryStringParameters']),
|
|
995
|
+
path_parameters_key_match_style: MockServer.key_match_style(data['pathParameters'])
|
|
857
996
|
)
|
|
858
997
|
end
|
|
859
998
|
|
|
@@ -968,23 +1107,74 @@ module MockServer
|
|
|
968
1107
|
end
|
|
969
1108
|
end
|
|
970
1109
|
|
|
1110
|
+
# Serves +fail_response+ for the first +fail_times+ matches, then the real
|
|
1111
|
+
# response; +idempotency_header+ keys an independent window per header value.
|
|
1112
|
+
class RecoverAfter
|
|
1113
|
+
attr_accessor :fail_times, :fail_response, :idempotency_header
|
|
1114
|
+
|
|
1115
|
+
def initialize(fail_times: nil, fail_response: nil, idempotency_header: nil)
|
|
1116
|
+
@fail_times = fail_times
|
|
1117
|
+
@fail_response = fail_response
|
|
1118
|
+
@idempotency_header = idempotency_header
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def to_h
|
|
1122
|
+
MockServer.strip_none({
|
|
1123
|
+
'failTimes' => @fail_times,
|
|
1124
|
+
'failResponse' => @fail_response&.to_h,
|
|
1125
|
+
'idempotencyHeader' => @idempotency_header
|
|
1126
|
+
})
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def self.from_hash(data)
|
|
1130
|
+
return nil if data.nil?
|
|
1131
|
+
|
|
1132
|
+
new(
|
|
1133
|
+
fail_times: data['failTimes'],
|
|
1134
|
+
fail_response: HttpResponse.from_hash(data['failResponse']),
|
|
1135
|
+
idempotency_header: data['idempotencyHeader']
|
|
1136
|
+
)
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
|
|
971
1140
|
class HttpResponse
|
|
972
1141
|
# trailers carries HTTP trailing headers (org.mockserver.model.Headers on the
|
|
973
1142
|
# response's +trailers+ slot), a keyToMultiValue collection like +headers+.
|
|
974
|
-
|
|
975
|
-
|
|
1143
|
+
# status_code_range serves a status from a range such as "2xx";
|
|
1144
|
+
# generate_from_schema is a JSON schema the body is generated from.
|
|
1145
|
+
# headers, trailers and cookies also accept a Hash or an Array of Hashes,
|
|
1146
|
+
# as in the server's JSON (see HttpRequest).
|
|
1147
|
+
attr_accessor :status_code, :reason_phrase, :body, :delay, :connection_options, :primary,
|
|
1148
|
+
:status_code_range, :generate_from_schema, :recover_after
|
|
1149
|
+
attr_reader :headers, :cookies, :trailers
|
|
976
1150
|
|
|
977
1151
|
def initialize(status_code: nil, reason_phrase: nil, headers: nil, cookies: nil,
|
|
978
|
-
body: nil, delay: nil, connection_options: nil, trailers: nil, primary: nil
|
|
1152
|
+
body: nil, delay: nil, connection_options: nil, trailers: nil, primary: nil,
|
|
1153
|
+
status_code_range: nil, generate_from_schema: nil, recover_after: nil)
|
|
979
1154
|
@status_code = status_code
|
|
980
1155
|
@reason_phrase = reason_phrase
|
|
981
|
-
|
|
982
|
-
|
|
1156
|
+
self.headers = headers
|
|
1157
|
+
self.cookies = cookies
|
|
983
1158
|
@body = body
|
|
984
1159
|
@delay = delay
|
|
985
1160
|
@connection_options = connection_options
|
|
986
|
-
|
|
1161
|
+
self.trailers = trailers
|
|
987
1162
|
@primary = primary
|
|
1163
|
+
@status_code_range = status_code_range
|
|
1164
|
+
@generate_from_schema = generate_from_schema
|
|
1165
|
+
@recover_after = recover_after
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def headers=(value)
|
|
1169
|
+
@headers = MockServer.coerce_key_multi_values(value)
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def trailers=(value)
|
|
1173
|
+
@trailers = MockServer.coerce_key_multi_values(value)
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def cookies=(value)
|
|
1177
|
+
@cookies = MockServer.coerce_cookies(value)
|
|
988
1178
|
end
|
|
989
1179
|
|
|
990
1180
|
def to_h
|
|
@@ -997,7 +1187,10 @@ module MockServer
|
|
|
997
1187
|
'delay' => @delay&.to_h,
|
|
998
1188
|
'connectionOptions' => @connection_options&.to_h,
|
|
999
1189
|
'trailers' => MockServer.serialize_key_multi_values(@trailers),
|
|
1000
|
-
'primary' => @primary
|
|
1190
|
+
'primary' => @primary,
|
|
1191
|
+
'statusCodeRange' => @status_code_range,
|
|
1192
|
+
'generateFromSchema' => @generate_from_schema,
|
|
1193
|
+
'recoverAfter' => @recover_after&.to_h
|
|
1001
1194
|
})
|
|
1002
1195
|
end
|
|
1003
1196
|
|
|
@@ -1013,7 +1206,10 @@ module MockServer
|
|
|
1013
1206
|
delay: Delay.from_hash(data['delay']),
|
|
1014
1207
|
connection_options: ConnectionOptions.from_hash(data['connectionOptions']),
|
|
1015
1208
|
trailers: MockServer.deserialize_key_multi_values(data['trailers']),
|
|
1016
|
-
primary: data['primary']
|
|
1209
|
+
primary: data['primary'],
|
|
1210
|
+
status_code_range: data['statusCodeRange'],
|
|
1211
|
+
generate_from_schema: data['generateFromSchema'],
|
|
1212
|
+
recover_after: RecoverAfter.from_hash(data['recoverAfter'])
|
|
1017
1213
|
)
|
|
1018
1214
|
end
|
|
1019
1215
|
|
|
@@ -1068,6 +1264,21 @@ module MockServer
|
|
|
1068
1264
|
@reason_phrase = reason_phrase
|
|
1069
1265
|
self
|
|
1070
1266
|
end
|
|
1267
|
+
|
|
1268
|
+
def with_status_code_range(status_code_range)
|
|
1269
|
+
@status_code_range = status_code_range
|
|
1270
|
+
self
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
def with_generate_from_schema(schema)
|
|
1274
|
+
@generate_from_schema = schema
|
|
1275
|
+
self
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
def with_recover_after(recover_after)
|
|
1279
|
+
@recover_after = recover_after
|
|
1280
|
+
self
|
|
1281
|
+
end
|
|
1071
1282
|
end
|
|
1072
1283
|
|
|
1073
1284
|
class HttpForward
|
|
@@ -1109,23 +1320,31 @@ module MockServer
|
|
|
1109
1320
|
end
|
|
1110
1321
|
|
|
1111
1322
|
class HttpTemplate
|
|
1112
|
-
|
|
1323
|
+
# response_override (an {HttpResponse}) is applied over the rendered output;
|
|
1324
|
+
# response_modifier is the server's responseModifier object, as a Hash.
|
|
1325
|
+
attr_accessor :template_type, :template, :template_file, :delay, :primary,
|
|
1326
|
+
:response_override, :response_modifier
|
|
1113
1327
|
|
|
1114
|
-
def initialize(template_type: 'JAVASCRIPT', template: nil, template_file: nil, delay: nil, primary: nil
|
|
1328
|
+
def initialize(template_type: 'JAVASCRIPT', template: nil, template_file: nil, delay: nil, primary: nil,
|
|
1329
|
+
response_override: nil, response_modifier: nil)
|
|
1115
1330
|
@template_type = template_type
|
|
1116
1331
|
@template = template
|
|
1117
1332
|
@template_file = template_file
|
|
1118
1333
|
@delay = delay
|
|
1119
1334
|
@primary = primary
|
|
1335
|
+
@response_override = response_override
|
|
1336
|
+
@response_modifier = response_modifier
|
|
1120
1337
|
end
|
|
1121
1338
|
|
|
1122
1339
|
def to_h
|
|
1123
1340
|
MockServer.strip_none({
|
|
1124
|
-
'templateType'
|
|
1125
|
-
'template'
|
|
1126
|
-
'templateFile'
|
|
1127
|
-
'delay'
|
|
1128
|
-
'primary'
|
|
1341
|
+
'templateType' => @template_type,
|
|
1342
|
+
'template' => @template,
|
|
1343
|
+
'templateFile' => @template_file,
|
|
1344
|
+
'delay' => @delay&.to_h,
|
|
1345
|
+
'primary' => @primary,
|
|
1346
|
+
'responseOverride' => @response_override&.to_h,
|
|
1347
|
+
'responseModifier' => @response_modifier
|
|
1129
1348
|
})
|
|
1130
1349
|
end
|
|
1131
1350
|
|
|
@@ -1137,7 +1356,9 @@ module MockServer
|
|
|
1137
1356
|
template: data['template'],
|
|
1138
1357
|
template_file: data['templateFile'],
|
|
1139
1358
|
delay: Delay.from_hash(data['delay']),
|
|
1140
|
-
primary: data['primary']
|
|
1359
|
+
primary: data['primary'],
|
|
1360
|
+
response_override: HttpResponse.from_hash(data['responseOverride']),
|
|
1361
|
+
response_modifier: data['responseModifier']
|
|
1141
1362
|
)
|
|
1142
1363
|
end
|
|
1143
1364
|
|
|
@@ -1149,6 +1370,16 @@ module MockServer
|
|
|
1149
1370
|
@template_file = template_file
|
|
1150
1371
|
self
|
|
1151
1372
|
end
|
|
1373
|
+
|
|
1374
|
+
def with_response_override(response_override)
|
|
1375
|
+
@response_override = response_override
|
|
1376
|
+
self
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
def with_response_modifier(response_modifier)
|
|
1380
|
+
@response_modifier = response_modifier
|
|
1381
|
+
self
|
|
1382
|
+
end
|
|
1152
1383
|
end
|
|
1153
1384
|
|
|
1154
1385
|
class HttpClassCallback
|
|
@@ -1256,16 +1487,27 @@ module MockServer
|
|
|
1256
1487
|
end
|
|
1257
1488
|
|
|
1258
1489
|
class HttpOverrideForwardedRequest
|
|
1490
|
+
# request_override (an {HttpRequest}) is merged over the forwarded request and
|
|
1491
|
+
# response_override (an {HttpResponse}) over the response; response_template
|
|
1492
|
+
# (an {HttpTemplate}) renders the response from the request and the forwarded
|
|
1493
|
+
# response. http_request / http_response are the older names of the overrides;
|
|
1494
|
+
# the server accepts either set, not both.
|
|
1259
1495
|
attr_accessor :http_request, :http_response, :delay,
|
|
1260
|
-
:
|
|
1496
|
+
:request_override, :request_modifier,
|
|
1497
|
+
:response_override, :response_modifier, :response_template, :primary
|
|
1261
1498
|
|
|
1262
1499
|
def initialize(http_request: nil, http_response: nil, delay: nil,
|
|
1263
|
-
|
|
1500
|
+
request_override: nil, request_modifier: nil,
|
|
1501
|
+
response_override: nil, response_modifier: nil, response_template: nil,
|
|
1502
|
+
primary: nil)
|
|
1264
1503
|
@http_request = http_request
|
|
1265
1504
|
@http_response = http_response
|
|
1266
1505
|
@delay = delay
|
|
1506
|
+
@request_override = request_override
|
|
1267
1507
|
@request_modifier = request_modifier
|
|
1508
|
+
@response_override = response_override
|
|
1268
1509
|
@response_modifier = response_modifier
|
|
1510
|
+
@response_template = response_template
|
|
1269
1511
|
@primary = primary
|
|
1270
1512
|
end
|
|
1271
1513
|
|
|
@@ -1274,8 +1516,11 @@ module MockServer
|
|
|
1274
1516
|
'httpRequest' => @http_request&.to_h,
|
|
1275
1517
|
'httpResponse' => @http_response&.to_h,
|
|
1276
1518
|
'delay' => @delay&.to_h,
|
|
1519
|
+
'requestOverride' => @request_override&.to_h,
|
|
1277
1520
|
'requestModifier' => @request_modifier,
|
|
1521
|
+
'responseOverride' => @response_override&.to_h,
|
|
1278
1522
|
'responseModifier' => @response_modifier,
|
|
1523
|
+
'responseTemplate' => @response_template&.to_h,
|
|
1279
1524
|
'primary' => @primary
|
|
1280
1525
|
})
|
|
1281
1526
|
end
|
|
@@ -1287,8 +1532,11 @@ module MockServer
|
|
|
1287
1532
|
http_request: HttpRequest.from_hash(data['httpRequest']),
|
|
1288
1533
|
http_response: HttpResponse.from_hash(data['httpResponse']),
|
|
1289
1534
|
delay: Delay.from_hash(data['delay']),
|
|
1535
|
+
request_override: HttpRequest.from_hash(data['requestOverride']),
|
|
1290
1536
|
request_modifier: data['requestModifier'],
|
|
1537
|
+
response_override: HttpResponse.from_hash(data['responseOverride']),
|
|
1291
1538
|
response_modifier: data['responseModifier'],
|
|
1539
|
+
response_template: HttpTemplate.from_hash(data['responseTemplate']),
|
|
1292
1540
|
primary: data['primary']
|
|
1293
1541
|
)
|
|
1294
1542
|
end
|
|
@@ -1296,6 +1544,21 @@ module MockServer
|
|
|
1296
1544
|
def self.forward_overridden_request(request: nil)
|
|
1297
1545
|
new(http_request: request)
|
|
1298
1546
|
end
|
|
1547
|
+
|
|
1548
|
+
def with_request_override(request_override)
|
|
1549
|
+
@request_override = request_override
|
|
1550
|
+
self
|
|
1551
|
+
end
|
|
1552
|
+
|
|
1553
|
+
def with_response_override(response_override)
|
|
1554
|
+
@response_override = response_override
|
|
1555
|
+
self
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
def with_response_template(response_template)
|
|
1559
|
+
@response_template = response_template
|
|
1560
|
+
self
|
|
1561
|
+
end
|
|
1299
1562
|
end
|
|
1300
1563
|
|
|
1301
1564
|
class HttpRequestAndHttpResponse
|
|
@@ -1376,15 +1639,18 @@ module MockServer
|
|
|
1376
1639
|
end
|
|
1377
1640
|
|
|
1378
1641
|
class HttpSseResponse
|
|
1379
|
-
|
|
1642
|
+
# template_type renders each event with that engine (VELOCITY / JAVASCRIPT / MUSTACHE).
|
|
1643
|
+
attr_accessor :status_code, :headers, :events, :close_connection, :delay, :primary, :template_type
|
|
1380
1644
|
|
|
1381
|
-
def initialize(status_code: nil, headers: nil, events: nil, close_connection: nil, delay: nil, primary: nil
|
|
1645
|
+
def initialize(status_code: nil, headers: nil, events: nil, close_connection: nil, delay: nil, primary: nil,
|
|
1646
|
+
template_type: nil)
|
|
1382
1647
|
@status_code = status_code
|
|
1383
1648
|
@headers = headers
|
|
1384
1649
|
@events = events
|
|
1385
1650
|
@close_connection = close_connection
|
|
1386
1651
|
@delay = delay
|
|
1387
1652
|
@primary = primary
|
|
1653
|
+
@template_type = template_type
|
|
1388
1654
|
end
|
|
1389
1655
|
|
|
1390
1656
|
def to_h
|
|
@@ -1395,6 +1661,7 @@ module MockServer
|
|
|
1395
1661
|
result['closeConnection'] = @close_connection unless @close_connection.nil?
|
|
1396
1662
|
result['delay'] = @delay.to_h if @delay
|
|
1397
1663
|
result['primary'] = @primary unless @primary.nil?
|
|
1664
|
+
result['templateType'] = @template_type unless @template_type.nil?
|
|
1398
1665
|
result
|
|
1399
1666
|
end
|
|
1400
1667
|
|
|
@@ -1409,7 +1676,8 @@ module MockServer
|
|
|
1409
1676
|
events: events,
|
|
1410
1677
|
close_connection: data['closeConnection'],
|
|
1411
1678
|
delay: Delay.from_hash(data['delay']),
|
|
1412
|
-
primary: data['primary']
|
|
1679
|
+
primary: data['primary'],
|
|
1680
|
+
template_type: data['templateType']
|
|
1413
1681
|
)
|
|
1414
1682
|
end
|
|
1415
1683
|
end
|
|
@@ -1478,18 +1746,63 @@ module MockServer
|
|
|
1478
1746
|
end
|
|
1479
1747
|
end
|
|
1480
1748
|
|
|
1749
|
+
# Matches a graphql-transport-ws subscribe message against this GraphQL query.
|
|
1750
|
+
# +type+ is an optional body-type discriminator; only "GRAPHQL" is meaningful.
|
|
1751
|
+
class GraphQLSubscriptionFilter
|
|
1752
|
+
attr_accessor :query, :operation_name, :variables_schema, :selection_set_match_type, :fields, :type
|
|
1753
|
+
|
|
1754
|
+
def initialize(query: nil, operation_name: nil, variables_schema: nil, selection_set_match_type: nil,
|
|
1755
|
+
fields: nil, type: nil)
|
|
1756
|
+
@query = query
|
|
1757
|
+
@operation_name = operation_name
|
|
1758
|
+
@variables_schema = variables_schema
|
|
1759
|
+
@selection_set_match_type = selection_set_match_type
|
|
1760
|
+
@fields = fields
|
|
1761
|
+
@type = type
|
|
1762
|
+
end
|
|
1763
|
+
|
|
1764
|
+
def to_h
|
|
1765
|
+
MockServer.strip_none({
|
|
1766
|
+
'type' => @type,
|
|
1767
|
+
'query' => @query,
|
|
1768
|
+
'operationName' => @operation_name,
|
|
1769
|
+
'variablesSchema' => @variables_schema,
|
|
1770
|
+
'selectionSetMatchType' => @selection_set_match_type,
|
|
1771
|
+
'fields' => @fields
|
|
1772
|
+
})
|
|
1773
|
+
end
|
|
1774
|
+
|
|
1775
|
+
def self.from_hash(data)
|
|
1776
|
+
return nil if data.nil?
|
|
1777
|
+
|
|
1778
|
+
new(
|
|
1779
|
+
query: data['query'],
|
|
1780
|
+
operation_name: data['operationName'],
|
|
1781
|
+
variables_schema: data['variablesSchema'],
|
|
1782
|
+
selection_set_match_type: data['selectionSetMatchType'],
|
|
1783
|
+
fields: data['fields'],
|
|
1784
|
+
type: data['type']
|
|
1785
|
+
)
|
|
1786
|
+
end
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1481
1789
|
class HttpWebSocketResponse
|
|
1482
1790
|
# matchers carries per-incoming-frame response rules (server
|
|
1483
1791
|
# HttpWebSocketResponseDTO.matchers); each is a {WebSocketFrameMatcher}.
|
|
1484
|
-
|
|
1792
|
+
# template_type renders each message with that engine.
|
|
1793
|
+
attr_accessor :subprotocol, :messages, :matchers, :close_connection, :delay, :primary,
|
|
1794
|
+
:template_type, :graphql_subscription_filter
|
|
1485
1795
|
|
|
1486
|
-
def initialize(subprotocol: nil, messages: nil, matchers: nil, close_connection: nil, delay: nil, primary: nil
|
|
1796
|
+
def initialize(subprotocol: nil, messages: nil, matchers: nil, close_connection: nil, delay: nil, primary: nil,
|
|
1797
|
+
template_type: nil, graphql_subscription_filter: nil)
|
|
1487
1798
|
@subprotocol = subprotocol
|
|
1488
1799
|
@messages = messages
|
|
1489
1800
|
@matchers = matchers
|
|
1490
1801
|
@close_connection = close_connection
|
|
1491
1802
|
@delay = delay
|
|
1492
1803
|
@primary = primary
|
|
1804
|
+
@template_type = template_type
|
|
1805
|
+
@graphql_subscription_filter = graphql_subscription_filter
|
|
1493
1806
|
end
|
|
1494
1807
|
|
|
1495
1808
|
def to_h
|
|
@@ -1500,6 +1813,8 @@ module MockServer
|
|
|
1500
1813
|
result['closeConnection'] = @close_connection unless @close_connection.nil?
|
|
1501
1814
|
result['delay'] = @delay.to_h if @delay
|
|
1502
1815
|
result['primary'] = @primary unless @primary.nil?
|
|
1816
|
+
result['templateType'] = @template_type unless @template_type.nil?
|
|
1817
|
+
result['graphqlSubscriptionFilter'] = @graphql_subscription_filter.to_h if @graphql_subscription_filter
|
|
1503
1818
|
result
|
|
1504
1819
|
end
|
|
1505
1820
|
|
|
@@ -1516,7 +1831,9 @@ module MockServer
|
|
|
1516
1831
|
matchers: matchers,
|
|
1517
1832
|
close_connection: data['closeConnection'],
|
|
1518
1833
|
delay: Delay.from_hash(data['delay']),
|
|
1519
|
-
primary: data['primary']
|
|
1834
|
+
primary: data['primary'],
|
|
1835
|
+
template_type: data['templateType'],
|
|
1836
|
+
graphql_subscription_filter: GraphQLSubscriptionFilter.from_hash(data['graphqlSubscriptionFilter'])
|
|
1520
1837
|
)
|
|
1521
1838
|
end
|
|
1522
1839
|
end
|
|
@@ -1672,13 +1989,23 @@ module MockServer
|
|
|
1672
1989
|
end
|
|
1673
1990
|
end
|
|
1674
1991
|
|
|
1992
|
+
# Values for BinaryResponse#upstream: what happens upstream to a message a binary
|
|
1993
|
+
# expectation matches on a relayed connection (matchBinaryExpectationsWhenProxying).
|
|
1994
|
+
# ANSWER_ONLY is the server's default; the other two need binaryMessageFraming POSTGRESQL.
|
|
1995
|
+
module BinaryUpstream
|
|
1996
|
+
ANSWER_ONLY = 'ANSWER_ONLY'
|
|
1997
|
+
ANSWER_AND_FORWARD = 'ANSWER_AND_FORWARD'
|
|
1998
|
+
FORWARD_AND_REPLACE = 'FORWARD_AND_REPLACE'
|
|
1999
|
+
end
|
|
2000
|
+
|
|
1675
2001
|
class BinaryResponse
|
|
1676
|
-
attr_accessor :binary_data, :delay, :primary
|
|
2002
|
+
attr_accessor :binary_data, :delay, :primary, :upstream
|
|
1677
2003
|
|
|
1678
|
-
def initialize(binary_data: nil, delay: nil, primary: nil)
|
|
2004
|
+
def initialize(binary_data: nil, delay: nil, primary: nil, upstream: nil)
|
|
1679
2005
|
@binary_data = binary_data
|
|
1680
2006
|
@delay = delay
|
|
1681
2007
|
@primary = primary
|
|
2008
|
+
@upstream = upstream
|
|
1682
2009
|
end
|
|
1683
2010
|
|
|
1684
2011
|
def to_h
|
|
@@ -1686,6 +2013,7 @@ module MockServer
|
|
|
1686
2013
|
result['binaryData'] = @binary_data unless @binary_data.nil?
|
|
1687
2014
|
result['delay'] = @delay.to_h if @delay
|
|
1688
2015
|
result['primary'] = @primary unless @primary.nil?
|
|
2016
|
+
result['upstream'] = @upstream unless @upstream.nil?
|
|
1689
2017
|
result
|
|
1690
2018
|
end
|
|
1691
2019
|
|
|
@@ -1695,7 +2023,8 @@ module MockServer
|
|
|
1695
2023
|
new(
|
|
1696
2024
|
binary_data: data['binaryData'],
|
|
1697
2025
|
delay: Delay.from_hash(data['delay']),
|
|
1698
|
-
primary: data['primary']
|
|
2026
|
+
primary: data['primary'],
|
|
2027
|
+
upstream: data['upstream']
|
|
1699
2028
|
)
|
|
1700
2029
|
end
|
|
1701
2030
|
end
|
|
@@ -2541,7 +2870,7 @@ module MockServer
|
|
|
2541
2870
|
id: data['id'],
|
|
2542
2871
|
priority: data['priority'],
|
|
2543
2872
|
percentage: data['percentage'],
|
|
2544
|
-
http_request:
|
|
2873
|
+
http_request: MockServer.deserialize_request_definition(data['httpRequest']),
|
|
2545
2874
|
http_response: HttpResponse.from_hash(data['httpResponse']),
|
|
2546
2875
|
http_response_template: HttpTemplate.from_hash(data['httpResponseTemplate']),
|
|
2547
2876
|
http_response_class_callback: HttpClassCallback.from_hash(data['httpResponseClassCallback']),
|
|
@@ -2583,18 +2912,25 @@ module MockServer
|
|
|
2583
2912
|
end
|
|
2584
2913
|
end
|
|
2585
2914
|
|
|
2915
|
+
# An OpenAPI request matcher: as an expectation's or verification's
|
|
2916
|
+
# httpRequest it matches the requests an OpenAPI operation accepts.
|
|
2586
2917
|
class OpenAPIDefinition
|
|
2587
|
-
|
|
2918
|
+
# not_operation inverts the match (the server's +not+ field).
|
|
2919
|
+
attr_accessor :spec_url_or_payload, :operation_id, :context_path_prefix, :not_operation
|
|
2588
2920
|
|
|
2589
|
-
def initialize(spec_url_or_payload: nil, operation_id: nil)
|
|
2921
|
+
def initialize(spec_url_or_payload: nil, operation_id: nil, context_path_prefix: nil, not_operation: nil)
|
|
2590
2922
|
@spec_url_or_payload = spec_url_or_payload
|
|
2591
2923
|
@operation_id = operation_id
|
|
2924
|
+
@context_path_prefix = context_path_prefix
|
|
2925
|
+
@not_operation = not_operation
|
|
2592
2926
|
end
|
|
2593
2927
|
|
|
2594
2928
|
def to_h
|
|
2595
2929
|
MockServer.strip_none({
|
|
2596
|
-
'
|
|
2597
|
-
'
|
|
2930
|
+
'not' => @not_operation,
|
|
2931
|
+
'specUrlOrPayload' => @spec_url_or_payload,
|
|
2932
|
+
'operationId' => @operation_id,
|
|
2933
|
+
'contextPathPrefix' => @context_path_prefix
|
|
2598
2934
|
})
|
|
2599
2935
|
end
|
|
2600
2936
|
|
|
@@ -2603,7 +2939,46 @@ module MockServer
|
|
|
2603
2939
|
|
|
2604
2940
|
new(
|
|
2605
2941
|
spec_url_or_payload: data['specUrlOrPayload'],
|
|
2606
|
-
operation_id: data['operationId']
|
|
2942
|
+
operation_id: data['operationId'],
|
|
2943
|
+
context_path_prefix: data['contextPathPrefix'],
|
|
2944
|
+
not_operation: data['not']
|
|
2945
|
+
)
|
|
2946
|
+
end
|
|
2947
|
+
end
|
|
2948
|
+
|
|
2949
|
+
# A conditional (if/then/else) request matcher, written as an httpRequest
|
|
2950
|
+
# holding if, then and else. When +if_request+ matches, +then_request+ must
|
|
2951
|
+
# match too; otherwise +else_request+ must (with none, the matcher matches
|
|
2952
|
+
# whenever +if_request+ does not). Each branch is an HttpRequest, an
|
|
2953
|
+
# OpenAPIDefinition or another ConditionalRequestDefinition.
|
|
2954
|
+
class ConditionalRequestDefinition
|
|
2955
|
+
# not_condition inverts the match (the server's +not+ field).
|
|
2956
|
+
attr_accessor :if_request, :then_request, :else_request, :not_condition
|
|
2957
|
+
|
|
2958
|
+
def initialize(if_request: nil, then_request: nil, else_request: nil, not_condition: nil)
|
|
2959
|
+
@if_request = if_request
|
|
2960
|
+
@then_request = then_request
|
|
2961
|
+
@else_request = else_request
|
|
2962
|
+
@not_condition = not_condition
|
|
2963
|
+
end
|
|
2964
|
+
|
|
2965
|
+
def to_h
|
|
2966
|
+
MockServer.strip_none({
|
|
2967
|
+
'not' => @not_condition,
|
|
2968
|
+
'if' => @if_request&.to_h,
|
|
2969
|
+
'then' => @then_request&.to_h,
|
|
2970
|
+
'else' => @else_request&.to_h
|
|
2971
|
+
})
|
|
2972
|
+
end
|
|
2973
|
+
|
|
2974
|
+
def self.from_hash(data)
|
|
2975
|
+
return nil if data.nil?
|
|
2976
|
+
|
|
2977
|
+
new(
|
|
2978
|
+
if_request: MockServer.deserialize_request_definition(data['if']),
|
|
2979
|
+
then_request: MockServer.deserialize_request_definition(data['then']),
|
|
2980
|
+
else_request: MockServer.deserialize_request_definition(data['else']),
|
|
2981
|
+
not_condition: data['not']
|
|
2607
2982
|
)
|
|
2608
2983
|
end
|
|
2609
2984
|
end
|
|
@@ -2705,7 +3080,7 @@ module MockServer
|
|
|
2705
3080
|
return nil if data.nil?
|
|
2706
3081
|
|
|
2707
3082
|
new(
|
|
2708
|
-
http_request:
|
|
3083
|
+
http_request: MockServer.deserialize_request_definition(data['httpRequest']),
|
|
2709
3084
|
http_response: HttpResponse.from_hash(data['httpResponse']),
|
|
2710
3085
|
expectation_id: ExpectationId.from_hash(data['expectationId']),
|
|
2711
3086
|
times: VerificationTimes.from_hash(data['times']),
|
|
@@ -2738,7 +3113,7 @@ module MockServer
|
|
|
2738
3113
|
http_responses_data = data['httpResponses']
|
|
2739
3114
|
expectation_ids_data = data['expectationIds']
|
|
2740
3115
|
new(
|
|
2741
|
-
http_requests: http_requests_data&.map { |r|
|
|
3116
|
+
http_requests: http_requests_data&.map { |r| MockServer.deserialize_request_definition(r) },
|
|
2742
3117
|
http_responses: http_responses_data&.map { |r| HttpResponse.from_hash(r) },
|
|
2743
3118
|
expectation_ids: expectation_ids_data&.map { |e| ExpectationId.from_hash(e) }
|
|
2744
3119
|
)
|
|
@@ -3116,20 +3491,61 @@ module MockServer
|
|
|
3116
3491
|
end
|
|
3117
3492
|
end
|
|
3118
3493
|
|
|
3494
|
+
# A per-step response assertion for a load scenario: reads a value from the
|
|
3495
|
+
# step's response (+source+ +STATUS+, +HEADER+ with +header_name+, or
|
|
3496
|
+
# +BODY_JSONPATH+ with +json_path+) and compares it with +value+ using
|
|
3497
|
+
# +comparator+ (+EQUALS+, +NOT_EQUALS+, +CONTAINS+, +MATCHES+, +GT+, +LT+,
|
|
3498
|
+
# +GTE+ or +LTE+). A failing check never fails the request; failures feed the
|
|
3499
|
+
# +CHECK_FAILURE_RATE+ threshold.
|
|
3500
|
+
class LoadCheck
|
|
3501
|
+
attr_accessor :source, :header_name, :json_path, :comparator, :value
|
|
3502
|
+
|
|
3503
|
+
def initialize(source:, comparator:, header_name: nil, json_path: nil, value: nil)
|
|
3504
|
+
@source = source
|
|
3505
|
+
@comparator = comparator
|
|
3506
|
+
@header_name = header_name
|
|
3507
|
+
@json_path = json_path
|
|
3508
|
+
@value = value
|
|
3509
|
+
end
|
|
3510
|
+
|
|
3511
|
+
def to_h
|
|
3512
|
+
MockServer.strip_none({
|
|
3513
|
+
'source' => @source,
|
|
3514
|
+
'headerName' => @header_name,
|
|
3515
|
+
'jsonPath' => @json_path,
|
|
3516
|
+
'comparator' => @comparator,
|
|
3517
|
+
'value' => @value
|
|
3518
|
+
})
|
|
3519
|
+
end
|
|
3520
|
+
|
|
3521
|
+
def self.from_hash(data)
|
|
3522
|
+
return nil if data.nil?
|
|
3523
|
+
|
|
3524
|
+
new(
|
|
3525
|
+
source: data['source'],
|
|
3526
|
+
header_name: data['headerName'],
|
|
3527
|
+
json_path: data['jsonPath'],
|
|
3528
|
+
comparator: data['comparator'],
|
|
3529
|
+
value: data['value']
|
|
3530
|
+
)
|
|
3531
|
+
end
|
|
3532
|
+
end
|
|
3533
|
+
|
|
3119
3534
|
# A single step within a load scenario. Each step fires +request+ (an HttpRequest)
|
|
3120
3535
|
# against the target, optionally pausing for +think_time+ (a Delay) afterwards.
|
|
3121
3536
|
# +captures+ binds values from this step's response for later steps in the same
|
|
3122
|
-
# iteration; +
|
|
3123
|
-
# +step_selection+ is +WEIGHTED+.
|
|
3537
|
+
# iteration; +checks+ (LoadCheck) assert on that response; +weight+ is the
|
|
3538
|
+
# relative selection weight when the scenario's +step_selection+ is +WEIGHTED+.
|
|
3124
3539
|
class LoadStep
|
|
3125
|
-
attr_accessor :name, :labels, :think_time, :request, :captures, :weight
|
|
3540
|
+
attr_accessor :name, :labels, :think_time, :request, :captures, :checks, :weight
|
|
3126
3541
|
|
|
3127
|
-
def initialize(request:, name: nil, labels: nil, think_time: nil, captures: nil, weight: nil)
|
|
3542
|
+
def initialize(request:, name: nil, labels: nil, think_time: nil, captures: nil, checks: nil, weight: nil)
|
|
3128
3543
|
@request = request
|
|
3129
3544
|
@name = name
|
|
3130
3545
|
@labels = labels
|
|
3131
3546
|
@think_time = think_time
|
|
3132
3547
|
@captures = captures
|
|
3548
|
+
@checks = checks
|
|
3133
3549
|
@weight = weight
|
|
3134
3550
|
end
|
|
3135
3551
|
|
|
@@ -3140,6 +3556,7 @@ module MockServer
|
|
|
3140
3556
|
'thinkTime' => @think_time&.to_h,
|
|
3141
3557
|
'request' => @request&.to_h,
|
|
3142
3558
|
'captures' => @captures.nil? ? nil : @captures.map(&:to_h),
|
|
3559
|
+
'checks' => @checks.nil? ? nil : @checks.map(&:to_h),
|
|
3143
3560
|
'weight' => @weight
|
|
3144
3561
|
})
|
|
3145
3562
|
end
|
|
@@ -3148,12 +3565,14 @@ module MockServer
|
|
|
3148
3565
|
return nil if data.nil?
|
|
3149
3566
|
|
|
3150
3567
|
captures_data = data['captures']
|
|
3568
|
+
checks_data = data['checks']
|
|
3151
3569
|
new(
|
|
3152
3570
|
name: data['name'],
|
|
3153
3571
|
labels: data['labels'],
|
|
3154
3572
|
think_time: Delay.from_hash(data['thinkTime']),
|
|
3155
3573
|
request: HttpRequest.from_hash(data['request']),
|
|
3156
3574
|
captures: captures_data ? captures_data.map { |c| LoadCapture.from_hash(c) } : nil,
|
|
3575
|
+
checks: checks_data ? checks_data.map { |c| LoadCheck.from_hash(c) } : nil,
|
|
3157
3576
|
weight: data['weight']
|
|
3158
3577
|
)
|
|
3159
3578
|
end
|
|
@@ -3230,6 +3649,384 @@ module MockServer
|
|
|
3230
3649
|
end
|
|
3231
3650
|
end
|
|
3232
3651
|
|
|
3652
|
+
# -------------------------------------------------------------------
|
|
3653
|
+
# Diagnostics: explain, debug_mismatch and explain_unmatched results.
|
|
3654
|
+
# They follow the RequestExplanation and ExplainedExpectation OpenAPI
|
|
3655
|
+
# schemas; an enum value a newer server sends is kept as its String.
|
|
3656
|
+
# -------------------------------------------------------------------
|
|
3657
|
+
|
|
3658
|
+
# A JSON patch (RFC 6902) of an expectation that would make it match the
|
|
3659
|
+
# request; nothing is applied. +unpatched_fields+ lists each field that
|
|
3660
|
+
# could not be patched, with a +reason+.
|
|
3661
|
+
class SuggestedFix
|
|
3662
|
+
attr_reader :patch, :patched_fields, :unpatched_fields, :matches_after_patch
|
|
3663
|
+
|
|
3664
|
+
def initialize(patch: [], patched_fields: [], unpatched_fields: [], matches_after_patch: false)
|
|
3665
|
+
@patch = patch
|
|
3666
|
+
@patched_fields = patched_fields
|
|
3667
|
+
@unpatched_fields = unpatched_fields
|
|
3668
|
+
@matches_after_patch = matches_after_patch
|
|
3669
|
+
end
|
|
3670
|
+
|
|
3671
|
+
def self.from_hash(data)
|
|
3672
|
+
return nil if data.nil?
|
|
3673
|
+
|
|
3674
|
+
new(
|
|
3675
|
+
patch: data['patch'] || [],
|
|
3676
|
+
patched_fields: data['patchedFields'] || [],
|
|
3677
|
+
unpatched_fields: data['unpatchedFields'] || [],
|
|
3678
|
+
matches_after_patch: data['matchesAfterPatch'] == true
|
|
3679
|
+
)
|
|
3680
|
+
end
|
|
3681
|
+
end
|
|
3682
|
+
|
|
3683
|
+
# One expectation as explain, debug_mismatch or explain_unmatched evaluated
|
|
3684
|
+
# it. The matched expectation has no +reason+; a skipped or shadowed one has
|
|
3685
|
+
# a +reason+ (NAMESPACE, INACTIVE, FIELDS_DIFFER, NOT_OPERATOR, CONVERSATION,
|
|
3686
|
+
# SCENARIO_STATE, PERCENTAGE or LOWER_PRIORITY) and an +explanation+.
|
|
3687
|
+
# +suggested_fix+ is set only by explain_unmatched with +suggest_fixes+.
|
|
3688
|
+
class ExplainedExpectation
|
|
3689
|
+
attr_reader :expectation_id, :expectation_method, :expectation_path, :position, :priority,
|
|
3690
|
+
:reason, :explanation, :matches, :matched_field_count, :total_field_count,
|
|
3691
|
+
:differing_field_count, :differences, :remediation, :suggested_fix
|
|
3692
|
+
|
|
3693
|
+
def initialize(expectation_id: nil, expectation_method: nil, expectation_path: nil, position: nil,
|
|
3694
|
+
priority: nil, reason: nil, explanation: nil, matches: nil, matched_field_count: nil,
|
|
3695
|
+
total_field_count: nil, differing_field_count: nil, differences: {}, remediation: {},
|
|
3696
|
+
suggested_fix: nil)
|
|
3697
|
+
@expectation_id = expectation_id
|
|
3698
|
+
@expectation_method = expectation_method
|
|
3699
|
+
@expectation_path = expectation_path
|
|
3700
|
+
@position = position
|
|
3701
|
+
@priority = priority
|
|
3702
|
+
@reason = reason
|
|
3703
|
+
@explanation = explanation
|
|
3704
|
+
@matches = matches
|
|
3705
|
+
@matched_field_count = matched_field_count
|
|
3706
|
+
@total_field_count = total_field_count
|
|
3707
|
+
@differing_field_count = differing_field_count
|
|
3708
|
+
@differences = differences
|
|
3709
|
+
@remediation = remediation
|
|
3710
|
+
@suggested_fix = suggested_fix
|
|
3711
|
+
end
|
|
3712
|
+
|
|
3713
|
+
def self.from_hash(data)
|
|
3714
|
+
return nil if data.nil?
|
|
3715
|
+
|
|
3716
|
+
new(
|
|
3717
|
+
expectation_id: data['expectationId'],
|
|
3718
|
+
expectation_method: data['expectationMethod'],
|
|
3719
|
+
expectation_path: data['expectationPath'],
|
|
3720
|
+
position: data['position'],
|
|
3721
|
+
priority: data['priority'],
|
|
3722
|
+
reason: data['reason'],
|
|
3723
|
+
explanation: data['explanation'],
|
|
3724
|
+
matches: data['matches'],
|
|
3725
|
+
matched_field_count: data['matchedFieldCount'],
|
|
3726
|
+
total_field_count: data['totalFieldCount'],
|
|
3727
|
+
differing_field_count: data['differingFieldCount'],
|
|
3728
|
+
differences: data['differences'] || {},
|
|
3729
|
+
remediation: data['remediation'] || {},
|
|
3730
|
+
suggested_fix: SuggestedFix.from_hash(data['suggestedFix'])
|
|
3731
|
+
)
|
|
3732
|
+
end
|
|
3733
|
+
|
|
3734
|
+
# @api private
|
|
3735
|
+
def self.list(items)
|
|
3736
|
+
(items || []).map { |item| from_hash(item) }
|
|
3737
|
+
end
|
|
3738
|
+
end
|
|
3739
|
+
|
|
3740
|
+
# The recorded request that explain re-checked.
|
|
3741
|
+
class ExplainedRequest
|
|
3742
|
+
attr_reader :timestamp, :method, :path, :body_truncated, :body_truncated_note
|
|
3743
|
+
|
|
3744
|
+
def initialize(timestamp: nil, method: nil, path: nil, body_truncated: false, body_truncated_note: nil)
|
|
3745
|
+
@timestamp = timestamp
|
|
3746
|
+
@method = method
|
|
3747
|
+
@path = path
|
|
3748
|
+
@body_truncated = body_truncated
|
|
3749
|
+
@body_truncated_note = body_truncated_note
|
|
3750
|
+
end
|
|
3751
|
+
|
|
3752
|
+
def self.from_hash(data)
|
|
3753
|
+
return nil if data.nil?
|
|
3754
|
+
|
|
3755
|
+
new(
|
|
3756
|
+
timestamp: data['timestamp'],
|
|
3757
|
+
method: data['method'],
|
|
3758
|
+
path: data['path'],
|
|
3759
|
+
body_truncated: data['bodyTruncated'] == true,
|
|
3760
|
+
body_truncated_note: data['bodyTruncatedNote']
|
|
3761
|
+
)
|
|
3762
|
+
end
|
|
3763
|
+
end
|
|
3764
|
+
|
|
3765
|
+
# How a request was handled when it arrived: +outcome+ is MATCHED, NO_MATCH,
|
|
3766
|
+
# FORWARDED_WITHOUT_EXPECTATION, RESPONDED_WITHOUT_EXPECTATION or UNKNOWN;
|
|
3767
|
+
# the expectation fields are set for MATCHED only.
|
|
3768
|
+
# +forward_failure+ is set only when the request was forwarded and the
|
|
3769
|
+
# forward failed.
|
|
3770
|
+
class Arrival
|
|
3771
|
+
attr_reader :outcome, :expectation_id, :expectation_still_stored, :forward_failure
|
|
3772
|
+
|
|
3773
|
+
def initialize(outcome: nil, expectation_id: nil, expectation_still_stored: nil, forward_failure: nil)
|
|
3774
|
+
@outcome = outcome
|
|
3775
|
+
@expectation_id = expectation_id
|
|
3776
|
+
@expectation_still_stored = expectation_still_stored
|
|
3777
|
+
@forward_failure = forward_failure
|
|
3778
|
+
end
|
|
3779
|
+
|
|
3780
|
+
def self.from_hash(data)
|
|
3781
|
+
return nil if data.nil?
|
|
3782
|
+
|
|
3783
|
+
new(
|
|
3784
|
+
outcome: data['outcome'],
|
|
3785
|
+
expectation_id: data['expectationId'],
|
|
3786
|
+
expectation_still_stored: data['expectationStillStored'],
|
|
3787
|
+
forward_failure: ArrivalForwardFailure.from_hash(data['forwardFailure'])
|
|
3788
|
+
)
|
|
3789
|
+
end
|
|
3790
|
+
end
|
|
3791
|
+
|
|
3792
|
+
# Why a request's forward failed, so MockServer answered 502: the same
|
|
3793
|
+
# facts the explain_forward_failure MCP tool gives. +reason+ is redacted as
|
|
3794
|
+
# the event log is and capped at 2048 characters (+reason_truncated+); when
|
|
3795
|
+
# +cause_named+ is false the reason is the raw error and +hint+ says what to
|
|
3796
|
+
# look at next.
|
|
3797
|
+
class ArrivalForwardFailure
|
|
3798
|
+
attr_reader :status_code, :reason, :reason_truncated, :cause_named, :forward_loop_suspected, :hint
|
|
3799
|
+
|
|
3800
|
+
def initialize(status_code: nil, reason: nil, reason_truncated: false, cause_named: nil,
|
|
3801
|
+
forward_loop_suspected: nil, hint: nil)
|
|
3802
|
+
@status_code = status_code
|
|
3803
|
+
@reason = reason
|
|
3804
|
+
@reason_truncated = reason_truncated
|
|
3805
|
+
@cause_named = cause_named
|
|
3806
|
+
@forward_loop_suspected = forward_loop_suspected
|
|
3807
|
+
@hint = hint
|
|
3808
|
+
end
|
|
3809
|
+
|
|
3810
|
+
def self.from_hash(data)
|
|
3811
|
+
return nil if data.nil?
|
|
3812
|
+
|
|
3813
|
+
new(
|
|
3814
|
+
status_code: data['statusCode'],
|
|
3815
|
+
reason: data['reason'],
|
|
3816
|
+
reason_truncated: data['reasonTruncated'] == true,
|
|
3817
|
+
cause_named: data['causeNamed'],
|
|
3818
|
+
forward_loop_suspected: data['forwardLoopSuspected'],
|
|
3819
|
+
hint: data['hint']
|
|
3820
|
+
)
|
|
3821
|
+
end
|
|
3822
|
+
end
|
|
3823
|
+
|
|
3824
|
+
# Set only when narration was asked for: a plain-language summary the
|
|
3825
|
+
# configured LLM backend wrote, or why none is available (+reason+). It is
|
|
3826
|
+
# AI-generated and can be wrong; the other fields are the facts.
|
|
3827
|
+
class ExplanationNarration
|
|
3828
|
+
attr_reader :available, :ai_generated, :label, :provider, :model, :text, :reason
|
|
3829
|
+
|
|
3830
|
+
def initialize(available: nil, ai_generated: nil, label: nil, provider: nil, model: nil, text: nil,
|
|
3831
|
+
reason: nil)
|
|
3832
|
+
@available = available
|
|
3833
|
+
@ai_generated = ai_generated
|
|
3834
|
+
@label = label
|
|
3835
|
+
@provider = provider
|
|
3836
|
+
@model = model
|
|
3837
|
+
@text = text
|
|
3838
|
+
@reason = reason
|
|
3839
|
+
end
|
|
3840
|
+
|
|
3841
|
+
def self.from_hash(data)
|
|
3842
|
+
return nil if data.nil?
|
|
3843
|
+
|
|
3844
|
+
new(
|
|
3845
|
+
available: data['available'],
|
|
3846
|
+
ai_generated: data['aiGenerated'],
|
|
3847
|
+
label: data['label'],
|
|
3848
|
+
provider: data['provider'],
|
|
3849
|
+
model: data['model'],
|
|
3850
|
+
text: data['text'],
|
|
3851
|
+
reason: data['reason']
|
|
3852
|
+
)
|
|
3853
|
+
end
|
|
3854
|
+
end
|
|
3855
|
+
|
|
3856
|
+
# Which expectation would serve a recorded request now, why each one
|
|
3857
|
+
# evaluated ahead of it was passed over, which lower-priority ones it
|
|
3858
|
+
# shadows, and how the request was handled when it arrived. Recomputed when
|
|
3859
|
+
# asked for, against the current expectations, scenario states and Times.
|
|
3860
|
+
class RequestExplanation
|
|
3861
|
+
attr_reader :correlation_id, :timestamp, :recomputed, :note, :request, :at_arrival,
|
|
3862
|
+
:same_outcome_as_at_arrival, :expectations_changed_since, :expectations_last_changed,
|
|
3863
|
+
:matched_expectation, :closest_expectation_id, :skipped_expectations,
|
|
3864
|
+
:shadowed_expectations, :total_expectations, :evaluated_expectations, :truncated,
|
|
3865
|
+
:max_expectations_evaluated, :narration
|
|
3866
|
+
|
|
3867
|
+
def initialize(correlation_id: nil, timestamp: nil, recomputed: nil, note: nil, request: nil,
|
|
3868
|
+
at_arrival: nil, same_outcome_as_at_arrival: nil, expectations_changed_since: false,
|
|
3869
|
+
expectations_last_changed: nil, matched_expectation: nil, closest_expectation_id: nil,
|
|
3870
|
+
skipped_expectations: [], shadowed_expectations: [], total_expectations: 0,
|
|
3871
|
+
evaluated_expectations: 0, truncated: false, max_expectations_evaluated: nil,
|
|
3872
|
+
narration: nil)
|
|
3873
|
+
@correlation_id = correlation_id
|
|
3874
|
+
@timestamp = timestamp
|
|
3875
|
+
@recomputed = recomputed
|
|
3876
|
+
@note = note
|
|
3877
|
+
@request = request
|
|
3878
|
+
@at_arrival = at_arrival
|
|
3879
|
+
@same_outcome_as_at_arrival = same_outcome_as_at_arrival
|
|
3880
|
+
@expectations_changed_since = expectations_changed_since
|
|
3881
|
+
@expectations_last_changed = expectations_last_changed
|
|
3882
|
+
@matched_expectation = matched_expectation
|
|
3883
|
+
@closest_expectation_id = closest_expectation_id
|
|
3884
|
+
@skipped_expectations = skipped_expectations
|
|
3885
|
+
@shadowed_expectations = shadowed_expectations
|
|
3886
|
+
@total_expectations = total_expectations
|
|
3887
|
+
@evaluated_expectations = evaluated_expectations
|
|
3888
|
+
@truncated = truncated
|
|
3889
|
+
@max_expectations_evaluated = max_expectations_evaluated
|
|
3890
|
+
@narration = narration
|
|
3891
|
+
end
|
|
3892
|
+
|
|
3893
|
+
def self.from_hash(data)
|
|
3894
|
+
return nil if data.nil?
|
|
3895
|
+
|
|
3896
|
+
new(
|
|
3897
|
+
correlation_id: data['correlationId'],
|
|
3898
|
+
timestamp: data['timestamp'],
|
|
3899
|
+
recomputed: data['recomputed'],
|
|
3900
|
+
note: data['note'],
|
|
3901
|
+
request: ExplainedRequest.from_hash(data['request']),
|
|
3902
|
+
at_arrival: Arrival.from_hash(data['atArrival']),
|
|
3903
|
+
same_outcome_as_at_arrival: data['sameOutcomeAsAtArrival'],
|
|
3904
|
+
expectations_changed_since: data['expectationsChangedSince'] == true,
|
|
3905
|
+
expectations_last_changed: data['expectationsLastChanged'],
|
|
3906
|
+
matched_expectation: ExplainedExpectation.from_hash(data['matchedExpectation']),
|
|
3907
|
+
closest_expectation_id: data['closestExpectationId'],
|
|
3908
|
+
skipped_expectations: ExplainedExpectation.list(data['skippedExpectations']),
|
|
3909
|
+
shadowed_expectations: ExplainedExpectation.list(data['shadowedExpectations']),
|
|
3910
|
+
total_expectations: data['totalExpectations'] || 0,
|
|
3911
|
+
evaluated_expectations: data['evaluatedExpectations'] || 0,
|
|
3912
|
+
truncated: data['truncated'] == true,
|
|
3913
|
+
max_expectations_evaluated: data['maxExpectationsEvaluated'],
|
|
3914
|
+
narration: ExplanationNarration.from_hash(data['narration'])
|
|
3915
|
+
)
|
|
3916
|
+
end
|
|
3917
|
+
end
|
|
3918
|
+
|
|
3919
|
+
# The expectation debug_mismatch found with the fewest differing fields.
|
|
3920
|
+
class ClosestMatch
|
|
3921
|
+
attr_reader :expectation_id, :matched_fields, :total_fields
|
|
3922
|
+
|
|
3923
|
+
def initialize(expectation_id: nil, matched_fields: nil, total_fields: nil)
|
|
3924
|
+
@expectation_id = expectation_id
|
|
3925
|
+
@matched_fields = matched_fields
|
|
3926
|
+
@total_fields = total_fields
|
|
3927
|
+
end
|
|
3928
|
+
|
|
3929
|
+
def self.from_hash(data)
|
|
3930
|
+
return nil if data.nil?
|
|
3931
|
+
|
|
3932
|
+
new(expectation_id: data['expectationId'], matched_fields: data['matchedFields'],
|
|
3933
|
+
total_fields: data['totalFields'])
|
|
3934
|
+
end
|
|
3935
|
+
end
|
|
3936
|
+
|
|
3937
|
+
# The result of debug_mismatch: each active expectation compared with the
|
|
3938
|
+
# request, and the closest match (nil when every expectation matches).
|
|
3939
|
+
class DebugMismatchResult
|
|
3940
|
+
attr_reader :correlation_id, :timestamp, :total_expectations, :evaluated_expectations,
|
|
3941
|
+
:truncated, :max_expectations_evaluated, :closest_match, :results
|
|
3942
|
+
|
|
3943
|
+
def initialize(correlation_id: nil, timestamp: nil, total_expectations: 0, evaluated_expectations: 0,
|
|
3944
|
+
truncated: false, max_expectations_evaluated: nil, closest_match: nil, results: [])
|
|
3945
|
+
@correlation_id = correlation_id
|
|
3946
|
+
@timestamp = timestamp
|
|
3947
|
+
@total_expectations = total_expectations
|
|
3948
|
+
@evaluated_expectations = evaluated_expectations
|
|
3949
|
+
@truncated = truncated
|
|
3950
|
+
@max_expectations_evaluated = max_expectations_evaluated
|
|
3951
|
+
@closest_match = closest_match
|
|
3952
|
+
@results = results
|
|
3953
|
+
end
|
|
3954
|
+
|
|
3955
|
+
def self.from_hash(data)
|
|
3956
|
+
return nil if data.nil?
|
|
3957
|
+
|
|
3958
|
+
new(
|
|
3959
|
+
correlation_id: data['correlationId'],
|
|
3960
|
+
timestamp: data['timestamp'],
|
|
3961
|
+
total_expectations: data['totalExpectations'] || 0,
|
|
3962
|
+
evaluated_expectations: data['evaluatedExpectations'] || 0,
|
|
3963
|
+
truncated: data['truncated'] == true,
|
|
3964
|
+
max_expectations_evaluated: data['maxExpectationsEvaluated'],
|
|
3965
|
+
closest_match: ClosestMatch.from_hash(data['closestMatch']),
|
|
3966
|
+
results: ExplainedExpectation.list(data['results'])
|
|
3967
|
+
)
|
|
3968
|
+
end
|
|
3969
|
+
end
|
|
3970
|
+
|
|
3971
|
+
# One recent unmatched request and its closest expectations, closest first.
|
|
3972
|
+
class UnmatchedRequest
|
|
3973
|
+
attr_reader :timestamp, :method, :path, :closest_expectations, :total_expectations_evaluated
|
|
3974
|
+
|
|
3975
|
+
def initialize(timestamp: nil, method: nil, path: nil, closest_expectations: [],
|
|
3976
|
+
total_expectations_evaluated: 0)
|
|
3977
|
+
@timestamp = timestamp
|
|
3978
|
+
@method = method
|
|
3979
|
+
@path = path
|
|
3980
|
+
@closest_expectations = closest_expectations
|
|
3981
|
+
@total_expectations_evaluated = total_expectations_evaluated
|
|
3982
|
+
end
|
|
3983
|
+
|
|
3984
|
+
def self.from_hash(data)
|
|
3985
|
+
return nil if data.nil?
|
|
3986
|
+
|
|
3987
|
+
new(
|
|
3988
|
+
timestamp: data['timestamp'],
|
|
3989
|
+
method: data['method'],
|
|
3990
|
+
path: data['path'],
|
|
3991
|
+
closest_expectations: ExplainedExpectation.list(data['closestExpectations']),
|
|
3992
|
+
total_expectations_evaluated: data['totalExpectationsEvaluated'] || 0
|
|
3993
|
+
)
|
|
3994
|
+
end
|
|
3995
|
+
end
|
|
3996
|
+
|
|
3997
|
+
# The result of explain_unmatched.
|
|
3998
|
+
class ExplainUnmatchedResult
|
|
3999
|
+
attr_reader :correlation_id, :timestamp, :unmatched_request_count, :truncated, :unmatched_requests,
|
|
4000
|
+
:narration
|
|
4001
|
+
|
|
4002
|
+
def initialize(correlation_id: nil, timestamp: nil, unmatched_request_count: 0, truncated: false,
|
|
4003
|
+
unmatched_requests: [], narration: nil)
|
|
4004
|
+
@correlation_id = correlation_id
|
|
4005
|
+
@timestamp = timestamp
|
|
4006
|
+
@unmatched_request_count = unmatched_request_count
|
|
4007
|
+
@truncated = truncated
|
|
4008
|
+
@unmatched_requests = unmatched_requests
|
|
4009
|
+
@narration = narration
|
|
4010
|
+
end
|
|
4011
|
+
|
|
4012
|
+
def self.from_hash(data)
|
|
4013
|
+
return nil if data.nil?
|
|
4014
|
+
|
|
4015
|
+
new(
|
|
4016
|
+
correlation_id: data['correlationId'],
|
|
4017
|
+
timestamp: data['timestamp'],
|
|
4018
|
+
unmatched_request_count: data['unmatchedRequestCount'] || 0,
|
|
4019
|
+
truncated: data['truncated'] == true,
|
|
4020
|
+
unmatched_requests: (data['unmatchedRequests'] || []).map { |r| UnmatchedRequest.from_hash(r) },
|
|
4021
|
+
narration: ExplanationNarration.from_hash(data['narration'])
|
|
4022
|
+
)
|
|
4023
|
+
end
|
|
4024
|
+
end
|
|
4025
|
+
|
|
3233
4026
|
# Alias matching the Python client
|
|
3234
4027
|
RequestDefinition = HttpRequest
|
|
4028
|
+
|
|
4029
|
+
constants.map { |name| const_get(name) }.each do |model|
|
|
4030
|
+
model.singleton_class.prepend(SymbolKeyTolerantFromHash) if model.is_a?(Class) && model.respond_to?(:from_hash)
|
|
4031
|
+
end
|
|
3235
4032
|
end
|