117 个版本 (27 个稳定版)

1.2.6 2024年8月13日
1.2.5 2024年7月17日
1.2.4 2024年6月14日
1.1.10 2024年2月7日
0.2.0 2016年7月12日

#27 in 文件系统

Download history 1516/week @ 2024-05-04 1290/week @ 2024-05-11 1468/week @ 2024-05-18 1741/week @ 2024-05-25 1868/week @ 2024-06-01 1575/week @ 2024-06-08 1592/week @ 2024-06-15 1624/week @ 2024-06-22 1419/week @ 2024-06-29 1385/week @ 2024-07-06 1819/week @ 2024-07-13 1140/week @ 2024-07-20 1877/week @ 2024-07-27 1291/week @ 2024-08-03 1635/week @ 2024-08-10 1469/week @ 2024-08-17

6,553 每月下载量
用于 12 个Crates (9 个直接使用)

MIT 许可证

1.5MB
33K SLoC

协议请求和响应匹配

此库实现了匹配HTTP请求和响应所需的核心匹配逻辑。它基于V3协议规范

在线rust文档

使用方法

要使用它,请在您的Cargo清单中将其添加到依赖项

[dependencies]
pact_matching = "1.1"

此包提供三个函数:`match_request`、`match_response` 和 `match_message`。这些函数接收来自 `pact_models` 包的预期和实际请求、响应或消息模型,并返回一个不匹配的向量。

要比较任何传入请求,首先需要将其转换为 `pact_models::Request` 类型,然后可以进行比较。对于任何响应也是如此。

包功能

默认启用所有功能

  • datetime:启用对日期和时间表达式的支持以及生成器。这将添加 chronos 包作为依赖项。
  • xml:启用解析XML文档的支持。此功能将添加 sxd-document 包作为依赖项。
  • plugins:启用使用插件的支撑。此功能将添加 pact-plugin-driver 包作为依赖项。
  • multipart:启用对MIME多部分体的支持。此功能将添加multer库作为依赖项。

读取和写入Pact文件

位于pact_models库中的Pact结构体提供了读取和写入pact JSON文件的方法。它支持所有V4版本之前的规范版本,但将V1、V1.1和V2规范文件转换为V3格式。

匹配请求和响应部分

支持V3规范匹配JSON和XML正文、头部、查询字符串和请求路径。

要了解匹配的基本规则,请参阅注意事项。例如,匹配的测试案例,请参阅Pact规范项目,版本3

默认情况下,Pact将遵循Postel定律使用字符串等价匹配。这意味着要使实际值与预期值匹配,它们必须由相同的字符序列组成。对于集合(基本上是映射和列表),它们必须具有相同顺序匹配的相同元素,对于实际映射中额外的元素,将忽略这些元素。

可以根据伪JSON-Path语法为请求和响应元素定义匹配规则。

匹配正文

在大多数情况下,匹配涉及在JSON或XML格式中匹配请求和响应正文。其他格式将具有自己的匹配规则,或遵循JSON规则。

JSON正文匹配规则

正文由对象(键值对的映射)、数组(列表)和值(字符串、数字、true、false、null)组成。正文匹配规则以$.为前缀。

以下方法用于确定两个正文是否匹配

  1. 如果实际正文和预期正文都为空,则正文匹配。
  2. 如果实际正文非空,而预期正文为空,则正文匹配。
  3. 如果实际正文为空,而预期正文非空,则正文不匹配。
  4. 否则,对正文的内容进行比较。
对于正文内容比较
  1. 如果实际值和预期值都是对象,则按映射进行比较。
  2. 如果实际值和预期值都是数组,则按列表进行比较。
  3. 如果预期值是对象,而实际值不是,则它们不匹配。
  4. 如果预期值是数组,而实际值不是,则它们不匹配。
  5. 否则,比较值
对于比较映射
  1. 如果实际映射非空而预期映射为空,则它们不匹配。
  2. 如果我们允许意外的键,并且预期键的数量大于实际键的数量,则它们不匹配。
  3. 如果我们不允许意外的键,并且预期映射和实际映射的键的数量不同,则它们不匹配。
  4. 否则,对于每个预期键值对
    1. 如果实际映射包含该键,则比较值
    2. 否则它们不匹配

Postel定律决定了是否允许意外的键。

