sml

SML { ❄ }

第 9 章:进阶——功能组合与设计模式

第 9 章:进阶——功能组合与设计模式

前 8 章分别学了键值、块、片段、include、契约、环境变量、多语言集成。本章把它们组合起来——真实的 SML 配置很少只用一项能力。

学完这一章,你会知道:一个生产级 SML 配置库应该长什么样、为什么这样写、怎么取舍。

9.1 一张全景图

先俯瞰 SML 的"能力栈":

能力 解决什么 默认
数据 键值、块、数组、标量 描述是什么
复用 片段 @name / &name 值的复制
拆分 include 跨文件复用
隔离 as ns(点分路径) 命名空间
默认 无扩展名 ⇒ as foo 隐式命名空间
批量化 多目标 ,、import 别名 一次含多
匹配 * 通配 glob 文件名匹配
匹配 re: / /.../ 正则 复杂文件名
改写 *->*.sml 把任意后缀当 sml
约束 @contract / @is 形状校验
注入 $env.VAR 环境变量
转义 \n \u{XXXX} 字符串转义
多语言 Rust/C/JS/Lua 解析器 跨生态

默认开的七件套让你"开箱即用";默认关的四件按需 opt-in,不重蹈 YAML 复杂化覆辙。

9.2 模式 1:模块化配置库

把"通用配置片段 + 契约"抽成共享库,业务项目 include 进来。

库文件(sml-lib/net.sml

@contract Service {
    name:   str
    port:   int  min 1 max 65535
    region: str
}

@default-http {
    port: 80
    region: cn-north-1
}

@default-https {
    port: 443
    region: cn-north-1
}

业务项目(app.sml

@version v1

# 整文件作命名空间
include "sml-lib/net" as net

gateway {
    @is net.Service
    &net.default-http
    name: api-gw
}

admin {
    @is net.Service
    &net.default-https
    name: admin-panel
    port: 8443
}

要点:

9.3 模式 2:可裁剪 schema(feature flag)

契约字段也可以按需裁剪——用 optional / default 让同一个契约在不同环境有不同的"必填集"。

@contract Database strict {
    host:     str
    port:     int  default 5432 min 1 max 65535
    user:     str  default app
    password: str  ?               # 可选:本地/测试用空
    sslmode:  enum(disable, allow, require) default require
}

一份契约,多个 profile。靠"optional + default + 严格模式"三件套,不需引入多个契约。

9.4 模式 3:按环境生成(env overlay)

同一份基础配置,在不同环境叠加不同片段——典型三段式 dev / staging / prod:

# base.sml
@base {
    region: cn-north-1
    timeout: 30
    log_level: info
}

service api { &base port: 8080 name: api }

# env/dev.sml
include "base" as cfg
service api { &cfg.base }
# 覆盖 log_level 不必写完整路径:可直接追加
service api { log_level: debug }

注意:SML 暂时不内置 “merge by name” 的复杂合并语义;上述写法是靠契约 + include 命名空间手写覆盖。更复杂的 merge 推荐在 Rust/JS 侧用 sml-merge 之类的库做(参考 https://github.com/snoware/sml-merge)。

9.5 模式 4:include + 契约 + $env 三件套

这是最常见也最稳的生产用法:

# common.sml
@contract Service {
    name: str
    port: int  default 8080
    debug: bool default false
}

# app.sml
@version v1

include "common" as cfg

gateway {
    @is cfg.Service
    name: gateway
    port: 9090
    debug: $env.DEBUG
}

secrets {
    api_key: $env.API_KEY
    db_password: $env.DB_PASSWORD
    webhook: $env.OPTIONAL_WEBHOOK   # 未设 -> 空串
}

为什么稳:

9.6 模式 5:协议级契约(“前后端共享 schema”)

SML 契约是纯声明,不绑语言。同一份契约可同时约束:

@contract User {
    id:    str
    email: str
    role:  enum(user, admin, owner) default user
    age:   int  min 0 max 150 ?
}

后端 Rust:

// 由 swsml-derive 宏从 .sml 自动派生
#[derive(FromSmlContract)]
struct User { id: String, email: String, role: Role, age: Option<u8> }

前端 TS:

// 由 swml-ts-codegen 从 .sml 自动生成
type User = { id: string; email: string; role: 'user'|'admin'|'owner'; age?: number };

SML 本身就是"协议级"——一份 schema、四端共享、行为一致。

9.7 模式 6:契约组合 + 递归

array[契约名] 表达"契约数组"——适合列表型数据:

@contract Endpoint { host: str port: int }
@contract Service {
    name:  str
    main:  Endpoint                # 单个
    peers: array[Endpoint]         # 列表
    back:  Endpoint?               # 可选
}

@is Service
name: gateway
main:  { host: a port: 1 }
peers: [ { host: b port: 2 } { host: c port: 3 } ]

递归层数不限(解析期检测环引用;环引用 = 错误)。

9.8 模式 7:glob + 契约(“所有模块统一 schema”)

需要 @feature enable glob 开启。

@feature enable glob

@contract Module {
    name:  str
    entry: str
    deps:  array[str] ?
}

#  modules/ 下所有 .sml 文件作为子模块加载,并各自校验
include "modules/*.sml" as modules

# 之后可用 modules.auth / modules.billing 访问
gateway {
    primary: modules.auth
    fallback: modules.billing
}

适合插件系统:每个插件是独立 .sml 文件,统一契约校验,统一命名空间访问。

9.9 模式 8:re: 正则做"按命名规则"批处理

需要 @feature enable regex 开启。

@feature enable regex

# 加载所有 v 开头的 .sml(如 v1.sml / v2.sml
include "re:^v[0-9]+\\.sml$" as versions

# 加载所有 .json 改写为 .sml 解析
include "configs/re:.*\\.json$" -> .sml

re: 前缀表明后面是正则;正则语法是手写子集. * + ? ^ $ [a-z] 即可满足 90% 场景,避免引入完整 regex 引擎)。

9.10 取舍原则(什么时候用什么)

9.11 反模式(请避免)

9.12 动手试一试

把第 8 章的"完整项目"按本节"模式 5(include + 契约 + $env)“重构一遍:

  1. common.sml 抽成共享契约 + 共享片段。
  2. app.sml 改成 include "common" as cfg
  3. 密钥全部用 $env.*
  4. cargo test(或对应语言的契约测试)跑通解析与校验。

第 10 章:feature 完整参考

动手练习

读完本章,在下面的编辑器里直接修改 SML 并点“运行”,立刻看到解析结果或校验错误——有输出才能高效学习。

✍ 动手练习 用契约组合:定义 @contract Endpoint { host: str, port: int }@contract Service { name: str, main: Endpoint, peers: array[Endpoint] ? },再用 @is Service 写 main 与一个 peers 元素。
💡 提示:Endpoint 被 main 字段与 array[Endpoint] 两处复用,这就是「契约组合」。
✍ 自测考题:第 9 章自测:功能组合 得分 0 / 4
Q1. 契约组合指什么?
Q2. 哪种模式适合“多环境共享基础配置”?
Q3. 判断:array[Endpoint] ? 末尾的 ? 表示该字段可省略。
Q4. 设计模式里“校验前置”推荐用?