#json #serde #serde-json #parser #ast-parser #小玄

bin+lib ason

ASON 是一种从 JSON 发展而来的数据格式,引入了强数据类型和变体类型支持

11 个版本 (1 个稳定版)

1.0.1 2024年7月23日
0.2.2 2024年6月24日
0.2.0 2024年4月11日
0.1.6 2024年4月8日
0.1.3 2024年3月29日

#291解析器实现

Download history 4/week @ 2024-05-21 1/week @ 2024-05-28 3/week @ 2024-06-04 2/week @ 2024-06-11 245/week @ 2024-06-18 63/week @ 2024-06-25 67/week @ 2024-07-02 228/week @ 2024-07-23 6/week @ 2024-07-30

每月下载量 234次

MPL-2.0 许可证

280KB
7K SLoC

ASON

ASON 是一种从 JSON 发展而来的数据格式,引入了强数据类型和变体类型支持,同时优先考虑可读性和可维护性。ASON 非常适合用于应用程序配置文件、数据交换和数据存储。

功能

  • JSON 兼容性: ASON 集成了基本的 JSON 和 JSON5 语法,使 JSON 用户更容易过渡到 ASON。

  • 简单且一致的语法: ASON 的语法与 JavaScript 和 Rust 非常相似,支持注释,结构(对象)字段名省略双引号,并允许在数组的最后一个元素后使用逗号。这些功能增强了熟悉度和写作流畅性。

  • 强数据类型: ASON 数字可以进行显式类型化(例如,u8i32f32f64),整数可以以十六进制和二进制格式表示。此外,还引入了新的数据类型,如 DateTimeTupleByteDataChar,使数据表示更加精确和严格。

  • 原生变体数据类型支持,消除空值: ASON 原生支持变体数据类型(也称为 代数类型,类似于 Rust 中的枚举)。这使得从高级编程语言中无缝序列化复杂数据结构成为可能。重要的是,它消除了易出错的 null 值。

目录

示例

以下是一个 ASON 文本的示例

{
    string: "hello world"
    raw_string: r"[a-z]+\d+"
    integer_number: 123
    floating_point_number: 3.14
    number_with_explicit_type: 10u8
    boolean: true
    datetime: d"2023-03-24 12:30:00+08:00"
    bytedata: h"68 65 6c 6c 6f"

    list: [1, 2, 3]
    tuple: (1, "foo", true)
    object: {
        id: 123
        name: "Alice🍀"
    }

    variant: Option::None
    variant_with_single_value: Option::Some(123)
    variant_with_multiple_values: Color::RGB(255, 127, 63)
    variant_with_object_style_values: Shape::Rect{
        width: 200
        height: 100
    }
}

比较

与 JSON 比较

ASON相对于JSON是一种改进。总的来说,ASON的语法比JSON更简单、更一致、更具有表现力。

  • 可以省略尾随逗号。
  • 省略了对象键的双引号。
  • 添加了数值数据类型。
  • 添加了整数的十六进制和二进制表示。
  • 添加了浮点数的十六进制表示。
  • 添加了对长字符串、"原始字符串"和"自动修剪字符串"的支持。
  • 添加了对行注释和块注释的支持。
  • 添加了Variant数据类型,并移除了null值。
  • 添加了新的数据类型,如CharDateTimeTupleByteData
  • 改进:字符串始终使用双引号表示。
  • 改进:List要求所有元素都具有相同的数据类型。
  • 改进:允许在ListTupleObject的最后一个元素的末尾使用尾随逗号。

与 YAML 和 TOML 比较

所有三种格式都很简单(ASON可能有一对额外的括号),在数据量小的时候能够很好地表示内容。然而,当数据量较大时,YAML使用缩进来表示层次结构,因此需要仔细控制前缀中的空格字符数,并且在回退多层时容易出错。此外,它的规范相当复杂。TOML在表达层次结构方面做得不好,文本中经常有冗余的关键字名,并且对象列表不如其他格式清晰。另一方面,ASON无论数据量大小都具有很好的一致性。

文件扩展名

ASON文件的扩展名为*.ason,例如

sample.asonpackage.ason

库和 API

