Rust 实战丨通过实现 json! 掌握声明宏

news2024/11/25 0:47:48

在 Rust 编程语言中,宏是一种强大的工具,可以用于在编译时生成代码。json! 是一个在 Rust 中广泛使用的宏,它允许我们在 Rust 代码中方便地创建 JSON 数据。

声明宏(declarative macros)是 Rust 中的一种宏,它们使用 macro_rules! 关键字定义。

本文将参考《Rust 程序设计(第二版)》,通过实现 json! 宏,深入理解声明宏的工作原理。

结论先行

本文我们将构建一个 json! 宏,它支持我们以字符串 JSON 风格的语法来编写 Json 值。如下面这个例子:

let students = json![
	{
		"name": "Hedon Wang",
		"class_of": 2022,
		"major": "Software engineering"
	},
	{
		"name": "Jun Lei",
		"class_of": 1991,
		"major": "Computor science"
	}
]

完整代码

实现 json!

定义 Json enum

首先我们需要思考一下 Json 结构是什么样子的?主要是以下 3 种模式:

{
  "name": "hedon",
  "age": 18,
  "school": {
    "name": "Wuhan University",
    "address": "Hubwi Wuhan"
  }
}
[
	{
	  "name": "hedon"
	},
	{
	  "name": "john"
	}
]
null

为此我们定义一个 Json 结构的枚举:

#[derive(Clone, PartialEq, Debug)]
pub enum Json {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Object(HashMap<String, Json>),
}

你应该可以感到非常奇妙,使用一个这么简单的枚举,居然就可以表示所有的 Json 结构了。遗憾的是,现在这个结构编写 Json 值的语法相当冗长。

let people = Json::Object(HashMap::from([
    ("name".to_string(), Json::String("hedon".to_string())),
    ("age".to_string(), Json::Number(10.0)),
    ("is_student".to_string(), Json::Boolean(true)),
    (
        "detail".to_string(),
        Json::Object(HashMap::from([
            ("address".to_string(), Json::String("beijing".to_string())),
            ("phone".to_string(), Json::String("1234567890".to_string()))
        ]))
    )
]))

我们期望可以以下面这种方式来声明 Json 变量,这看起来就清爽许多了。

let students = json!([
    {
        "name": "Jim Blandy",
        "class_of": 1926,
        "major": "Tibetan throat singing"
    },
    {
        "name": "Jason Orendorff",
        "class_of": 1702,
        "major": "Knots"
    }
]);

猜想 json!

我们可以预见 Json 宏内部将会有多条规则,因为 JSON 数据有多种类型:对象、数组、数值等。事实上,我们可以合理地猜测每种 JSON 类型都将有一条规则:

macro_rules! json {
    (null)    => { Json::Null };
    ([ ... ]) => { Json::Array(...) };
    ({ ... }) => { Json::Object(...) };
    (???)     => { Json::Boolean(...) };
    (???)     => { Json::Number(...) };
    (???)     => { Json::String(...) };
}

然而这不太正确,因为宏模式无法区分最后 3 种情况,稍后我们会讨论如何处理。至于前 3 种情况,显然它们是以不同的语法标记开始的,所以这几种情况比较好处理。

实现 Null

我们先从最简单的 Null 分支开始,先编写如下测试用例:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_null_json() {
        let json = json!(null);
        assert_eq!(json, Json::Null);
    }
}

想要通过上述测试用例非常简单,我们只需要在 macro_rules! 支持中匹配这种情况即可:

#[macro_export]
macro_rules! json {
    (null) => {
        Json::Null
    };
}
  • #[macro_export] 注解是 Rust 中的一个属性,用于指示这个宏应该被导出到调用者的作用域中,这样其他模块也可以使用它。
  • macro_rules! 宏定义了一个自定义的宏。在这里,它创建了一个名为 json 的宏,用于生成 JSON 数据。
  • 宏定义中 (null) 是匹配模式。这意味着当你调用 json! 宏并传递 null 作为参数时,将会触发这个规则。
  • => 符号用于指示匹配模式后的代码块。在这里,它指定了当匹配 (null) 时应该生成的代码块。
  • Json::Null 是一个 JSON 类型的枚举值,表示 JSON 中的 null 值。这个宏的目的是将传入的 null 转换为 Json::Null

