///|
/// One HTTP field line. Field names are normalized to lowercase ASCII at
/// construction; field values are retained verbatim.
pub(all) struct Header {
name : String
value : String
} derive(Debug, Eq)
///|
/// Ordered, duplicate-preserving collection of HTTP fields.
pub(all) struct Headers {
entries : Array[Header]
} derive(Debug, Eq)
///|
/// Construct a field line. Invalid names are retained so later parsing can emit
/// a structured diagnostic rather than panic.
pub fn header(name : String, value : String) -> Header {
{ name: @lex.lower(name), value, }
}
///|
pub fn Headers::empty() -> Headers {
{ entries: [], }
}
///|
pub fn Headers::from_array(entries : Array[Header]) -> Headers {
{ entries: entries.copy(), }
}
///|
/// Return a new collection with one field appended.
pub fn Headers::add(self : Headers, name : String, value : String) -> Headers {
let entries = self.entries.copy()
entries.push(header(name, value))
{ entries, }
}
///|
/// Return all field-line values for a case-insensitive field name.
pub fn Headers::values(self : Headers, name : String) -> Array[String] {
let normalized = @lex.lower(name)
let values : Array[String] = []
for entry in self.entries {
if entry.name == normalized {
values.push(entry.value)
}
}
values
}
///|
/// Return the first matching field line.
pub fn Headers::first(self : Headers, name : String) -> String? {
let normalized = @lex.lower(name)
for entry in self.entries {
if entry.name == normalized {
return Some(entry.value)
}
}
None
}
///|
pub fn Headers::contains(self : Headers, name : String) -> Bool {
self.first(name) is Some(_)
}
///|
/// Combine repeated list-valued field lines using the RFC 9110 comma form.
pub fn Headers::combined(self : Headers, name : String) -> String? {
let values = self.values(name)
if values.length() == 0 {
return None
}
let output = StringBuilder()
for index, value in values {
if index > 0 {
output.write_string(", ")
}
output.write_string(value)
}
Some(output.to_string())
}
///|
/// Return a copy with every field of this name removed.
pub fn Headers::without(self : Headers, name : String) -> Headers {
let normalized = @lex.lower(name)
let entries : Array[Header] = []
for entry in self.entries {
if entry.name != normalized {
entries.push(entry)
}
}
{ entries, }
}
///|
/// Return a copy where all old values are replaced by one field line.
pub fn Headers::replace(
self : Headers,
name : String,
value : String,
) -> Headers {
self.without(name).add(name, value)
}
///|
/// Report invalid field names without rejecting the complete message.
pub fn Headers::diagnostics(self : Headers) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for entry in self.entries {
if !@lex.is_token(entry.name) {
diagnostics.push({
level: Error,
code: "HTTP_FIELD_NAME_INVALID",
message: "field name is not a valid HTTP token",
field_name: Some(entry.name),
})
}
if entry.value.contains_char('\u{0000}') ||
entry.value.contains_char('\r') ||
entry.value.contains_char('\n') {
diagnostics.push({
level: Error,
code: "HTTP_FIELD_VALUE_UNSAFE",
message: "field value contains NUL or a line break",
field_name: Some(entry.name),
})
}
}
diagnostics
}