///|
/// HTTP 头名的大小写无关规范化:先裁掉两侧空白,再把 `A`-`Z` 折成 `a`-`z`。
///
/// HTTP/1.1 规定头字段名只在 ASCII 范围内大小写不敏感(RFC 9110 §5.1),
/// 所以这里不做 Unicode 大小写折叠,和 axios 的 `normalizeHeader` 在 ASCII 上等价。
///
/// 包内可见(`fn` 而非 `pub fn`):规范化是 `Headers` 的封装细节,
/// 外部使用者不应该依赖「key 是小写的」这一实现事实。
fn normalize_name(name : StringView) -> String {
let buf = StringBuilder(size_hint=name.length())
for c in name {
buf.write_char(c.to_ascii_lowercase())
}
buf.to_string()
}
///|
/// 大小写不敏感的 HTTP 头集合,对应 axios 的 `AxiosHeaders`。
///
/// 内部以小写化后的头名作为 `Map` 的 key,值里再记住写入时的原始拼写,
/// 于是 `get("Content-Type")` 与 `get("content-type")` 命中同一条记录,
/// 而遍历/打印时仍然保留调用方书写时的形状(与 axios「首次出现的拼写胜出」一致)。
///
/// 与 axios 相比的两处有意简化(README 里有完整清单):
/// - 不支持 axios 用 `false` 表示「这条头禁止被默认值覆盖」的哨兵值;
/// - 同一个头名只能有一个字符串值,不展开多值头(axios 允许数组)。
///
/// 所有变更方法都返回新实例而不是就地修改:`Config` 是值语义的,
/// 合并配置时同一份 `Headers` 可能被多个 `Config` 引用,
/// 返回新值可以避免「改一个实例的头,另一个实例跟着变」这类共享可变状态 bug。
pub struct Headers {
priv entries : Map[String, (String, String)]
}
///|
/// 空的头集合。
pub fn Headers::new() -> Headers {
{ entries: Map([]), }
}
///|
/// 从「头名, 值」序列构造;同名(忽略大小写)时后出现的覆盖先出现的。
pub fn Headers::from_pairs(pairs : Array[(String, String)]) -> Headers {
let mut headers = Headers::new()
for pair in pairs {
headers = headers.set(pair.0, pair.1)
}
headers
}
///|
/// 写入一条头,返回新的集合;已有同名头(忽略大小写)时会被整体覆盖。
///
/// `(spelling, value)` 一起替换:拼写以最后一次写入为准,
/// 这与 axios 的 `AxiosHeaders::set` 默认覆盖行为一致。
pub fn Headers::set(
self : Headers,
name : StringView,
value : String,
) -> Headers {
let spelling = name.trim().to_owned()
let entries = self.entries.copy()
entries[normalize_name(spelling)] = (spelling, value)
{ entries, }
}
///|
/// 仅当同名头(忽略大小写)尚不存在时写入,返回新的集合。
///
/// 用于「补默认值但不能盖掉用户显式设置」的场景,例如自动补 `Content-Type`。
pub fn Headers::set_if_absent(
self : Headers,
name : StringView,
value : String,
) -> Headers {
if self.has(name) {
self
} else {
self.set(name, value)
}
}
///|
/// 按名取值,大小写不敏感。查不到返回 `None`,而不是空串,
/// 以便区分「这个头不存在」和「这个头的值是空字符串」。
pub fn Headers::get(self : Headers, name : StringView) -> String? {
match self.entries.get(normalize_name(name.trim())) {
Some(entry) => Some(entry.1)
None => None
}
}
///|
/// 是否存在该头,大小写不敏感。
pub fn Headers::has(self : Headers, name : StringView) -> Bool {
self.entries.contains(normalize_name(name.trim()))
}
///|
/// 删除一条头,返回新的集合;不存在时原样返回。
pub fn Headers::remove(self : Headers, name : StringView) -> Headers {
let entries = self.entries.copy()
entries.remove(normalize_name(name.trim()))
{ entries, }
}
///|
/// 以 `other` 覆盖自身,返回新的集合(大小写不敏感,同名时 `other` 胜出)。
///
/// 这是 `flatten_headers` 实现「common 层 < 按方法层 < 请求级平铺」优先级的基石:
/// 只要按优先级从低到高依次 merge,最终结果天然就是高优先级覆盖低优先级。
pub fn Headers::merge(self : Headers, other : Headers) -> Headers {
let entries = self.entries.copy()
for key, entry in other.entries {
// key 已是规范化后的小写名,直接落库即可保证两边同名项正确对撞。
entries[key] = entry
}
{ entries, }
}
///|
/// 头的条数。
pub fn Headers::length(self : Headers) -> Int {
self.entries.length()
}
///|
/// 是否一条头都没有。
pub fn Headers::is_empty(self : Headers) -> Bool {
self.entries.is_empty()
}
///|
/// 展开为「头名, 值」数组,头名保留原始拼写,顺序为插入顺序。
///
/// 顺序有意义:请求级平铺的头是最后 merge 进来的,
/// 因此它们在数组尾部,也是发送到网络上时最靠后的覆盖者。
pub fn Headers::entries(self : Headers) -> Array[(String, String)] {
let result = []
for _, entry in self.entries {
result.push(entry)
}
result
}
///|
/// 渲染为 `名: 值` 形式,多条之间用 `", "` 连接。
/// 只用于日志与断言,不参与任何协议编码。
pub fn Headers::to_string(self : Headers) -> String {
let buf = StringBuilder()
let mut first = true
for _, entry in self.entries {
if !first {
buf.write_string(", ")
}
first = false
buf.write_string(entry.0)
buf.write_string(": ")
buf.write_string(entry.1)
}
buf.to_string()
}
///|
pub impl Show for Headers with fn to_string(self) {
self.to_string()
}
///|
/// 显式声明把 `Show` 的方法挂成常规方法。
/// 隐式挂载已被标记为废弃,这里按当前推荐写法显式 extend;
/// `to_string` 不用列进来,因为上面已经有一个同名的固有方法。
pub extend Headers with Show::{output}
///|
/// 调试输出与 `Show` 共用同一套渲染,避免出现两种格式。
pub impl @debug.Debug for Headers with fn to_repr(self) {
@debug.Repr::string(self.to_string())
}
///|
pub extend Headers with @debug.Debug::{to_repr}
///|
/// 语义相等:只比较「规范化后的头名 → 值」,忽略保存的原始拼写。
/// 因为头名大小写不敏感,`Content-Type: a` 与 `content-type: a` 应当判等,
/// 直接比内部 Map 会把拼写差异泄漏成不相等。
pub impl Eq for Headers with fn equal(self, other) {
if self.entries.length() != other.entries.length() {
return false
}
for key, entry in self.entries {
match other.entries.get(key) {
Some(other_entry) => if entry.1 != other_entry.1 { return false }
None => return false
}
}
true
}
///|
pub extend Headers with Eq::{equal, not_equal}