实现 Boolean/Number/String

我们先准备如下测试用例:

#[test]
fn test_boolean_number_string_json() {
    let json = json!(true);
    assert_eq!(json, Json::Boolean(true));

    let json = json!(1.0);
    assert_eq!(json, Json::Number(1.0));

    let json = json!("hello");
    assert_eq!(json, Json::String("hello".to_string()));
}

通过观察分析,它们其实都是同一种模式:

Boolean/Number/String 分析

现在需要解决的问题就是,如何将这 3 种模式进行统一,这样在 macro_rules! 中才可以统一匹配模式并进行代码生成。

这里我们其实需要做的就是将 boolf64&str 转为对应的 Json 类型。那就需要用到标准库中的 From trait 了。

做法很简单,我们实现如下代码:

impl From<bool> for Json {
    fn from(value: bool) -> Self {
        Json::Boolean(value)
    }
}

impl From<&str> for Json {
    fn from(value: &str) -> Self {
        Json::String(value.to_string())
    }
}

impl From<f64> for Json {
    fn from(value: f64) -> Self {
        Json::Number(value)
    }
}

然后完善我们的 json!,目前的实现如下:

#[macro_export]
macro_rules! json {
    (null) => {
        Json::Null
    };
    ($value: tt) => {
        Json::from($value)
    };
}

这里我们使用 $value作 为变量来承接匹配到的元素,其类型为 tt ,表示任意的语法标记树。具体可以参考:片段类型。

这时运行上述测试用例,是没有问题的:

  PASS [   0.004s] json-macro tests::test_boolean_number_string_json
  PASS [   0.004s] json-macro tests::test_null_json

美中不足的是,JSON 结构中的数字类型,其实不一定是 f64,也可以是 i32、u32、f32 或其他的数字类型,如果我们要为这全部的数字类型都实现到 Json 的 From trait,那就多冗余。

这个时候我们又可以实现一个宏,用于快速生成 impl From<T> for Json 。这个实现比较简单,本文就不赘述了,代码如下:

#[macro_export]
macro_rules! impl_from_for_primitives {
    (  $( $type: ty ) * ) => {
        $(
            impl From<$type> for Json {
                fn from(value: $type) -> Self {
                    Json::Number(value as f64)
                }
            }
        )*
    }
}

然后我们只需要用下面这一行代码,就可以为所有的数字类型实现 From trait 了:

impl_from_for_primitives!(u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 isize usize);

记得这个时候你要删除上面手动实现的 impl From<f64> for Json,不然会有 impl 冲突错误。

再次运行测试,也是可以通过的。

实现 Array

准备如下测试用例:

#[test]
fn test_array_json() {
    let json = json!([1, null, "string", true]);
    assert_eq!(
        json,
        Json::Array(vec![
            Json::Number(1.0),
            Json::Null,
            Json::String("string".to_string()),
            Json::Boolean(true)
        ])
    )
}

要匹配 [1, null, "string", true]这个模式,笔者的分析过程如下:

  1. 首先是外面的两个中括号 []
  2. 再往里,是一个重复匹配的模式,以 , 分割,可以匹配 0 到任意多个元素,所以是 $( ,*) ,具体可以参考:重复模式;
  3. 最里面就是第 2 步要匹配的元素了,我们先用 $element 作为变量来承接每一个元素,其类型为 tt ,表示任意的语法标记树。

分析完匹配的表达式后,我们就可以得到:

([ $( $element:tt ), * ]) => { /* TODO */ }

我们要生成的代码长这个样子:

Json::Array(vec![
    Json::Number(1.0),
    Json::Null,
    Json::String("string".to_string()),
    Json::Boolean(true)
])

其实就是一个 vec!,然后里面每个元素都是一个 Json,如此递归下去。