Rust库ason提供了两组API来访问ASON文本:一组基于serde进行序列化和反序列化,另一组基于AST(抽象语法树)进行低级访问。

通常建议使用serde API,因为它足够简单,可以满足大多数需求。

安装

在Rust项目的目录中运行命令$ cargo add ason以添加ason库。

序列化和反序列化

考虑以下ASON文本

{
    name: "foo"
    type: Type::Application
    version: "0.1.0"
    dependencies: [
        {
            name: "random"
            version: Option::None
        }
        {
            name: "regex"
            version: Option::Some("1.0.1")
        }
    ]
}

此文本由两种类型的对象组成:一个顶层对象,具有nametypeversiondependencies字段,另一个以列表形式存在的对象,包含nameversion字段。我们需要为这两个对象创建两个Rust结构体。首先,创建一个名为"Dependency"的结构体

#[derive(Serialize, Deserialize)]
struct Dependency {
    name: String,
    version: Option<String>,
}

请注意,此结构体有一个derive属性,在其中,SerializeDeserialize是由serde序列化框架提供的特性。将它们应用于结构体或枚举可以使结构体或枚举进行序列化和反序列化。

然后创建一个名为"Package"的结构体

#[derive(Serialize, Deserialize)]
struct Package {
    name: String,

    #[serde(rename = "type")]
    package_type: PackageType,

    version: String,
    dependencies: Vec<Dependency>,
}

由于“type”在Rust语言中是一个关键字,我们使用“package_type”作为字段名,然后使用#[serde(rename = "type")]属性来告诉serde将该字段序列化为"type"。注意,该字段的值是一个枚举,因此我们创建了一个名为"PackageType"的枚举

#[derive(Serialize, Deserialize)]
enum PackageType {
    Application,
    Library,
}

准备工作完成之后,以下语句将将ASON文档文本反序列化为Rust结构体实例

let text = "..."; // The above ASON text
let package = ason::from_str::<Package>(text).unwrap();

以下语句将Rust结构体实例序列化为字符串

let package = Package{...}; // Feel free to build the `Package` instance
let text = ason::to_string(&package);

Rust 数据和相应的 ASON 文本

结构体

通常,我们在Rust中使用结构体来存储一组相关数据。Rust结构体对应于ASON Object数据类型。以下是一个名为"User"的结构体及其实例的示例

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String
}

let v1 = User {
    id: 123,
    name: String::from("John")
};

实例v1对应的ASON文本是

{
    id: 123
    name: "John"
}

现实世界的数据通常很复杂,例如,一个包含另一个结构体的结构体可以形成层次关系。以下代码演示了结构体User包含一个名为Address的子结构体

#[derive(Serialize, Deserialize)]
struct Address {
    city: String,
    street: String
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    address: Box<Address>
}

let v2 = User {
    id: 123,
    name: String::from("John"),
    address: Box::new(Address{
        city: String::from("Shenzhen"),
        street: String::from("Xinan")
    })
}

实例v2对应的ASON文本

{
    id: 123
    name: "John"
    address: {
        city: "Shenzhen"
        street: "Xinan"
    }
}

Vec

Vec(向量)是Rust中另一种常见的数据结构,用于存储一系列相似的数据。Vec对应于ASON List数据类型。以下代码演示了在结构体User中添加一个名为orders的字段来存储订单编号

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    orders: Vec<i32>
}

let v3 = User {
    id: 123,
    name: String::from("John"),
    orders: vec![11, 13, 17, 19]
};

实例v3对应的ASON文本是

{
    id: 123
    name: "John"
    orders: [11, 13, 17, 19]
}

向量中的元素可以是简单的数据(如上述示例中的i32),也可以是复杂的数据,如结构体。以下代码演示了在结构体User中添加一个名为addresses的字段来存储送货地址

#[derive(Serialize, Deserialize)]
struct Address {
    city: String,
    street: String
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    addresses: Vec<Address>
}

let v4 = User {
    id: 123,
    name: String::from("John"),
    address: vec![
        Address {
            city: String::from("Guangzhou"),
            street: String::from("Tianhe")
        },
        Address {
            city: String::from("Shenzhen"),
            street: String::from("Xinan")
        },
    ]
};

