sml

SML { ❄ }

中文 EN

Chapter 9: Advanced - Function Combination and Design Patterns

Chapter 9: Advanced - Function Combination and Design Patterns

The first 8 chapters respectively learned key values, blocks, fragments include、 Contract, environmental variables, and multilingual integration. This chapter combines them - real SML configurations rarely use only one capability.

After completing this chapter, you will know what a production level SML configuration library should look like, why it should be written this way, and how to make choices.

9.1 A panoramic view

First, let’s take a look at SML’s’ capability stack ‘:

LayerAbilitySolutionDefault
DataKey value, block, array, scalarDescriptionOpen
ReuseFragment @name/&nameCopy of valuesOpen
SplitincludeCross file ReuseOpen
Isolationas ns (dotted path)namespaceopen
DefaultNo extension ⇒ as fooImplicit namespaceOpen
BatchMulti objective ,, import aliasMultiple in one goRelated
Matching* universal globFile name matchingOff
Matchre://.../ RegularComplex File NameOff
Rewrite*->*.smlTreat any suffix as smlOff
Constraint@contract/@isShape VerificationOpen
Injection$env.VAREnvironment VariablesOn
Escaping\n \u{XXXX}String EscapingOpen
MultilingualRust/C/JS/Lua ParserCross Ecological-

The default seven piece set allows you to use it out of the box; Default four on demand opt in for**, avoiding the pitfalls of YAML complexity.

9.2 Mode 1: Modular Configuration Library

Extract the ‘General Configuration Fragment+Contract’ into a shared library and bring in the business project include.

Library file (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
}

Business Project (app.sml)

@version v1

# Create a namespace for the entire file
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
}

main points:

-include "sml-lib/net" without extension ⇒ Implicit as net (equivalent to explicit as net).

-The namespace isolates the @contract and @name of the library, which are referenced by net.Service/&net.default-http in the business.

-Libraries can be independently released (even in Git submodule/package management).

-When the same library is included by multiple projects, the macro/contract names do not conflict - because they are all installed in different namespaces.

9.3 Mode 2: Customizable schema (feature flag)

Contract fields can also be trimmed as needed - using optional/default allows the same contract to have different ‘required sets’ in different environments.

@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
}

-Production: Fill in all fields, sslmode: require is required.

-Development: password (empty connection+trust authentication) is optional, sslmode: disable

-CI: The contract treats undeclared fields as errors - so mock fields inserted in the testing environment will be immediately detected.

One contract, multiple profiles. By relying on the “optional+default+strict mode” three piece set, there is no need to introduce multiple contracts.

9.4 Mode 3: Generate by Environment (env overlay)

Same basic configuration, stacking different segments in different environments - typical three-stage 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 }
# Covering log_1evel does not require writing the complete path: can be directly appended
service api { log_level: debug }

Note: SML currently does not have built-in complex merge semantics for “merge by name”; The above writing method relies on contract+include namespace handwritten overlay. For more complex merges, it is recommended to use libraries such as sml-merge on the Rust/JS side (see reference) https://github.com/snoware/sml-merge ).

9.5 Mode 4: Three piece set of include+contract+$env

This is the most common and stable production usage:

# 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   # 未设 -> 空串
}

Why Stable:

-Contract Guarantee Structure.

-include namespace ensures that contracts do not pollute the scope of the main file.

-$env prevents sensitive values from being stored.

-The disadvantage (in development state) is also obvious: $env is evaluated only during the parsing period, and the value cannot be seen during IDE debugging - the solution is to use sml-playground or write a mock env loader yourself.

9.6 Mode 5: Protocol level contract (“front-end and back-end shared schema”)

The SML contract is a pure declaration without language binding. The same contract can simultaneously bind:

-Configuration file (.sml)

-Types generated by backend Go/Rust code

-Front end TS type (automatically generated)

-API request/response verification

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

Backend Rust:

// Automatically derived from. sml by swsml derive macro
# [derive(FromSmlContract)]
struct User { id: String, email: String, role: Role, age: Option<u8> }

Front end TS:

// Automatically generated from. sml by swml ts codegen
type User = { id: string; email: string; role: 'user'|'admin'|'owner'; age?: number };

SML itself is a ‘protocol level’ - a schema, shared across four endpoints, and consistent behavior.

9.7 Mode 6: Contract combination+recursion

Using array[契约名] to express “contract array” - suitable for list type data:

@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 } ]

Unrestricted number of recursion layers (parsing period detects loop references; loop references=errors).

9.8 Pattern 7: glob+contract (“Unified schema for all modules”)

@feature enable glob needs to be enabled.

@feature enable glob

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

# Load all. sml files under modules/as submodules and verify them separately
include "modules/*.sml" as modules

# Afterwards, modules. auth/modules. billing can be used for access
gateway {
    primary: modules.auth
    fallback: modules.billing
}

Suitable for plugin system: Each plugin is an independent. sml file, with unified contract verification and unified namespace access.

9.9 Mode 8: re: Regularly perform batch processing according to naming rules

@feature enable regex needs to be enabled.

@feature enable regex

# Load all. sml files starting with 'v' (such as v1. sml/v2. sml)
include "re:^v[0-9]+\\.sml$" as versions

# Load all. json files and rewrite them to. sml for parsing
include "configs/re:.*\\.json$" -> .sml

The re: prefix indicates that it is followed by regularization; The regular syntax is handwritten subset (. * + ? ^ $ [a-z] can meet 90% of the scenarios, avoiding the introduction of a complete regex engine).

9.10 Principle of Choice (When to Use What)

-Stable structure and frequent field changes → Contract+default value+enum.

-Multiple repeated fields, combined configuration ->fragment+include.

-Multiple teams share a schema → modular library (include+namespace).

-Same configuration for multiple environments → Contract as skeleton, $env injected with differences.

-Plug in extension → glob+namespace.

-Complex file name matching → re: regular.

-Mixed source (.json/.yaml used as .sml) → ext rewrite -> .sml.

9.11 anti pattern (please avoid)

-One huge contract covers all scenarios - there are too many required fields, and team members roast.

-Nesting namespaces more than 3 layers (a.b.c.d) - Decreased readability, it is better to split files.

-The executable logic in the contract - SML contract is a declaration, not a script; To calculate, use the host language.

-$env is used as a “dynamic configuration source” - it is only evaluated during the parsing period, and runtime changes are invalid; Please use code for runtime configuration.

-Include nested more than 5 layers - will slow down reading; It’s time to draw inventory.

9.12 Give it a try with your hands

Refactor the “Complete Project” in Chapter 8 according to the “Mode 5 (include+contract+$env)” in this section:

  1. Extract common.sml into a shared contract and shared fragments.

  2. Change app.sml to include "common" as cfg.

  3. Use $env.* for all keys.

  4. Use cargo test (or corresponding language contract testing) to run parsing and verification.

→ [Chapter 10: Complete Reference to Features](/en/book/ch10 features)

Hands on practice

After reading this chapter, directly modify SML in the editor below and click “Run” to immediately see the parsing results or validation errors - having output is necessary for efficient learning.

✍ 动手练习 用契约组合:定义 @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. 设计模式里“校验前置”推荐用?