即可以得到代码生成部分的逻辑为:

Json::Array(vec![$(json!($element)),* ])

Json::Array 宏分析

综上,我们实现的代码如下:

#[macro_export]
macro_rules! json {
    (null) => {
        Json::Null
    };
    ([ $( $element: tt),* ]) => {
        Json::Array(vec![ $( json!($element)), * ])
    };
    ($value: tt) => {
        Json::from($value)
    };
}

运行测试用例:

PASS [   0.003s] json-macro tests::test_null_json
PASS [   0.003s] json-macro tests::test_boolean_number_string_json
PASS [   0.004s] json-macro tests::test_array_json

实现 Object

写好如下测试用例,这次我们顺带把 Null、Boolean、Number 和 String 带上了:

#[test]
fn test_object_json() {
    let json = json!({
        "null": null,
        "name": "hedon",
        "age": 10,
        "is_student": true,
        "detail": {
            "address": "beijing",
            "phone": "1234567890"
        }
    });
    assert_eq!(
        json,
        Json::Object(HashMap::from([
            ("name".to_string(), Json::String("hedon".to_string())),
            ("age".to_string(), Json::Number(10.0)),
            ("is_student".to_string(), Json::Boolean(true)),
            (
                "detail".to_string(),
                Json::Object(HashMap::from([
                    ("address".to_string(), Json::String("beijing".to_string())),
                    ("phone".to_string(), Json::String("1234567890".to_string()))
                ]))
            )
        ]))
    )
}

对比预期的 json! 宏内容和展开后的代码:

Json::Object 宏分析

完善我们的 macro_rules! json

#[macro_export]
macro_rules! json {
    (null) => {
        Json::Null
    };
    ([ $( $element: tt),* ]) => {
        Json::Array(vec![ $( json!($element)), * ])
    };
    ({ $( $key:tt : $value:tt ),* }) => {
        Json::Object(HashMap::from([
            $(
                ( $key.to_string(), json!($value) )
            ), *
        ]))
    };
    ($value: tt) => {
        Json::from($value)
    };
}

运行测试用例:

PASS [   0.004s] json-macro tests::test_object_json
PASS [   0.005s] json-macro tests::test_array_json
PASS [   0.004s] json-macro tests::test_null_json
PASS [   0.005s] json-macro tests::test_boolean_number_string_json

至此,我们就完成了 json! 宏的构建了!完整源码可见:完整代码

Peace! Enjoy coding~

附录

重复模式

在 实现 Array 中,我们匹配了这样一个模式:

([ $( $element:tt ), * ]) => { /* TODO */ }

其中 $($element:tt), *) 就是一个重复模式,其可以进一步抽象为 $( ... ),* ,表示匹配 0 次或多次,以 , 分隔。

Rust 支持以下全部重复模式:

模式含义
$( … ) *匹配 0 次或多次,没有分隔符
$( … ), *匹配 0 次或多次,以逗号分隔
$( … ); *匹配 0 次或多次,以分号分隔
$( … ) +匹配 1 次或多次,没有分隔符
$( … ), +匹配 1 次或多次,以逗号分隔
$( … ); +匹配 1 次或多次,以分号分隔
$( … ) ?匹配 0 次或 1 次,没有分隔符

即:

  • * 表示 0 次或多次
  • + 表示 1 次或多次
  • ? 表示 0 次或 1 次
  • 可在上述 3 者之前加入分隔符

片段类型

在 实现 Array 中,我们匹配了这样一个模式:

([ $( $element:tt ), * ]) => { /* TODO */ }

这里我们将 $element 指定为 tt,这个 tt 就是宏中的一种片段类型。

tt 能匹配单个语法标记树,包含:

  • 一对括号,如 (..)[..]、或 {..} ,以及位于其中的所有内容,包括嵌套的语法标记树。
  • 单独的非括号语法标记,比如 1926Knots

所以为了匹配任意类型的 Json ,我们选择了 tt 作为 $element 的片段类型。

