///|
/// Controls which request fields participate in cassette matching.
pub(all) enum BodyMatchMode {
Exact
Ignore
} derive(Eq, @debug.Debug)
///|
/// Header and body matching policy for a session.
pub(all) struct MatchConfig {
include_headers : Array[String]
ignore_headers : Array[String]
body_mode : BodyMatchMode
} derive(Eq, @debug.Debug)
///|
/// Return the conservative default matching policy.
pub fn MatchConfig::default() -> MatchConfig {
{ include_headers: [], ignore_headers: [], body_mode: Exact, }
}
///|
fn normalized_header_name(name : String) -> String {
name.trim().to_owned().to_lower()
}
///|
fn normalized_header_value(value : String) -> String {
value.trim().to_owned()
}
///|
fn header_name_is_listed(name : String, names : Array[String]) -> Bool {
let normalized = normalized_header_name(name)
for candidate in names {
if normalized == normalized_header_name(candidate) {
return true
}
}
false
}
///|
fn should_include_header(name : String, config : MatchConfig) -> Bool {
if header_name_is_listed(name, config.ignore_headers) {
return false
}
if config.include_headers.length() == 0 {
return true
}
header_name_is_listed(name, config.include_headers)
}
///|
fn normalize_query(url : String) -> String {
let without_fragment = match url.split_once("#") {
Some((before, _)) => before.to_owned()
None => url
}
match without_fragment.split_once("?") {
None => without_fragment
Some((base, raw_query)) => {
let parts : Array[String] = []
for raw_part in raw_query.split("&") {
let part = raw_part.to_owned().trim().to_owned()
if part.length() == 0 {
continue
}
let normalized = match part.split_once("=") {
Some((key, value)) =>
normalized_header_value(key.to_owned()) +
"=" +
normalized_header_value(value.to_owned())
None => normalized_header_value(part) + "="
}
parts.push(normalized)
}
if parts.length() == 0 {
base.to_owned()
} else {
parts.sort_by((left, right) => left.compare(right))
base.to_owned() + "?" + parts.join("&")
}
}
}
}
///|
/// Remove fragments and sort query pairs without decoding user data.
pub fn normalize_url(url : String) -> String {
normalize_query(url)
}
///|
fn normalize_headers(headers : Array[Header], config : MatchConfig) -> String {
let parts : Array[String] = []
for header in headers {
if should_include_header(header.name, config) {
parts.push(
normalized_header_name(header.name) +
"=" +
normalized_header_value(header.value),
)
}
}
parts.sort_by((left, right) => left.compare(right))
parts.join("&")
}
///|
fn normalize_body(body : Body, mode : BodyMatchMode) -> String {
match mode {
Ignore => ""
Exact =>
match body {
Empty => "empty"
Text(value) => "text:" + value.length().to_string() + ":" + value
Base64(value) => "base64:" + value.length().to_string() + ":" + value
}
}
}
///|
/// Build a deterministic, transport-independent key for a request.
pub fn normalize_request(request : Request, config : MatchConfig) -> String {
let method = request.method.trim().to_owned().to_upper()
let headers = normalize_headers(request.headers, config)
let body = normalize_body(request.body, config.body_mode)
"method=" +
method +
"\nurl=" +
normalize_url(request.url) +
"\nheaders=" +
headers +
"\nbody=" +
body
}