对于比较列表
  1. 如果已定义一个匹配器来匹配列表的路径,则默认为此匹配器,然后比较列表内容。
  2. 如果预期列表为空而实际列表不为空,则列表不匹配。
  3. 否则
    1. 比较列表大小
    2. 比较列表内容
对于比较列表内容
  1. 对于预期列表中的每个值
    1. 如果值的索引小于实际列表的大小,则使用比较值的方法将值与同一索引的实际值进行比较。
    2. 否则该值不匹配
对于比较值
  1. 如果已定义一个匹配器来匹配值的路径,则默认为此匹配器
  2. 否则使用等价性比较值

XML体匹配规则

XML体由根元素、元素(带有子元素的列表)、属性(映射)和值(字符串)组成。体匹配规则以$.开头。

以下方法用于确定两个正文是否匹配

  1. 如果实际正文和预期正文都为空,则正文匹配。
  2. 如果实际正文非空,而预期正文为空,则正文匹配。
  3. 如果实际正文为空,而预期正文非空,则正文不匹配。
  4. 否则,对正文的内容进行比较。
对于正文内容比较

首先比较根元素。

对于比较元素
  1. 如果定义了一个匹配元素路径的体匹配器,则在元素名称或子元素上使用该匹配器。
  2. 否则,如果元素具有相同的名称,则它们匹配。

然后,如果没有不匹配

  1. 比较元素的属性
  2. 比较子元素
  3. 比较文本节点
对于比较属性

属性被视为键值对的映射。

  1. 如果实际映射非空而预期映射为空,则它们不匹配。
  2. 如果我们允许意外的键,并且预期键的数量大于实际键的数量,则它们不匹配。
  3. 如果我们不允许意外的键,并且预期映射和实际映射的键的数量不同,则它们不匹配。

然后,对于每个预期的键值对

  1. 如果实际映射包含该键,则比较值
  2. 否则它们不匹配

根据是否允许意外键,Postel定律决定了我们是否允许意外键。注意对于匹配路径,属性名称以@开头。

对于比较子元素
  1. 如果为子元素的路径定义了匹配器,则将预期的子元素填充到与实际子元素相同的大小。
  2. 否则
    1. 如果实际子元素不为空而预期为空,则它们不匹配。
    2. 如果我们允许意外键,并且预期的子元素数量大于实际子元素,则它们不匹配。
    3. 如果我们不允许意外键,并且预期的和实际子元素的元素数量不同,则它们不匹配。

然后,对于每个预期的和实际元素对,使用比较元素的规则进行比较。

对于比较文本节点