macro_rules! 支持的片段类型如下所示:

片段类型匹配(带例子)后面可以跟 ······
expr表达式:2 + 2, “udon”, x.len()=>,;
stmt表达式或声明,不包括任何尾随分号(很难用,请尝试使用 expr 或 block)=>,;
ty类型:String, Vec, (&str, bool), dyn Read + Send=>,; =
path路径:ferns, ::std::sync::mpsc=>,; =
pat模式:_, Some(ref x)=>,=
item语法项:struct Point { x: f64, y: f64 }, mod ferns;任意
block块:{ s += “ok\n”; true }任意
meta属性的主体:inline, derive(Copy, Clone), doc=“3D models.”任意
literal字面量值:1024, “Hello, world!”, 1_000_000f64任意
lifetime生命周期:'a, 'item, 'static任意
vis可见性说明符:pub, pub(crate), pub(in module::submodule)任意
ident标识符:std, Json, longish_variable_name任意
tt语法标记树:;, >=, {}, [0 1 (+ 0 1)]任意

完整代码

use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq)]
#[allow(unused)]
enum Json {
    Null,
    Boolean(bool),
    String(String),
    Number(f64),
    Array(Vec<Json>),
    Object(HashMap<String, Json>),
}

impl From<bool> for Json {
    fn from(value: bool) -> Self {
        Json::Boolean(value)
    }
}

impl From<&str> for Json {
    fn from(value: &str) -> Self {
        Json::String(value.to_string())
    }
}

impl From<String> for Json {
    fn from(value: String) -> Self {
        Json::String(value)
    }
}

#[macro_export]
macro_rules! impl_from_for_primitives {
    (  $( $type: ty ) * ) => {
        $(
            impl From<$type> for Json {
                fn from(value: $type) -> Self {
                    Json::Number(value as f64)
                }
            }
        )*
    }
}

impl_from_for_primitives!(u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 isize usize);

