sml

SML { ❄ }

第 5 章:契约系统

第 5 章:契约系统

前面学的片段是"值的复用"。契约(Contract)是"形状的约束"——它定义"一个块应该有哪些字段、各自什么类型、是否必填、默认值多少、取值范围",并在解析期就校验,而不是等你运行程序才发现问题。

适用场景:用 SML 做应用配置时,契约就是你的 Schema。改错字段名、漏填必填项、填了超出范围的端口号——解析时就直接报错,并告诉你精确到行列。

5.1 定义契约:@contract

@contract ResenderConfig loose {
    api_key:     str                # 必填字符串
    port:        int  default 8080 min 1 max 65535
    debug:       bool default false
    mode:        enum(active, disabled) default active
    tags:        array[str] ?       # 可选字符串数组
}

字段修饰符一览:

修饰符 含义
str / int / num / bool 字段类型
enum(a, b, c) 枚举,取值须在其中之一
array[T] 数组,元素类型为 T(如 array[int]array[str]
?optional 可选字段
required 显式必填(默认即必填,可不写)
default <值> 缺失时填入默认值(同时自动视为可选)
min <数> / max <数> 数值取值范围(含端点)

5.2 应用契约:@is

两种写法:

# 写法一:匿名块顶层直接 @is
@contract Cfg loose { api_key: str port: int default 8080 }
@is Cfg
api_key: re_abc
port: 8080
# 写法二:块级 @is
server prod {
    @is Cfg
    api_key: re_prod
    port: 9090
}

校验发生在解析期:违反契约直接返回带位置的精确错误,例如 contract: Service — 字段 main.port 大于最大值 65535

5.3 严格 vs 宽松

@contract Metrics loose { latency: num min 0 }

loose 只放宽"未声明字段",已声明字段照样校验类型 / 区间 / 必填。

5.4 组合契约(递归引用)

契约之间不共享字段,而是"字段的类型是另一个契约"——直接填契约名即可,不引入新语法:

@contract Endpoint { host: str port: int }
@contract Service {
    name:  str
    main:  Endpoint          # 引用另一个契约
    peers: array[Endpoint]   # 契约数组
}

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

嵌套块会递归校验并回填默认值。被引用契约允许在 @is 之后才定义(引用在 @is 时才解析)。

5.5 真实范例:resender 邮件工具

resender 用 SML 契约做 AppConfig 持久化:

@contract ResenderConfig loose {
    api_key:    str
    from:       str
    to:         array[str]
    subject:    str default "Hello"
    port:       int default 465  min 1 max 65535
    tls:        bool default true
}

@is ResenderConfig
api_key: re_xxxxxx
from: me@example.com
to: [ alice@example.com bob@example.com ]
subject: Weekly Report
port: 465
tls: true

其 Rust 端维护 CONFIG_CONTRACT 常量,保存时把配置序列化回 SML 并自动附上 @is ResenderConfig,读取时再校验——“契约即 Schema” 的典型用法。

5.6 动手试一试

给你的游戏服务器集群(第 3 章)加契约:

@contract Server strict {
    name: str
    port: int min 1024 max 65535
    region: str
}

@common {
    region: ap-east-1
    max_players: 64
}

lobby {
    @is Server
    &common
    port: 25565
    name: 大厅
}

试着把 port: 80(小于 1024)写进去,看解析器是否报错。

第 6 章:环境变量与转义

动手练习

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

✍ 动手练习 定义 @contract Server strict { name: str, port: int min 1024 max 65535, region: str },并用 @is Server 写一份合法数据(name=大厅, port=25565, region=ap-east-1)。
💡 提示:把 port 改成 80(小于 1024),看契约校验如何报错。
✍ 自测考题:第 5 章自测:契约系统 得分 0 / 4
Q1. @contract Server loose { port: int min 1024 } 中 loose 表示?
Q2. port: int min 1024 max 65535,下面哪个值会校验失败?
Q3. role: enum(user, admin, owner) default user 表示?
Q4. 判断:@is Server 之后写的数据,字段类型不对也会被契约拦下。