Sign in

    moonlibconfig

    A pure MoonBit reader and writer for a documented subset of libconfig 1.8.2

    libconfig
    configuration
    parser
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    17

    No dependencies

    #moonlibconfig

    CI Mooncakes License

    MoonBit 原生 libconfig 格式配置库,提供解析、类型化查询、配置编辑和规范化写出能力。

    适合读取已有 C/C++ 应用的 libconfig 配置、生成部署参数,以及在 Wasm、JavaScript 和 native 后端复用配置处理逻辑。核心由纯 MoonBit 实现,不依赖 C/C++ FFI,文件读取和保存由调用方负责。

    #特性

    • 保留配置类型:区分32位整数、64位整数、浮点数、布尔值、字符串,以及 group、同类型标量 array 和异构 list。
    • 读取 libconfig 文本:支持嵌套配置、三类注释、字符串转义与拼接、尾逗号、多进制整数和64位整数标记。
    • 查询和编辑:支持命名成员、列表/数组索引、路径查询、严格类型提取,以及字段新增、替换和删除。
    • 保存字符串字节:保留非 UTF-8 内容;需要 MoonBit String 时显式执行严格 UTF-8 解码。
    • 确定性写出:生成可重读的规范化 ASCII 文本,保留值及类型,正确处理转义和浮点负零。
    • 明确的错误和限制:提供错误码、消息、字节位置,以及输入、输出、嵌套、节点和字符串大小限制。

    #安装

    在使用这个库的 MoonBit 工程中执行:

    moon add Ryan-1125/moonlibconfig@0.2.0

    下方示例保存为 config_test.mbt,在对应包的 moon.pkg 中加入测试导入:

    import { "Ryan-1125/moonlibconfig" @config, } for "test"

    业务源码使用同一包时,去掉导入声明后的 for "test"。

    #快速开始

    解析配置、查询节点、修改端口、新增字段,再写出并重读:

    ///|
    test "read edit and write service configuration" {
    let source = @config.parse(
    "service={port=9000;enabled=true;} peers=({host=\"localhost\";weight=1L;})",
    )
    assert_eq(source.lookup("service.port").as_int(), 9000)
    assert_eq(source.lookup("peers[0].host").as_string(), "localhost")
    assert_eq(source.lookup("peers.[0].weight").as_int64(), 1L)

    let service = source.lookup("service").with_member("workers", @config.Integer(4))
    let changed = source
    .replaced(@config.parse_path("service"), service)
    .replaced(@config.parse_path("service.port"), @config.Integer(8080))

    let output = @config.stringify(changed)
    let reloaded = @config.parse(output)
    assert_eq(reloaded.lookup("service.port").as_int(), 8080)
    assert_eq(reloaded.lookup("service.workers").as_int(), 4)
    assert_eq(source.lookup("service.port").as_int(), 9000)

    let removed = service.without_member("workers")
    assert_eq(removed, source.lookup("service"))
    }

    运行示例测试:

    moon test

    编辑操作复制修改路径上的容器,保留原有成员顺序,新字段追加到末尾;未修改的分支共享。删除不存在的字段会报 missing_member。

    #类型与 API

    能力接口
    文本/字节解析parse、parse_bytes
    规范化写出stringify
    路径解析与查询parse_path、lookup、get_path
    成员与索引查询get_member、at
    配置编辑replaced、with_member、without_member
    列表/数组编辑inserted、appended、removed_at
    独立校验validate,返回带路径的 ValidationIssue 数组
    源位置与类型诊断parse_document、parse_document_bytes、Document.location、Document.get_*
    类型提取as_int、as_int64、as_double、as_bool、as_string、as_bytes
    类型名称kind

    整数提取采用严格类型规则:as_int64 不会隐式接收32位整数。ScalarArray 只接受同一类型的标量;List 可包含不同类型的值及嵌套容器。

    Text(Bytes) 保存字符串字节,as_string 进行严格 UTF-8 解码;无效 UTF-8 返回 invalid_utf8。as_bytes 可直接提取原始字节。完整签名见 公开 API。

    #错误与资源限制

    错误通过 ConfigError(Diagnostic) 返回。Diagnostic 包含 code、message、offset、line、column;解析位置以字节计,行列从1开始,offset从0开始。查询和写出错误采用默认位置。

    ///|
    test "handle a missing setting" {
    let root = @config.parse("port=9000")
    let code = try {
    ignore(root.lookup("missing"))
    "accepted"
    } catch {
    @config.ConfigError(d) => d.code
    }
    assert_eq(code, "missing_member")
    }

    限制默认值
    输入/规范化输出1 MiB
    容器嵌套64层,根 group 不计入
    值节点100000个,根 group 不计入
    解码后单字符串256 KiB
    查询/替换路径256段

    通过 Limits 调整解析和写出上限,容器嵌套最多设置为256层。例如:

    ///|
    test "apply explicit resource limits" {
    let limits = { ..@config.Limits::default(), max_input: 4096, max_depth: 16 }
    let root = @config.parse("port=9000", limits~)
    assert_eq(root.lookup("port").as_int(), 9000)
    assert_eq(@config.parse(@config.stringify(root, limits~)), root)
    }

    #兼容范围

    格式参照固定为 hyperrealm/libconfig v1.8.2。项目提供明确范围内的独立实现:

    • @include 返回 unsupported_include,不触发文件读取或路径展开。
    • 未知或不完整转义、NUL 字节和非有限浮点明确拒绝。
    • 写出使用十进制数值和规范化排版,不保留原始注释、空白及进制。
    • 两项非十进制整数行为与实测 Windows 上游存在差异;交换大整数时优先使用十进制及显式 L 后缀。

    完整行为、8项实测差异及双方结果见 兼容性说明。资源限制同样适用于调用方手工构造的配置树。

    #示例与验证

    从源码直接运行端到端服务配置示例:

    git clone https://github.com/Ryan-1125/moonlibconfig.git cd moonlibconfig moon update moon run examples/service --target wasm-gc

    该示例读取服务参数、修改端口、规范化写出并重读,异常时返回非零状态。

    验证项结果
    核心与边界测试40项,覆盖三后端
    GitHub ActionsUbuntu、Windows:检查、三后端构建/测试、示例、API及打包
    真实上游差分140个输入,8项精确记录的差异
    共同范围双向读写110个输入通过
    实际 ZIP 独立消费Wasm GC、JS、Windows native 通过
    Mooncakes 0.2.0 独立消费注册表下载安装及三后端 API 调用通过

    真实上游参照使用固定提交构建的外部程序;测试比较接受/拒绝、值结构、类型与数据。详见 验证记录及 CI。

    本地检查:

    moon fmt --check moon check --deny-warn moon test --target wasm-gc --deny-warn moon test --target js --deny-warn moon test --target native --deny-warn moon info moon package --list

    #0.2.0:容器编辑、独立校验与源位置

    inserted(index, value) 在索引前插入,索引等于长度时追加;appended(value) 追加;removed_at(index) 删除已有元素。所有操作复制容器并保留原顺序。array 区分32/64位整数,只接受同类型标量;list 可包含任意配置值。索引越界或类型不符均返回明确错误。

    validate(root, limits~) 不分配序列化文本,检查根类型、名称、重复键、数组元素类型、非有限浮点、NUL及深度/节点/字符串预算。返回多个带路径的问题;遇到全局节点预算时停止遍历,超深分支单独截断。空路径代表根;非法字段名的路径仅用于显示,不保证可用于lookup。此接口的max_input不作为输出预算,文本输入/输出预算由parse/stringify执行。手工修改后的配置应先校验再写出。

    parse_document/parse_document_bytes 保留字段名和值的起始位置。location 接受同样的点号/下标路径;get_int、get_int64、get_double、get_bool、get_bytes、get_string 提供严格查询。类型错误包含路径、期望类型、实际类型和原值位置;字段缺失时使用最近已知父节点的位置。Document位置是原始输入的快照,后续修改共享值树不会重新计算位置,也不用于保真编辑。

    ///|
    test "edit validate and locate configuration" {
    let doc = @config.parse_document("service={port=9000;} peers=(\"one\",\"two\")")
    assert_eq(doc.get_int("service.port"), 9000)
    assert_true(doc.location("service.port") != None)
    let peers = doc.lookup("peers")
    .inserted(1, @config.Text(b"backup"))
    .appended(@config.Text(b"last"))
    .removed_at(0)
    let root = doc.root().replaced(@config.parse_path("peers"), peers)
    assert_eq(@config.validate(root), [])
    assert_eq(@config.parse(@config.stringify(root)), root)
    let invalid = @config.Group([
    ("ports", @config.ScalarArray([@config.Integer(1), @config.Integer64(2L)])),
    ])
    let issues = @config.validate(invalid)
    assert_eq(issues[0].path, "ports[1]")
    assert_eq(issues[0].code, "array_type")
    }

    #许可证与参考

    本项目原创代码使用 Apache-2.0。

    格式参考 hyperrealm/libconfig v1.8.2,上游源码许可证为 LGPL-2.1-or-later。MoonBit 实现依据公开格式文档独立编写;没有复制或翻译上游解析器及测试源码。上游程序仅作为外部行为参照使用,来源和参考范围见 NOTICE。

    ConfigError

    pub suberror ConfigError {
    ConfigError(Diagnostic)
    } derive(
    Debug
    )

    Parsing, access and writing errors share a structured diagnostic.

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    message : String
    offset : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    Locations are one-based lines/byte-columns; offset is zero-based bytes.

    Document

    pub struct Document {
    tree : Value
    locations : Map[String, SourceLocation]
    }

    Parsed values and original source locations. Locations do not follow subsequent edits.

    Document::get_bool

    fn Document::get_bool(self : Document, path : String) -> Bool raise ConfigError

    Document::get_bytes

    fn Document::get_bytes(self : Document, path : String) -> Bytes raise ConfigError

    Document::get_double

    fn Document::get_double(self : Document, path : String) -> Double raise ConfigError

    Document::get_int

    fn Document::get_int(self : Document, path : String) -> Int raise ConfigError

    Strict typed queries, anchored to the original value's position.

    Document::get_int64

    fn Document::get_int64(self : Document, path : String) -> Int64 raise ConfigError

    Document::get_string

    fn Document::get_string(self : Document, path : String) -> String raise ConfigError

    Document::location

    fn Document::location(self : Document, path : String) -> SourceLocation? raise ConfigError

    Return original positions; both peers[0] and peers.[0] address the same location.

    Document::lookup

    fn Document::lookup(self : Document, path : String) -> Value raise ConfigError

    Query with source-aware errors. Missing members/indices use the nearest known parent.

    Document::root

    fn Document::root(self : Document) -> Value

    Return the value tree. Unchanged values/containers are shared with this document.

    Limits

    pub(all) struct Limits {
    max_input : Int
    max_depth : Int
    max_nodes : Int
    max_string : Int
    } derive(Eq,
    Debug
    )

    Resource limits count input/string bytes and value nodes (excluding root).

    Limits::default

    fn Limits::default() -> Limits

    Conservative defaults for in-memory configuration files.

    Segment

    pub(all) enum Segment {
    Key(String)
    Index(Int)
    } derive(Eq,
    Debug
    )

    A typed path avoids ambiguity between named members and indices.

    SourceLocation

    pub(all) struct SourceLocation {
    setting : SourcePosition
    value : SourcePosition
    } derive(Eq,
    Debug
    )

    Starting positions of a setting name and its value. List elements use the same position.

    SourcePosition

    pub(all) struct SourcePosition {
    offset : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    A zero-based byte offset and one-based line/byte-column in the original input.

    ValidationIssue

    pub(all) struct ValidationIssue {
    path : String
    code : String
    message : String
    } derive(Eq,
    Debug
    )

    A semantic/resource issue in a caller-constructed configuration. Empty path is root.

    Value

    pub(all) enum Value {
    Integer(Int)
    Integer64(Int64)
    Float(Double)
    Boolean(Bool)
    Text(Bytes)
    Group(Array[(String, Value)])
    ScalarArray(Array[Value])
    List(Array[Value])
    } derive(Eq,
    Debug
    )

    A typed libconfig value. Text stores bytes, including non-UTF-8 escapes.

    Value::appended

    fn Value::appended(self : Value, value : Value) -> Value raise ConfigError

    Append a scalar to an array or any value to a list.

    Value::as_bool

    fn Value::as_bool(self : Value) -> Bool raise ConfigError

    Strict boolean extraction.

    Value::as_bytes

    fn Value::as_bytes(self : Value) -> Bytes raise ConfigError

    Strict byte string extraction, without UTF-8 decoding.

    Value::as_double

    fn Value::as_double(self : Value) -> Double raise ConfigError

    Strict floating point extraction.

    Value::as_int

    fn Value::as_int(self : Value) -> Int raise ConfigError

    Strict 32-bit extraction.

    Value::as_int64

    fn Value::as_int64(self : Value) -> Int64 raise ConfigError

    Strict 64-bit extraction (does not widen Integer automatically).

    Value::as_string

    fn Value::as_string(self : Value) -> String raise ConfigError

    Strict UTF-8 extraction; escaped arbitrary bytes may fail decoding.

    Value::at

    fn Value::at(self : Value, index : Int) -> Value raise ConfigError

    Retrieve an array/list element. Negative indices are errors.

    Value::get_member

    fn Value::get_member(self : Value, name : String) -> Value raise ConfigError

    Retrieve a group member without implicit conversion.

    Value::get_path

    fn Value::get_path(self : Value, path : ArrayView[Segment]) -> Value raise ConfigError

    Look up a typed path. An empty path addresses the root.

    Value::inserted

    fn Value::inserted(self : Value, index : Int, value : Value) -> Value raise ConfigError

    Containers are copied; unchanged elements are shared.

    Value::kind

    fn Value::kind(self : Value) -> String

    Stable type names; array element checking uses the exact scalar type.

    Value::lookup

    fn Value::lookup(self : Value, path : String) -> Value raise ConfigError

    Look up a dot/bracket path.

    Value::removed_at

    fn Value::removed_at(self : Value, index : Int) -> Value raise ConfigError

    Remove an existing list/array element; remaining elements retain order.

    Value::replaced

    fn Value::replaced(self : Value, path : ArrayView[Segment], replacement : Value) -> Value raise ConfigError

    Return a tree with an existing setting replaced, leaving source containers unchanged.

    Value::with_member

    fn Value::with_member(self : Value, name : String, value : Value) -> Value raise ConfigError

    Return a group with a member inserted or replaced. Existing order is retained; new names append at the end. Unchanged values are shared with the source.

    Value::without_member

    fn Value::without_member(self : Value, name : String) -> Value raise ConfigError

    Return a group without an existing member; missing names are errors. Unchanged values are shared; no source container is modified.

    parse

    fn parse(input : String, limits? : Limits) -> Value raise ConfigError

    Parse UTF-8 text. Include directives are explicitly unsupported.

    parse_bytes

    fn parse_bytes(input : Bytes, limits? : Limits) -> Value raise ConfigError

    Parse libconfig bytes; returned root is always a Group.

    parse_document

    fn parse_document(input : String, limits? : Limits) -> Document raise ConfigError

    Parse UTF-8 text while preserving setting/value starting locations.

    parse_document_bytes

    fn parse_document_bytes(input : Bytes, limits? : Limits) -> Document raise ConfigError

    Parse bytes with locations; no filesystem access is performed.

    parse_path

    fn parse_path(path : String) -> Array[Segment] raise ConfigError

    Parse paths such as server.port, peers[0].host or peers.[0].host.

    stringify

    fn stringify(root : Value, limits? : Limits) -> String raise ConfigError

    max_input bounds the serialized output; all other limits also apply.

    validate

    fn validate(root : Value, limits? : Limits) -> Array[ValidationIssue] raise ConfigError

    Returns independent issues; traversal stops on the global node budget.