#[macro_export]
macro_rules! json {
    (null) => {
        Json::Null
    };
    ([ $( $element: tt),* ]) => {
        Json::Array(vec![ $( json!($element)), * ])
    };
    ({ $( $key:tt : $value:tt ),* }) => {
        Json::Object(HashMap::from([
            $(
                ( $key.to_string(), json!($value) )
            ), *
        ]))
    };
    ($value: tt) => {
        Json::from($value)
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_null_json() {
        let json = json!(null);
        assert_eq!(json, Json::Null);
    }

    #[test]
    fn test_boolean_number_string_json() {
        let json = json!(true);
        assert_eq!(json, Json::Boolean(true));

        let json = json!(1.0);
        assert_eq!(json, Json::Number(1.0));

        let json = json!("hello");
        assert_eq!(json, Json::String("hello".to_string()));
    }

    #[test]
    fn test_object_json() {
        let json = json!({
            "null": null,
            "name": "hedon",
            "age": 10,
            "is_student": true,
            "detail": {
                "address": "beijing",
                "phone": "1234567890"
            }
        });
        assert_eq!(
            json,
            Json::Object(HashMap::from([
                ("null".to_string(), Json::Null),
                ("name".to_string(), Json::String("hedon".to_string())),
                ("age".to_string(), Json::Number(10.0)),
                ("is_student".to_string(), Json::Boolean(true)),
                (
                    "detail".to_string(),
                    Json::Object(HashMap::from([
                        ("address".to_string(), Json::String("beijing".to_string())),
                        ("phone".to_string(), Json::String("1234567890".to_string()))
                    ]))
                )
            ]))
        )
    }

    #[test]
    fn test_array_json() {
        let json = json!([1, null, "string", true]);
        assert_eq!(
            json,
            Json::Array(vec![
                Json::Number(1.0),
                Json::Null,
                Json::String("string".to_string()),
                Json::Boolean(true)
            ])
        )
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1817335.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

debug调试_以Pycharm为例

文章目录 作用步骤打断点调试调试窗口 作用 主要是检查逻辑错误&#xff0c;而非语法错误。 步骤 打断点 在需要调试的代码行前打断点&#xff0c;执行后会停顿在断点位置&#xff08;不运行&#xff09; 调试 右键“debug”&#xff0c;或者直接点击右上角的小虫子 调试…

2-2 基于matlab的变邻域

基于matlab的变邻域&#xff0c;含变惯性权重策略的自适应离散粒子群算法&#xff0c;适应函数是多式联运路径优化距离。有10城市、30城市、75城市三个案例。可直接运行。 2-2 路径规划 自适应离散粒子群算法 - 小红书 (xiaohongshu.com)

Vue基本使用-02

上节我们讲了什么是mvvm模型&#xff0c;以及我们vue的一些常用指令&#xff0c;今天给大家讲一下vue的基本使用&#xff0c;在将之前我们需要重点讲解我们的一个指令&#xff0c;v-model指令 v-model v-model 可以在组件上使用以实现双向绑定,什么是双向绑定呢?意思就是当我们…

【Ubuntu双系统】两块硬盘分别安装系统,一块硬盘安装Ubuntu 一块安装Windows

【Ubuntu双系统】两块硬盘分别安装双系统&#xff0c;一块硬盘安装Ubuntu 一块安装Windows 前言安装Ubuntu前置操作安装过程参考文献 前言 机器情况&#xff1a;两块1T的硬盘&#xff0c;其中一块已安装Windows 11现需在另一块硬盘上安装Ubuntu&#xff0c;该硬盘还未初始化Ub…

SQL聚合函数---汇总数据

此篇文章内容均来自与mysql必知必会教材&#xff0c;后期有衍生会继续更新、补充知识体系结构 文章目录 SQL聚集函数表&#xff1a;AGV()count()根据需求可以进行组合处理 max()min()max&#xff08;&#xff09;、min&#xff08;&#xff09;、avg&#xff08;&#xff09;组…

Mac下载了docker,在终端使用docker命令时用不了

问题&#xff1a;在mac使用docker的时候&#xff0c;拉取docker镜像失败 原因&#xff1a;docker是需要用app使用的 &#xff0c;所以在使用的时候必须打开这个桌面端软件才可以在终端上使用docker命令&#xff01;&#xff01;&#xff01;

【PL理论】(21) 函数式语言:支持匿名函数 fun x → E | 设计递归函数 | 支持递归函数:let rec ...

&#x1f4ad; 写在前面&#xff1a;本章我们将讲解支持匿名函数&#xff0c;先回顾一下 F# 语言表示函数的方法&#xff0c;然后引出它。随后我们讲解一下如何设计递归函数&#xff0c;最后让我们的 F- 语言支持递归函数。 目录 0x00 回顾&#xff1a;F# 语言 0x01 支持匿名…

深度学习笔记: 最详尽Airbnb租赁搜索排名设计

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; Airbnb租赁搜索排名 1. 问题陈述 Airbnb用户在特定地点搜索可用房源。系统应在搜索结果中对多个房源进…

Qt飞机大战小游戏

Gitee地址 &#xff1a;plane-game: 基于Qt的飞机大战小游戏 GitHub地址&#xff1a; https://github.com/a-mo-xi-wei/plane-game

Vue25-内置指令02:v-text指令

一、v-html对比v-text v-html支持结构的解析&#xff0c;v-text不支持结构的解析。 二、v-html的安全性问题 2-1、cookie的原理&#xff08;node.js&#xff09; 7天免登录&#xff0c;cookie实现。 cookie的本质就是类似于json的字符串&#xff0c;格式是&#xff1a;key-va…

图片导入AutoCAD建立草图—CAD图像导入插件

插件介绍 CAD图像导入插件可将PNG&#xff0c;JPG等格式图片导入到AutoCAD软件内建立图像边缘的二维线条模型。插件可以提取图像黑色或白色区域的边界&#xff0c;并可绘制原状边界或平滑边界两种样式。 模型说明 边界提取&#xff0c;黑色或白色边界的提取根据原图类型选择…

【云原生| K8S系列】Kubernetes Daemonset,全面指南

Kubernetes中的DaemonSet是什么? Kubernetes是一个分布式系统&#xff0c;Kubernetes平台管理员应该有一些功能可以在所有节点上运行特定于平台的应用程序。例如&#xff0c;在所有Kubernetes节点上运行日志代理。 这就是Daemonset发挥作用的地方。 Daemonset是一个原生的K…

查询满足条件的元组-WHRER子句(运算符、BETWEEN 、LIKE、IN、NULL)

一、WHERE子句&#xff08;筛选出使选择表达式为真的元组&#xff09; 1、SELECT-FROM子句可以实现数据的查询&#xff08;会查询出所有元组&#xff09;&#xff0c;加上WHERE子句之后可以实现数据的筛选&#xff08;会查询出满足条件的元组&#xff09; SELECT 【ALL|DISTI…

windows 下 基于 WSL2安装DeepSpares进行YOLOV8 v5 的加速推理

文章大纲 简介软硬件限制安装安装 WSL2 基础环境WSL2 手动安装安装 miniconda 环境本地USB 摄像头使用:Windows 无延迟视频流本地USB 摄像头使用:WSL2 挂载 本地 USB 摄像头WSL2更新报错: 离线安装 wsl --update安装 DeepSpares测试打开本地USB 摄像头进行测试测试结果参考文…

50.Python-web框架-Django中引入静态的bootstrap样式

目录 Bootstrap 官网 特性 下载 在线样例 Bootstrap 入门 Bootstrap v5 中文文档 v5.3 | Bootstrap 中文网 在django中使用bootstrap 新建static\bootstrap5目录&#xff0c;解压后的Bootstrap文件&#xff0c;拷贝项目里就好。 在template文件里引用css文…

Nginx+KeepAlived高可用负载均衡集群的部署

目录 一.KeepAlived补充知识 1.一个合格的群集应该具备的特点 2.健康检查&#xff08;探针&#xff09;常用的工作方式 3.相关面试问题 问题1 问题2 二.Keepealived脑裂现象 1.现象 2.原因 硬件原因 运用配置原因 3.解决 4.预防 方法1 方法2 方法3 方法4 三.…

VUE之重定向redirect

VUE之路由和重定向redirect 这个小知识点是在学习做项目的时候遇到的一个问题&#xff0c;借鉴了一个他人的项目&#xff0c;是一个酒店管理系统&#xff0c;拿到源码之后导到我的vscode里。 参考链接 导的过程比较顺利&#xff0c;正常安装&#xff0c;加依赖&#xff0c;没有…

禁渔期水域监管:EasyCVR视频智能监控方案

一、背景与需求分析 根据农业部印发的《中国渔政亮剑2024系列专项执法行动方案》&#xff0c;我国将持续推进长江十年禁渔、海洋伏季休渔、黄河等内陆重点水域禁渔等专项行动。根据四川省相关规定&#xff0c;每年3月1日至6月30日为禁渔期&#xff0c;在此期间&#xff0c;四川…

坚持每天学编程的有多少?聊聊有多少人躺平了,工作生活压力大吗

以前刚开始学编程的时候&#xff0c;一晚上就能看完一本Frontpage网页编程的书&#xff0c;就像是WORD一样简单&#xff0c;第二天就敢去找工作。工作后学习VB6SQL SERVER数据库&#xff0c;几百页的大部头书&#xff0c;基本上一个月也能看完&#xff0c;后面还买了2个大书柜&…

【全开源】Java无人共享棋牌室茶室台球室系统JAVA版本支持微信小程序+微信公众号

无人共享棋牌室系统——棋牌娱乐新体验 &#x1f3b2;引言 随着科技的不断发展&#xff0c;传统棋牌室正逐渐迈向智能化、无人化。今天&#xff0c;我要为大家介绍的就是这款引领潮流的“无人共享棋牌室系统”。它不仅为棋牌爱好者提供了全新的娱乐体验&#xff0c;更在便捷性…