// 配置合并契约(对应 axios `lib/core/mergeConfig.js`),契约细节见 docs/02-config-merge.md。
//
// 它曾经是独立的 `merge` 包,现在与 `Config` 同包:`Config.data` 是私有字段之后,
// 只有 config 包能构造 `Config`(跨包的记录字面量与记录展开都会被编译器拒绝,
// 报 `cannot use struct update syntax ... because it has private fields`),
// 而 `merge_config` 必须逐字段构造出一份新配置。
// 同包的额外好处是「字段 → 策略」的同步被编译器强制:往 `Config` 加字段而忘了
// 在这里选一档策略,是编译错误,不是静默漏合并。
///|
/// 策略 1:只取请求级(对应 axios `mergeConfig` 的 `valueFromConfig2`)。
///
/// `url` / `http_method` / `data` 用这个策略:默认值里就算有同名字段也会被丢弃。
/// 理由是这三项在语义上回答的是「这一次请求要做什么」,
/// 从实例默认值里继承一个 url 或 body 没有意义,
/// 而且很容易造成「配置里没写 url 却发出了上一次的请求」这类意外。
fn[T] value_from_request(base : T?, request : T?) -> T? {
ignore(base)
request
}
///|
/// 策略 2:请求级优先,否则回退默认值(对应 `defaultToConfig2`)。
///
/// 这是最符合直觉的一档:请求里没写的配置项就沿用实例默认值。
/// 函数字段(`params_serializer`)同样走这一档,所以请求级提供时是**整体替换**:
/// axios 的 `mergeConfig` 把它们都登记成 `defaultToConfig2`,两个函数不会被串成
/// 一条链(要叠加就在自己的函数里调另一个)。
fn[T] prefer_request(base : T?, request : T?) -> T? {
match request {
Some(_) => request
None => base
}
}
///|
/// 策略 4:请求级「存在即生效」(对应 `mergeDirectKeys`)。
///
/// 在 axios 里这一档与策略 2 的差别是:它只看键「存不存在」,
/// 连显式的 `undefined`/`null` 也算存在,于是可以用它把默认的
/// `validateStatus` 整个关掉(传 `null` 即可)。
///
/// 本项目的字段是 `Option`,「存在」就等于 `Some`,所以实现与策略 2 同形。
/// 保留独立的名字是为了标明语义:想关掉状态码校验时,
/// 传一个恒真的函数(`with_validate_status(fn(_) { true })`),
/// 而不是删掉这个字段——删掉只会退回默认的 2xx 规则。
fn[T] request_overrides(base : T?, request : T?) -> T? {
prefer_request(base, request)
}
///|
/// `Headers` 的深合并(axios 里唯一按大小写不敏感方式合并的字段)。
///
/// axios 的 `AxiosHeaders` 把 `Content-Type` 与 `content-type` 视为同一个键,
/// 本项目的 `Headers` 同样在存储时规范化键,所以直接交给 `Headers::merge`:
/// 请求级同名头覆盖默认值,默认值里请求级没提的头则保留下来。
///
/// 只有一侧存在时直接返回该侧,不做拷贝——`Headers` 的所有变更方法都返回新实例,
/// 共享引用不会被后续操作改坏(axios 在这里要 clone,是因为它的普通对象是可变的)。
fn merge_headers(base : Headers?, request : Headers?) -> Headers? {
match (base, request) {
(None, None) => None
(Some(base), None) => Some(base)
(None, Some(request)) => Some(request)
(Some(base), Some(request)) => Some(base.merge(request))
}
}
///|
/// `Auth` 的逐字段深合并,对应 axios 对 `{username, password}` 这种普通对象的递归合并。
///
/// 「默认值只给 username、请求只给 password」能拼出完整凭据,
/// 而不是请求级的对象整体替换掉默认值。
fn merge_auth(base : Auth?, request : Auth?) -> Auth? {
match (base, request) {
(None, None) => None
(Some(base), None) => Some(base)
(None, Some(request)) => Some(request)
(Some(base), Some(request)) =>
Some({
username: prefer_request(base.username, request.username),
password: prefer_request(base.password, request.password),
})
}
}
///|
/// `Proxy` 的逐字段深合并,与 `merge_auth` 同一档策略。
///
/// 内层 `auth` 递归交回 `merge_auth`:代理凭据同样享受逐字段合并
/// (默认值给 `username`、请求给 `password` 照样拼得齐),
/// 而不是「请求级带了凭据就把代理对象整个换掉、顺手丢掉默认值的 host」。
fn merge_proxy(base : Proxy?, request : Proxy?) -> Proxy? {
match (base, request) {
(None, None) => None
(Some(base), None) => Some(base)
(None, Some(request)) => Some(request)
(Some(base), Some(request)) =>
Some({
protocol: prefer_request(base.protocol, request.protocol),
host: prefer_request(base.host, request.host),
port: prefer_request(base.port, request.port),
auth: merge_auth(base.auth, request.auth),
})
}
}
///|
/// 按方法分层的默认头(`common_headers` 之外的第二层)深合并。
///
/// 合并发生在「桶」的粒度上:请求级只想改 GET 的头时,
/// 其它方法(POST/PUT…)的桶必须原样保留,因此不能整体替换 Map。
fn merge_method_headers(
base : Map[Method, Headers]?,
request : Map[Method, Headers]?,
) -> Map[Method, Headers]? {
match (base, request) {
(None, None) => None
(Some(base), None) => Some(base)
(None, Some(request)) => Some(request)
(Some(base), Some(request)) => {
// 先 copy 再改:base 可能被别的 Config 引用着,就地修改会破坏值语义。
let merged = base.copy()
for meth, headers in request {
let bucket = match merged.get(meth) {
Some(existing) => existing.merge(headers)
None => headers
}
merged[meth] = bucket
}
Some(merged)
}
}
}
///|
/// 合并两份配置,返回新配置。`base` 通常是实例的默认值,`request` 是本次请求的配置。
///
/// 这是 axios `mergeConfig(config1, config2)` 的对应物。axios 用一张
/// 「字段 → 合并函数」的表来遍历所有键,MoonBit 没有运行时反射,
/// 所以这里改成逐字段显式调用对应策略——好处是每个字段用哪一档策略
/// 在源码里一眼可见,不需要再去查那张表。
///
/// 策略分配与 axios 保持一致:
/// - 只取请求级:url / http_method / data
/// - 请求级优先、否则默认:base_url / timeout / max_redirects / response_encoding /
/// allow_absolute_urls / params_serializer / on_upload_progress / on_download_progress
/// - 深合并:params / auth / proxy / headers / common_headers / method_headers
/// - 存在即生效:validate_status
///
/// 注意 `allow_absolute_urls` 这类标量在 axios 里走的是默认策略
/// `mergeDeepProperties`,而「深合并」作用在标量上退化成「请求级有就用请求级」,
/// 结果与「请求级优先、否则默认」相同。`max_redirects` 也一样:它不在 axios 的
/// `mergeMap` 里,落到默认策略,标量上等价于请求级优先(所以实例默认值里的
/// `max_redirects` 会被请求级的值覆盖,包括用 0 关掉跟随)。
///
/// `None` 一律表示「未提供」,永远是回退而不是覆盖;
/// 数组(例如 `params` 里的数组)是整体替换而不是拼接,
/// 因为 axios 的 `utils.merge` 只对普通对象递归。
pub fn merge_config(base : Config, request : Config) -> Config {
{
// 策略 1:只取请求级
url: value_from_request(base.url, request.url),
http_method: value_from_request(base.http_method, request.http_method),
data: value_from_request(base.data, request.data),
// 策略 2:请求级优先,否则回退默认值
base_url: prefer_request(base.base_url, request.base_url),
timeout: prefer_request(base.timeout, request.timeout),
max_redirects: prefer_request(base.max_redirects, request.max_redirects),
response_encoding: prefer_request(
base.response_encoding,
request.response_encoding,
),
allow_absolute_urls: prefer_request(
base.allow_absolute_urls,
request.allow_absolute_urls,
),
// `params_serializer` 与 `params` 是配套的两个字段,但策略不同:它走策略 2
// 而不是深合并——axios 把 `paramsSerializer` 登记成 `defaultToConfig2`,
// 所以请求级提供就**整体替换**实例默认值,两个序列化器不会被拼起来
// (函数也没法「合并」)。
params_serializer: prefer_request(
base.params_serializer,
request.params_serializer,
),
// 两个进度回调同样走策略 2(axios 把它们登记成 `defaultToConfig2`),
// 请求级提供即整体替换实例默认值,不会出现「两个回调都被调用」。
on_upload_progress: prefer_request(
base.on_upload_progress,
request.on_upload_progress,
),
on_download_progress: prefer_request(
base.on_download_progress,
request.on_download_progress,
),
// 取消句柄同样走策略 2:两个 token 没法「合并」成第三个,只能二选一。
// 请求级提供就整体替换实例默认值——实例级 token 是「本实例发出的所有请求
// 共用一个取消信号」的用法,不该被某次请求悄悄换掉后其余请求还留着旧的。
cancel_token: prefer_request(base.cancel_token, request.cancel_token),
// 策略 3:深合并
params: merge_json_option(base.params, request.params),
auth: merge_auth(base.auth, request.auth),
proxy: merge_proxy(base.proxy, request.proxy),
headers: merge_headers(base.headers, request.headers),
common_headers: merge_headers(base.common_headers, request.common_headers),
method_headers: merge_method_headers(
base.method_headers,
request.method_headers,
),
// 策略 4:请求级存在即生效
validate_status: request_overrides(
base.validate_status,
request.validate_status,
),
}
}
///|
/// 把三层头按 axios 的优先级拍平成一份:`common` < 按方法 < 请求级平铺。
///
/// axios 在 `_request` 里做的是
/// `AxiosHeaders.concat(headers.common, headers[config.method], headers)`
/// 然后把 `common` / 各方法名从对象上删掉;`concat` 的语义是后者覆盖前者,
/// 所以这里从低到高依次 `merge` 就能得到同样的结果。
/// `flatten_headers(common, None, flat, Get)` 这种缺层的情况直接跳过即可,
/// 空层不参与合并不会改变结果。
pub fn flatten_headers(
common : Headers?,
method_headers : Map[Method, Headers]?,
flat : Headers?,
meth : Method,
) -> Headers {
let mut result = Headers::new()
match common {
Some(common) => result = result.merge(common)
None => ()
}
match method_headers {
Some(buckets) =>
match buckets.get(meth) {
Some(bucket) => result = result.merge(bucket)
None => ()
}
None => ()
}
match flat {
Some(flat) => result = result.merge(flat)
None => ()
}
result
}