实例v4对应的ASON文本是

{
    id: 123
    name: "John"
    addresses: [
        {
            city: "Guangzhou"
            street: "Tianhe"
        }
        {
            city: "Shenzhen"
            street: "Xinan"
        }
    ]
}

元组

在Rust中还有另一种常见的数据类型“元组”,它可以被认为是省略了字段名的结构体。例如,在上面的示例代码中orders: Vec<i32>,如果您想使订单列表不仅包括订单编号,还包括订单状态,您可以使用元组(i32, String)而不是i32。修改后的代码是

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    orders: Vec<(i32, String)>
}

let v5 = User {
    id: 123,
    name: String::from("John"),
    orders: vec![
        (11, String::from("ordered"),
        (13, String::from("shipped"),
        (17, String::from("delivered"),
        (19, String::from("cancelled")
    ]
};

实例v5对应的ASON文本是

{
    id: 123
    name: "John"
    orders: [
        (11, "ordered")
        (13, "shipped")
        (17, "delivered")
        (19, "cancelled")
    ]
}

Rust元组对应于ASON Tuple数据类型。需要注意的是,在某些编程语言中,元组和向量没有明确区分,但在Rust中它们是完全不同的数据类型。向量要求所有元素具有相同的数据类型(Rust数组类似于向量,但向量具有可变数量的元素,而数组在创建后大小固定,无法更改),而元组不需要其成员数据类型相同,但要求成员数量固定。ASON对Tuple的定义与Rust的定义一致。

枚举

在上面的例子中,订单状态用一个字符串表示。从历史经验中我们知道,一种更好的解决方案是使用枚举。Rust的枚举对应于ASON中的Variant数据类型。下面的代码使用枚举Status来替换String,它在Vec<(i32, String)>

#[derive(Serialize, Deserialize)]
enum Status {
    Ordered,
    Shipped,
    Delivered,
    Cancelled
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    orders: Vec<(i32, Status)>
}

let v6 = User {
    id: 123,
    name: String::from("John"),
    orders: vec![
        (11, Status::Ordered),
        (13, Status::Shipped),
        (17, Status::Delivered),
        (19, Status::Cancelled)
    ]
};

例如,对于v6,相应的ASON文本是

{
    id: 123
    name: "John"
    orders: [
        (11, Status::Ordered)
        (13, Status::Shipped)
        (17, Status::Delivered)
        (19, Status::Cancelled)
    ]
}

Rust枚举类型实际上非常强大,它不仅可以表示不同类别的数据,还可以携带数据。例如,考虑以下枚举Color

#[derive(Serialize, Deserialize)]
enum Color {
    Transparent,
    Grayscale(u8),
    Rgb(u8, u8, u8),
    Hsl{
        hue: i32,
        saturation: u8,
        lightness: u8
    }
}

Rust枚举中有四种类型的值

  • 没有值,例如Color::Transparent
  • 有一个值,例如Color::Grayscale(u8)
  • 类似于元组的多个值,例如Color::Rgb(u8, u8, u8)
  • 类似于结构的多个“键值”对,例如Color::Hsl{...}

ASON Variant完全支持Rust枚举的所有类型值,考虑以下实例

let v7 = vec![
    Color::Transparent,
    Color::Grayscale(127),
    Color::Rgb(255, 127, 63),
    Color::Hsl{
        hue: 300,
        saturation: 100,
        lightness: 50
    }
];

例如,对于v7,相应的ASON文本是

[
    Color::Transparent
    Color::Grayscale(127_u8)
    Color::Rgb(255_u8, 127_u8, 63_u8)
    Color::Hsl{
        hue: 300
        saturation: 100_u8
        lightness: 50_u8
    }
]

ASON文本与Rust数据字面量非常相似,这是故意的。设计者旨在通过使ASON类似于现有的数据格式(如JSON)和编程语言(如Rust)来降低用户的学习曲线。

有关ASON数据类型和规范的更多信息,请参阅后续章节。

对 Rust 数据类型的支持

ASON原生支持大多数Rust数据类型,包括元组、枚举和向量。由于ASON也是强类型的,因此序列化和反序列化可以确保数据准确性。与JSON、YAML和TOML等其他数据格式相比,ASON与Rust数据类型的兼容性更好。

ASON是一种与Rust数据类型完美匹配的数据格式。

以下是一个支持Rust数据类型的列表

  • 有符号和无符号整数,从i8/u8到i64/u64
  • 浮点数,包括f32和f64
  • 布尔值
  • 字符
  • 字符串
  • 数组,例如[i32; 4]
  • Vec
  • 枚举
  • 结构体
  • 元组

然而,为了简单起见,某些Rust数据类型不受支持,包括

  • 八进制整数字面量
  • 单元(即()
  • 单元结构,例如()
  • 新类型结构,例如struct Width(u32);
  • 类似于元组的结构,例如struct RGB(u8, u8, u8);

值得注意的是,serde框架的数据模型不包括DateTime类型,因此ASON DateTime不能直接序列化或反序列化为Rust的chrono::DateTime。如果您序列化一个chrono::DateTime类型的值,您将得到一个普通字符串。一种解决方案是将chrono::DateTime值包装为ason::Date类型。更多详情,请参考库源代码中的ason::serde::serde_date::tests目录下的'test_serialize'单元测试。

此外,serde将固定长度的数组(如[i32; 4])视为元组而不是向量,因此Rust数组[11, 13, 17, 19]将被序列化为ASON Tuple ((11, 13, 17, 19))

AST 解析器和写入器

这些是库提供的低级API。它们可以用于验证或格式化ASON文本。

考虑以下ASON文本

{
    id: 123
    name: "John"
    orders: [11, 13]
}

以下代码演示了使用解析器将上述文本转换为AST,然后使用写入器将AST转换为字符串。

let text = "..."; // The above ASON text
let node = ason::parse_from(text).unwrap();

assert_eq!(
    node,
    AsonNode::Object(vec![
        NameValuePair {
            name: String::from("id"),
            value: Box::new(AsonNode::Number(Number::Int(123)))
        },
        NameValuePair {
            name: String::from("name"),
            value: Box::new(AsonNode::String_(String::from("John")))
        },
        NameValuePair {
            name: String::from("orders"),
            value: Box::new(AsonNode::Array(vec![
                AsonNode::Number(Number::Int(11)),
                AsonNode::Number(Number::Int(13))
            ]))
        }
    ])
);

let s = ason::write_to(&node);
assert_eq!(text, s);

实用工具

该库还提供了一种工具,可以用于验证或格式化ASON文件。

首先使用以下命令安装该工具:

$cargo install ason

该命令会将可执行文件ason添加到~/.cargo/bin目录。

该工具的使用方法:

$ason<filename>

例如:

$ason test.ason

如果文件"test.ason"没有错误,程序会将格式化后的文本打印到终端。输出可以重定向到新文件,例如:

$ason test.ason>test_formatted.ason

语法快速参考

ASON由值和可选注释组成。值有两种类型:基本值和复合值。

基本值是基本数据类型,如整数、字符串、布尔值和日期时间。复合值是由多个值组成的更复杂结构,如列表和对象。

原始值

以下是不同类型的基本值的示例

  • 整数:123+456-789

  • 浮点数:3.142+1.414-1.732

  • 带有指数的浮点数:2.998e106.674e-11

    可以在数字的任意两位之间插入下划线来分组它们,例如:123_456_7896.626_070_e-34

    数字的数据类型可以通过在数字后附加类型名称来显式指定,例如 65u83.14f32

    在数字和类型名称之间也可以插入下划线,例如 933_199_u326.626e-34_f32

ASON中的每个数字都有一个特定的数据类型。如果没有显式指定,整数的默认数据类型是 i32,浮点数的默认数据类型是 f64。ASON支持以下数值数据类型:i8u8i16u16i32u32i64u64f32f64

  • 十六进制整数:0x410x61_u8

  • 二进制整数:0b11000b0110_0001_u8

  • C/C++语言十六进制浮点字面量格式中的浮点数:0x1.4p30x1.921f_b6p1_f32

    请注意,您不能通过简单地给正常的十六进制整数添加 "f32" 或 "f64" 后缀来表示浮点数,例如 0x21_f32。这是因为字符 "f" 是十六进制数字字符之一(即 [0-9a-f]),所以 0x21_f32 只会被解析为正常的十六进制整数 0x21f32

  • 布尔值:truefalse

  • 字符:'a''''😊'

  • 转义字符:'\r''\n''\t''\\'

  • Unicode转义字符:'\u{2d}''\u{6587}'

  • 字符串: "abc文字😊", "foo\nbar"

  • 原始字符串: r"[a-z]+\d+", r#"<\w+\s(\w+="[^"]+")*>"#

  • 日期和时间: d"2024-03-16", d"2024-03-16 16:30:50", d"2024-03-16T16:30:50+08:00"

  • 字节数据: h"11 13 17 19", h"4d 6f 6f 6e"

多行字符串

ASON字符串允许多行,如下例所示

{
    multiline_string: "1. Mercury Venus Earth
                     2. Mars Jupiter Saturn
                     3. Uranus Neptune"
}

这表示包含三个段落的字符串。

自动去除前导空白字符串

ASON支持“自动删除前导空白字符串”以提高可读性。例如,在上面的例子中,第二行和第三行引入了不必要的空白字符。虽然我们可能并不总是需要这些前导空格,但在每行的开始处编写将破坏文档的缩进。使用“自动删除前导空白字符串”可以解决这个问题,修改后的文本如下

{
    auto_trimming_string: """
        1. Mercury Venus Earth
        2. Mars Jupiter Saturn
        3. Uranus Neptune
        """
}

长字符串

有时一个字符串可能不需要跨越多行,但其内容相对较长。为了提高可读性,ASON支持跨多行编写字符串。只需在行尾添加一个\符号,然后开始新的一行。后续文本将自动附加到当前字符串。例如

{
    long_string: "My very educated \
                mother just served \
                us nine pizzas"
}

示例中的字符串相当于 "My very educated mother just served us nine pizzas"。请注意,文本正文行中的所有前导空白都将自动删除。

对象

一个对象可以包含多个值,每个值都有一个名为 的名称。键和值的组合称为 键值对。对象是键值对的集合。例如

{
    name: "ason",
    version: "1.0.1",
    edition: "2021",
}

每个键值对结尾的逗号是可选的。例如,上面的ASON文本可以写成

{
    name: "ason"
    version: "1.0.1"
    edition: "2021"
}

请注意,ASON对象允许在最后一个键值对后加逗号,而在JSON中则不允许。这个特性主要是为了方便重新排列“键值对”(在编辑ASON文档时)。

当然,多个键值对也可以写在同一行上。在这种情况下,键值对之间需要逗号。例如

{name: "ason", version: "1.0.1", edition: "2021",}

对象内的值可以是任何类型,包括原始值(如数字、字符串、日期)和组合值(如列表、对象、元组)。在现实生活中,对象通常包含其他对象。例如

{
    name: "ason"
    version: "1.0.1"
    edition: "2021"
    dependencies: {
        serde: "1.0"
        chrono: "0.4"
    }
    dev_dependencies: {
        pretty_assertions: "1.4"
    }
}

列表

列表是一系列相同类型的值,如下

[11, 13, 17, 19]

与对象类似,列表中的元素也可以单独写在一行上,每行末尾可以加逗号,并且允许在最后一个元素后加逗号。例如

[
    "Alice",
    "Bob",
    "Carol",
    "Dan",
]

[
    "Alice"
    "Bob"
    "Carol"
    "Dan"
]

列表中的元素可以是任何数据类型,但同一列表中的所有元素必须具有相同的类型。例如,以下列表是无效的

// invalid list due to inconsistent data types of elements
[11, 13, "Alice", "Bob"]

如果列表中的元素是对象,则每个对象中的键名和键的数量,以及相应值的类型,必须保持一致。换句话说,对象的类型由所有键值对决定,而键值对的类型由键名和值的类型决定。例如,以下列表是有效的

[
    {
        id: 123
        name: "Alice"
    }
    {
        id: 456
        name: "Bob"
    }
]

而以下列表是无效的

// invalid list due to inconsistent types of the two objects.
[
    {
        id: 123
        name: "Alice"
    }
    {
        id: 456
        score: 'A'
    }
]

如果列表中的元素是列表,则每个子列表中的元素的数据类型必须相同。换句话说,列表的类型由其元素的数据类型决定,元素的数量无关紧要。例如,以下列表是有效的

[
    [11, 13, 17]
    [101, 103, 107, 109]
    [211, 223]
]

在上面的例子中,顶层列表包含3个子列表,尽管每个子列表中的元素数量不同(3、4和2),但所有三个子列表的元素类型都是i32,这使得它们的类型相同。

元组

元组可以被视为省略键的对象,例如

(11, "Alice", true)

元组在外观上与列表相似,但元组不需要每个元素的数据类型保持一致。其次,元素的数据类型和数量都是元组类型的一部分,例如("Alice", "Bob", "Carol")("Alice", "Bob", "Carol")是不同类型的元组,因为它们的元素数量不同。

与对象和列表类似,元组的元素也可以分别写在单独的行上,每行末尾可以省略逗号,最后元素后面可以有逗号。例如

(
    "Alice",
    11,
    true,
)

(
    "Alice"
    11
    true
)

变体

变体由三个部分组成:变体类型名、变体成员名和可选值。例如

// Variant without value
Option::None

// Variant with a value
Option::Some(11)

在上面的两个变体中,“Option”是变体类型名,“None”和“Some”是变体成员名,“11”是变体值。

只要变体类型名相同,类型就相同。例如,Color::RedColor::Green是相同类型,而Option::NoneColor::Red是不同类型。

如果一个变体成员携带值,则该值的类型也是变体成员类型的一部分。例如,Option::Some(11)Option::Some(13) 是同一类型,但 Option::Some(11)Option::Some("John") 则是不同类型。

因此,以下列表是有效的,因为所有元素都具有相同的变体类型名称,并且成员 Some 的类型相同

[
    Option::None
    Option::Some(11)
    Option::None
    Option::Some(13)
]

然而,以下列表无效,尽管所有元素的变体类型名称一致,但成员 Some 的类型不一致

[
    // Invalid list because of inconsistent element types
    Option::None
    Option::Some(11)
    Option::Some("John")
]

一个变体可以携带任何类型的值,例如对象

Option::Some({
    id: 123
    name: "Alice"
})

或者元组

Option::Some((211, 223))

事实上,一个变体也可以携带多个值,这些值可以是对象风格的或元组风格的,例如

// Object-style variant
Shape:Rectangle{
    width: 307
    height: 311
}

// Tuple-style variant
Color::RGB(255, 127, 63)

注释

类似于JavaScript和C/C++,ASON也支持两种类型的注释:行注释和块注释。注释是为了人类阅读的,并且完全被机器忽略。

行注释以 // 符号开始,并一直持续到行尾。例如

// This is a line comment
{
    id: 123 // This is also a line comment
    name: "Bob"
}

块注释以 /* 符号开始,并以 */ 符号结束。例如

/* This is a block comment */
{
    /*
     This is also a block comment
    */
    id: 123 
    name: /* definitely a block comment */ "Bob"
}

与JavaScript和C/C++不同,ASON的块注释支持嵌套。例如

/* 
    This is the first level
    /*
        This is the second level
    */
    This is the first level again
*/  

块注释的嵌套功能使得我们更容易对已经有一个块注释的代码片段进行注释。如果不支持嵌套,我们需要先移除内部块注释,然后再添加外部的注释,因为内部块注释符号 */ 会导致所有外部块注释提前结束。

文档

一个ASON文档只能包含一个值(包括原始值和复合值),就像JSON一样,一个典型的ASON文档通常是一个对象或列表。事实上,所有类型的值都是允许的,不仅限于对象或列表。例如,元组、变体,甚至数字或字符串也是允许的。只需确保文档恰好有一个值。例如,以下都是有效的ASON文档

(11, "Alice", true)

"Hello World!"

而以下两个是无效的

(11, "Alice", true)
'A' // Invalid ASON document because there is more than one value

"Hello World!"
true // Invalid ASON document because there is more than one value

规范

请参阅完整文档

源代码

许可证

查看许可证附加许可证

依赖项

~1.4–2.2MB
~42K SLoC