将文本节点合并为单个字符串,然后作为值进行比较。

  1. 如果定义了一个匹配文本节点路径的匹配器(文本节点路径以#text结尾),则默认使用该匹配器
  2. 否则使用相等性比较文本。
对于比较值
  1. 如果已定义一个匹配器来匹配值的路径,则默认为此匹配器
  2. 否则使用等价性比较值

匹配路径

以下内容通过以下方式匹配路径

  1. 如果为path定义了匹配器,则默认使用该匹配器。
  2. 否则将路径作为字符串进行比较

匹配查询

  1. 如果实际和预期的查询字符串都为空,则它们匹配。
  2. 如果实际不为空而预期为空,则它们不匹配。
  3. 如果实际为空而预期不为空,则它们不匹配。
  4. 否则将两者转换为键映射到值列表的映射,并比较这些映射。

匹配查询映射

查询字符串被解析为键映射到值列表的映射。键值对可以以任何顺序出现,但当一个键出现多次时,值按照在查询字符串中出现的顺序进行比较。

匹配头部

  1. 通过键对头部进行不区分大小写的排序
  2. 对于排序列表中的每个预期头部
    1. 如果实际头部包含该键,则比较头部值
    2. 否则头部不匹配

对于匹配头部值

  1. 如果为header.<HEADER_KEY>定义了匹配器,则默认使用该匹配器
  2. 否则删除逗号后的所有空白,并比较得到的字符串。

匹配请求头部

通过排除cookie头部来匹配请求头部。

匹配请求cookie

如果预期的cookie列表包含所有实际的cookie,则cookie匹配。

匹配状态代码

状态代码作为整数值进行比较。

匹配HTTP方法

实际和预期的方法作为不区分大小写的字符串进行比较。

匹配规则

Pact 支持在每种类型的对象(请求或响应)上扩展匹配规则,通过在 pact 文件中使用 matchingRules 元素来实现。这是一个 JSON 路径字符串到匹配器的映射。当比较一个项时,如果匹配规则中存在与该项路径相对应的条目,比较将委托给定义的匹配器。请注意,匹配规则是级联的,因此可以在值上指定规则,并将应用于该值的所有子项。

匹配器路径表达式

Pact 不支持完整的 JSON 路径表达式,仅支持以下规则

  1. 所有路径都以美元符号 ($) 开头,表示根。
  2. 所有路径元素通过点 (.) 分隔,除了数组索引,它使用方括号 ([])。
  3. 路径元素代表键。
  4. 星号 (*) 可以用于匹配映射的所有键或数组的所有项(仅限一个级别)。

因此,表达式 $.item1.level[2].id 将匹配以下正文中的高亮项

{
  "item1": {
    "level": [
      {
        "id": 100
      },
      {
        "id": 101
      },
      {
        "id": 102 // <---- $.item1.level[2].id
      },
      {
        "id": 103
      }
    ]
  }
}

$.*.level[*].id 将匹配所有项的所有级别的所有 id。

匹配器选择算法

由于星号表示法,可以定义多个与项对应的匹配器路径。通过将权重分配给每个路径元素并取权重的乘积,选择第一个最具体的表达式。使用具有最大权重的路径的匹配器。

  • 根节点 ($) 被分配值为 2。
  • 不匹配的任何路径元素被分配值为 0。
  • 匹配路径元素的任何属性名被分配值为 2。
  • 匹配路径元素的任何数组索引被分配值为 2。
  • 匹配属性或数组索引的任何星号 (*) 被分配值为 1。
  • 其余所有内容被分配值为 0。

因此,对于带有高亮项的正文

{
  "item1": {
    "level": [
      {
        "id": 100
      },
      {
        "id": 101
      },
      {
        "id": 102 // <--- Item under consideration
      },
      {
        "id": 103
      }
    ]
  }
}

表达式将具有以下权重

表达式 权重计算 权重
$ $(2) 2
$.item1 $(2).item1(2) 4
$.item2 $(2).item2(0) 0
$.item1.level $(2).item1(2).level(2) 8
$.item1.level[1] $(2).item1(2).level(2)[1(2)] 16
$.item1.level[1].id $(2).item1(2).level(2)[1(2)].id(2) 32
$.item1.level[1].name $(2).item1(2).level(2)[1(2)].name(0) 0
$.item1.level[2] $(2).item1(2).level(2)[2(0)] 0
$.item1.level[2].id $(2).item1(2).level(2)[2(0)].id(2) 0
$.item1.level[*].id $(2).item1(2).level(2)[*(1)].id(2) 16
$.*.level[*].id $(2.(1).level(2)[(1)].id(2) 8

因此,对于 id 为 102 的项,将选择具有路径 $.item1.level[1].id 和权重 32 的匹配器。

支持的匹配器

以下匹配器受到支持

匹配器 规范版本 示例配置 描述
相等性 V1 { "match": "equality" } 这是默认匹配器,依赖于等于运算符
正则表达式 V2 { "match": "regex", "regex": "\\d+" } 这将对值的字符串表示执行正则表达式匹配
类型 V2 { "match": "类型" } 这将对值执行基于类型的匹配,即如果它们的类型相同,则它们相等。
MinType V2 { "match": "类型", "最小值": 2 } 这将对值执行基于类型的匹配,即如果它们的类型相同,则它们相等。此外,如果值表示一个集合,实际值的长度将与最小值进行比较。
MaxType V2 { "match": "类型", "最大值": 10 } 这将对值执行基于类型的匹配,即如果它们的类型相同,则它们相等。此外,如果值表示一个集合,实际值的长度将与最大值进行比较。
MinMaxType V2 { "match": "类型", "最大值": 10, "最小值": 2 } 这将对值执行基于类型的匹配,即如果它们的类型相同,则它们相等。此外,如果值表示一个集合,实际值的长度将与最小值和最大值进行比较。
包含 V3 { "match": "包含", "": "子串" } 这检查值的字符串表示是否包含子串。
整数 V3 { "match": "整数" } 这检查值的类型是否为整数。
小数 V3 { "match": "小数" } 这检查值的类型是否为带小数点的数字。
数字 V3 { "match": "数字" } 这检查值的类型是否为数字。
时间戳 V3 { "match": "日期时间", "格式": "yyyy-MM-dd HH:ss:mm" } 将值的字符串表示与日期时间格式进行匹配
时间 V3 { "match": "时间", "格式": "HH:ss:mm" } 将值的字符串表示与时间格式进行匹配
日期 V3 { "match": "日期", "格式": "yyyy-MM-dd" } 将值的字符串表示与日期格式进行匹配
空值 V3 { "match": "空值" } 如果值是空值(这取决于内容,对于JSON将匹配JSON空值)
布尔值 V3 { "match": "布尔值" } 如果值是布尔值(布尔值和字符串值truefalse
内容类型 V3 { "match": "内容类型", "": "image/jpeg" } 通过其内容类型(魔数文件检查)匹配二进制数据
V3 { "match": "" } 匹配映射中的值,忽略键
ArrayContains V4 { "match": "arrayContains", "变体": [...] } 检查所有变体是否都存在于数组中。
状态码 V4 { "match": "状态码", "状态": "成功" } 匹配响应状态码。
非空 V4 { "match": "非空" } 值必须存在且不为空(非null或空字符串)
semver V4 { "match": "semver" } 值必须根据semver规范有效
semver V4 { "match": "semver" } 值必须根据semver规范有效
EachKey V4 { "match": "eachKey", "规则": [{"match": "regex", "regex": "\\$(\\.\\w+)+"}], "": "$.test.one" } 允许定义应用于映射键的匹配规则
EachValue V4 { "match": "eachValue", "规则": [{"match": "regex", "regex": "\\$(\\.\\w+)+"}], "": "$.test.one" } 允许定义应用于集合值的匹配规则。对于映射,委托给Values匹配器。

匹配规则定义语言

匹配规则定义语言是一种文本格式,用于指定应用于插件数据格式的匹配规则。它允许在不需要指定数据格式的具体细节的情况下应用匹配规则。

例如,它们可以同样应用于非常不同的数据格式,如Protobuf和CSV。

CSV

"column:Date", "matching(datetime, 'yyyy-MM-dd','2000-01-01')"

Protobuf

"contents": {
    "contentType": "notEmpty('application/json')",
    "content": "matching(contentType, 'application/json', '{}')",
    "contentTypeHint": "matching(equalTo, 'TEXT')"
}

匹配规则定义

每个匹配规则定义是一个以逗号分隔的函数列表,其中包含括号内的一定数量的参数。通常只需要为单个值定义一个定义,但如果需要多个定义,它们只需用逗号分隔即可。

匹配函数

主函数是matching函数。这个函数根据类型和多个值创建一个匹配规则。所需值取决于匹配规则的类型。

例如,使用正则表达式匹配:matching(regex, '\\$(\\.\\w+)+', '$.test.one')

equalTo

指定属性/字段必须等于示例值。

参数

  • example (原始值)

示例

matching(equalTo, 'TEXT')
类型

指定属性/字段必须具有与示例值相同的类型。

参数

  • example (原始值)

示例

matching(type, 100)
数字类型(数字、整数、小数)

指定属性/字段必须是数字类型。number将匹配任何数值,integer将匹配没有小数(小数点后没有有效数字)的数值,而decimal将匹配有小数的数值(至少有一个有效数字在小数点后)。

参数

  • example (整数或小数值)

示例

matching(integer, 100)
matching(decimal, 100.1234)
日期和时间匹配器(datetime、date、time)

指定属性/字段的字符串表示形式必须与格式说明符匹配。这些基于Java DateTimeFormatter。

参数

  • 格式(字符串)
  • example (字符串)

示例

matching(datetime, 'yyyy-MM-dd HH:mm:ss', '2021-10-07 13:00:13')
matching(date, 'yyyy-MM-dd', '2021-10-07')
matching(time, 'HH:mm:ss', '13:00:13')
正则表达式

指定属性/字段的字符串表示形式必须与提供的正则表达式匹配。

参数

  • regex(字符串)
  • example (字符串)

示例

matching(regex, '\w+ \w+', 'Hello World')
包含

指定属性/字段的字符串表示形式必须包含给定的字符串。

参数

  • example (字符串)

示例

matching(include, 'Hello World')
布尔值

指定属性/字段必须是布尔值,或者其字符串表示形式必须是字符串truefalse

参数

  • example (布尔值)

示例

matching(boolean, false)
semver

指定属性/字段的字符串表示形式必须符合semver规范的有效语义版本。

参数

  • example (字符串)

示例

matching(semver, '1.0.0')
内容类型

指定属性/字段的字节字符串表示形式必须通过魔数文件检查匹配给定的内容类型。这通过将前几个字节与规则数据库进行比较来确定内容的类型。

参数

  • 内容类型(MIME格式,字符串)
  • example (字符串)

示例

matching(contentType, 'application/json', '{}')
内容类型 - 二进制内容的检测机制

pact_matching目前对匹配二进制内容类型执行以下操作

  1. 确定用户在测试中请求的expected Content-Type标题
  2. 使用infer库读取内容缓冲区,并根据魔数字节猜测Content-Type
  3. 如果失败
    1. 使用tree_magic_mini库读取内容缓冲区,并根据shared-mime-info数据库猜测Content-Type
      1. 由于GPL限制,MagicDB不随pact_matching一起提供,用户可以手动添加
        1. Linux Alpine - apk add shared-mime-info
        2. MacOS - brew install shared-mime-info
          1. arm64 MacOS需要tree_magic_mini 分支
        3. Linux - Debianapt-get install -y shared-mime-info
  4. 如果任一结果返回text/plain,则手动使用pact_models中的detect_content_type_from_bytes函数读取字节。
  5. 如果所有 234 失败,则抛出错误,否则返回 Ok

使用的 Rust 库

通过引用匹配示例类型

类型匹配也可以通过示例的引用来指定。引用由美元符号($)后跟字符串值定义。字符串值必须是包含示例类型的属性/字段名。

参数

  • 引用名称

示例

matching($'items') // where items is the name of the example to match the types against

非空

指定属性字段必须存在并包含一个值(非空或空字符串)。

参数

  • 示例(原始值)

示例

notEmpty('DateTime')

EachKey

允许将匹配规则定义应用于映射中的键。

参数

  • 定义*(匹配规则定义的逗号分隔列表)

示例

eachKey(matching(regex, '\\$(\\.\\w+)+', '$.test.one'))

EachValue

允许将匹配规则定义应用于集合(列表/数组或映射形式)中的值。

参数

  • 定义*(匹配规则定义的逗号分隔列表)

示例

eachValue(matching($'items'))

语法

匹配规则定义语言(ANTLR 4 格式)的语法

grammar MatcherDefinition;

matchingDefinition :
    matchingDefinitionExp  ( COMMA matchingDefinitionExp  )* EOF
    ;

matchingDefinitionExp :
    (
      'matching' LEFT_BRACKET matchingRule RIGHT_BRACKET 
      | 'notEmpty' LEFT_BRACKET primitiveValue RIGHT_BRACKET 
      | 'eachKey' LEFT_BRACKET matchingDefinitionExp RIGHT_BRACKET 
      | 'eachValue' LEFT_BRACKET matchingDefinitionExp RIGHT_BRACKET 
    )
    ;

matchingRule :
  (
    ( 'equalTo' | 'type' ) COMMA primitiveValue )
  | 'number' COMMA ( DECIMAL_LITERAL | INTEGER_LITERAL ) 
  | 'integer' COMMA INTEGER_LITERAL 
  | 'decimal' COMMA DECIMAL_LITERAL 
  | ( 'datetime' | 'date' | 'time' ) COMMA string COMMA string 
  | 'regex' COMMA string COMMA string 
  | 'include' COMMA string 
  | 'boolean' COMMA BOOLEAN_LITERAL 
  | 'semver' COMMA string 
  | 'contentType' COMMA string COMMA string 
  | DOLLAR string 
  ;

primitiveValue :
  string 
  | DECIMAL_LITERAL
  | INTEGER_LITERAL
  | BOOLEAN_LITERAL
  ;

string :
  STRING_LITERAL 
  | 'null'
  ;

INTEGER_LITERAL : '-'? DIGIT+ ;
DECIMAL_LITERAL : '-'? DIGIT+ '.' DIGIT+ ;
fragment DIGIT  : [0-9] ;

LEFT_BRACKET    : '(' ;
RIGHT_BRACKET   : ')' ;
STRING_LITERAL  : '\'' (~['])* '\'' ;
BOOLEAN_LITERAL : 'true' | 'false' ;
COMMA           : ',' ;
DOLLAR          : '$';

WS : [ \t\n\r] + -> skip ;

依赖关系

~20–56MB
~1M SLoC