JAF 规范 v0.1

JAF 规范 v0.1

1. 概述

JAF (JSON API Flow) 是一种用于快速测试 JSON API 的轻量级脚本语言。支持变量、路径级联、登录态管理和响应断言。

使用场景: 快速开发迭代中的 JSON API 服务器功能验证,不适用于生产环境监控、性能测试、复杂集成测试。

设计原则:

  • 理解、生成优先于执行
  • 只支持 JSON,无其他格式
  • 线性执行,无循环条件
  • 人类可读,LLM 友好
  • 够用即可,避免过度设计
  • Fast-fail 策略,简化错误处理
  • 脚本使用 UTF-8 编码
  • 不处理数字精度和科学计数法(按宿主语言处理)
  • 变量循环引用由具体实现处理
  • 变量只支持 primitive 类型(string/number/boolean/null),不支持 object/array
  • 变量替换为文本级 :变量值直接转为字符串插入 JSON,用户负责确保替换后为合法 JSON
  • 指令严格单行,请求 JSON 可跨多行格式化
  • 符号语义独立?= 在 JAF 中表示"默认值",与 Shell 等其他语言语义不同

2. 适用范围

支持:

  • 请求:JSON body(自动序列化为 JSON 字符串),有限 header 注入
  • 响应:JSON body,状态码,响应头读取(大小写敏感)
  • 变量替换:${ENV}@{var} 支持出现在 uri、header value、body 字段值中

不支持:

  • 二进制 body、非 JSON 响应(视为错误)
  • 文件上传、multipart/form-data、url-encoded-form
  • Cookie 管理、重定向跟随、复杂认证流程(OAuth 等)
  • 响应体大于内存、流式响应

3. 基础语法

3.1 四要素

jaf 复制代码
#!      变量定义、默认值、路径前缀、session(单行)
#@      单条请求配置(timeout/assert/ignore error)(单行)
#>      响应结构定义与捕获(单行)
{}      HTTP 请求(JSON,可多行)
#       行内注释(单行)

3.2 注释

jaf 复制代码
# 单行注释
#!var HOST = localhost:3000    # 行尾注释

4. 变量系统

4.1 变量定义

jaf 复制代码
#!var HOST = localhost:3000        # 内部变量,脚本全局作用域
#!env API_KEY                      # 环境变量(必须存在,无默认值)
#!env TIMEOUT ?= 5000              # 环境变量带默认值(?= 表示若未设置则使用)
#!unset VAR                        # 删除变量

4.2 变量引用

语法 说明 作用域 生命周期
${ENV} 环境变量 进程级 脚本执行期间
${ENV?=default} 环境变量带默认值(JAF 语义:若未定义使用 default) 进程级 脚本执行期间
@{var} 上下文变量(字符串) 脚本全局 脚本执行期间,跨请求

变量不可变规则:

  • 变量一旦定义不可重新赋值(包括 #!var 和捕获)
  • 捕获变量必须是新标识符,不能覆盖已有变量
  • 环境变量 ${ENV} 不受此限制(外部可变更)
  • 变量只支持 primitive 类型(string/number/boolean/null),不支持 object/array

4.3 内置变量

实现应提供以下内置变量(只读,不可覆盖):

变量 说明 示例
@{_uuid_} UUID v4 字符串 "550e8400-e29b-41d4-a716-446655440000"
@{_timestamp_} Unix 时间戳(秒) 1704067200
@{_timestamp_ms_} Unix 时间戳(毫秒) 1704067200000
@{_iso_timestamp_} ISO 8601 格式 "2024-01-15T10:30:00.000Z"
@{_random_} 0-1 随机小数 0.7423

5. 路径级联(#!base)

5.1 定义基础路径

jaf 复制代码
#!base = http://api.com/v1

5.2 相对与绝对路径

形式 行为 示例
/ 开头 相对 #!base 拼接 /usershttp://api.com/v1/users
http:// https:// 开头 绝对路径,忽略 base https://other.com/api
// 开头 协议相对 //cdn.com/file
纯变量 展开后判断:http(s)开头为绝对路径,其他相对#!base @{endpoint}

5.3 URI 变量替换

jaf 复制代码
{"uri":"/users/${API_VERSION}/@{userId}"}     # 混合使用
{"uri":"https://@{host}/api"}                 # 变量作为域名

URL 编码: 变量替换后,路径组件进行 URL 编码(保留 / 作为分隔符,编码 ?&#、空格等,禁止或编码 ../ 序列)。绝对路径变量不编码但校验协议。


6. 默认值(#!default)

用途: 仅用于 HTTP 请求参数(method, headers),不涉及 uribodytimeout

语法: 单行 JSON,支持变量替换

jaf 复制代码
#!default {"method": "POST", "headers": {"content-type": "application/json"}}

与请求合并规则:

  • method:请求覆盖 default,请求无则使用 default
  • headers:深度合并,请求同 key 覆盖 default
  • uri:请求必须提供,default 不提供 uri
  • body:请求独有,default 不提供 body

覆盖规则: 单行覆盖,后续 #!default 完全替换前者

jaf 复制代码
#!default {"method": "POST"}                    # 第一行
#!default {"headers": {"x-request-id": "@{_uuid_}"}}    # 完全替换,method 恢复默认

清空全部:

jaf 复制代码
#!default {}    # 清空所有默认值,恢复内置默认

用户责任: 变量替换后必须是合法 JSON,否则报错停止。


7. 登录与 Session

7.1 Session 定义(延迟求值)

语法: 单行 JSON,多行累积合并

jaf 复制代码
#!session {"headers": {"Authorization": "Bearer @{token}"}}
#!session {"headers": {"X-Custom": "value"}}    # 累积合并,后者覆盖同 key

合并规则: 多行 #!session 累积,后续覆盖前相同 key

求值规则:

  • @{var} 延迟求值 ,在 #!session begin 执行时才展开
  • 允许先定义会话,后捕获变量
  • 若 begin 时变量未定义,报错停止

7.2 Session 控制

jaf 复制代码
#!session begin    # 进入 session,注入头部生效(此时求值)
#!session end      # 退出 session,清除注入头部

禁止嵌套: 进入新会话前必须先结束当前会话,否则报错停止。

7.3 完整流程

jaf 复制代码
# 1. 登录并捕获 token
{
  "uri": "/auth",
  "body": {
    "user": "u",
    "pass": "p"
  }
}
#> capture {"$.token": "token"}

# 2. 定义会话(多行累积,token 可尚未定义)
#!session {"headers": {"Authorization": "Bearer @{token}"}}
#!session {"headers": {"X-Request-ID": "@{_uuid_}"}}

# 3. 进入会话(此时展开 token)
#!session begin
{"uri": "/posts"}          # 自动带 Authorization 和 X-Request-ID
{"uri": "/profile"}        # 同上

# 4. 退出会话
#!session end
{"uri": "/public"}         # 无注入头部

# 5. 可重新进入(每次 begin 重新求值当前变量)
#!session begin
{"uri": "/new-post"}
#!session end

头部优先级(高→低):

  1. 请求内 headers 字段
  2. #!session 注入头部
  3. #!default headers
  4. 内置默认

8. 请求配置(#@)

jaf 复制代码
#@ timeout 5000           # 超时毫秒(无默认值,不设置则使用实现默认)
#@ assert $status == 200  # 断言条件
#@ ignore error           # 忽略当前请求错误(默认fast fail)

Fast-fail 策略:

场景 行为
网络不可达、DNS 失败 停止
超时 停止
非 JSON 响应 停止
状态码 4xx/5xx(无 ignore) 停止
断言失败(无 ignore) 停止
变量未定义 停止(语法错误)
Session begin 变量未定义 停止
类型转换失败 停止(或 ignore 时继续)

#@ ignore error 仅忽略当前请求的执行失败(网络/超时/状态码/断言),继续执行下一请求

断言不支持复合条件&& || !),通过多条 assert 实现:

jaf 复制代码
#@ assert $status == 200
#@ assert $.id > 0

9. 响应定义(#>)

9.1 路径引用

前缀 含义 示例
$$body 响应体根对象 $, $.data.id
$header. 响应头(大小写敏感) $header.Content-Type
$status HTTP 状态码 $status == 200

数组索引:

jaf 复制代码
$.items[0]        # 数组第0项
$.items[0].name   # 嵌套访问
$.errors[0].code  # 常用场景

简写规则: $.$body. 的简写,$body 本身可单独使用表示整个响应体。

9.2 响应捕获(单行 JSON,多行累积)

语法: {"path": "var"}{"path": "var:type"}

jaf 复制代码
#> capture {"$.token": "auth_token"}
#> capture {"$.expires": "expires:number"}
#> capture {"$header.X-Request-ID": "rid"}
#> capture {"$.items[0].id": "firstId"}

合并规则: 多行累积,同 var 后者覆盖前者

9.3 响应期望(单行 JSON)

jaf 复制代码
#> expect {"status": "ok", "code": <number>, "data": <object>}

9.4 类型系统

类型 说明 示例
string 字符串 "name": <string>
number 数字 "count": <number>
boolean 布尔 "active": <boolean>
array 数组 "items": <array>
object 对象 "data": <object>
null 空值 "deleted": <null>
any 任意类型 "meta": <any>
string? 可选字段 "nickname": <string?>

捕获规则:

  • 捕获语法 {"path": "var:type"} 尝试类型转换
  • 捕获路径指向非 primitive 值(object/array)时报错停止
  • 转换结果为 primitive 类型,赋值给变量
  • 转换失败则失败(若 #@ ignore error 则继续)

9.5 匹配规则

jaf 复制代码
# 精确值匹配
#> expect {"status": "ok", "count": 5}

# 类型匹配
#> expect {"id": <string>, "count": <number>}

# 忽略其他字段
#> expect {"id": <string>, "...": "..."}

# 捕获与匹配组合(捕获先执行,匹配后执行)
#> capture {"$.id": "pid"}
#> expect {"status": "ok"}

执行顺序: 捕获始终先执行,匹配失败则停止(或忽略错误继续)


10. 请求格式

jaf 复制代码
# body 字段必须是 JSON 对象/数组,运行时序列化为 JSON 字符串发送
# Content-Type 默认为 application/json(可通过 #!default 覆盖)

# 单行请求
{"uri": "/api", "body": {"key": "value"}}

# 多行请求(格式化 JSON)
{
  "uri": "/api",
  "body": {
    "key": "value",
    "nested": {
      "data": [1, 2, 3]
    }
  }
}

# 不支持:
# - form-data(无文件上传)
# - x-www-form-urlencoded
# - 原始二进制

变量替换规则(文本级):

  • 变量值(primitive)直接转为字符串插入
  • 用户负责确保替换后为合法 JSON,否则报错停止
jaf 复制代码
#!var name = "tom"
{"body": {"user": "@{name}"}}        # 结果: {"user": "tom"}

#!var count = 123
{"body": {"total": @{count}}}        # 结果: {"total": 123}  (数字直接插入)

# 危险示例(用户责任):
#!var bad = "a\"b"                   # 含未转义引号
{"body": {"x": "@{bad}"}}            # 结果: {"x": "a"b"}  ← 非法JSON,报错

多行请求规则:

  • {[ 开始的行启动请求解析
  • 累积至匹配的 }] 结束
  • 期间不允许空行、不允许注释、必须是合法 JSON
  • 结束标志:匹配闭合符后的换行,下一行必须是新指令或新请求

11. 完整示例

jaf 复制代码
# 场景:验证用户 CRUD
# 假设:API 基础路径由环境变量提供,默认 localhost:3000

#!base = http://${API_HOST?=localhost:3000}/api/v1
#!default {"method": "POST", "headers": {"content-type": "application/json"}}

# 创建用户(多行请求)
{
  "uri": "/users",
  "body": {
    "name": "test",
    "email": "@{_uuid_}@test.com"
  }
}
#> capture {"$.id": "uid"}
#> capture {"$.createdAt": "created"}
#> expect {"status": "created"}
#@ assert $status == 201

# 获取用户(URI 变量替换)
{"uri": "/users/@{uid}"}
#> expect {"id": <string>, "name": "test"}
#@ assert $.id == @{uid}

# 更新(Session 注入认证头)
#!session {"headers": {"Authorization": "Bearer @{auth_token}"}}
#!session {"headers": {"X-Request-ID": "@{_uuid_}"}}
#!session begin

{
  "uri": "/users/@{uid}",
  "body": {"name": "updated"}
}
#@ assert $status == 200

#!session end

# 删除(允许 404,可能已被删除)
#@ ignore error
{"method": "DELETE", "uri": "/users/@{uid}"}
#@ assert $status == 204
#@ assert $status == 404

12. 语法速查

指令 用途
#!base = URL 设置路径前缀
#!var NAME = VALUE 定义内部变量(primitive)
#!env NAME / #!env NAME ?= DEFAULT 环境变量(?= 为 JAF 语义:默认值)
#!unset NAME 删除变量
#!default {...} 设置 HTTP 请求默认值(单行,覆盖)
#!default {} 清空所有默认值
#!session {...} 定义会话注入(单行,累积合并)
#!session begin 进入会话(求值并注入)
#!session end 退出会话(清除注入)
#@ timeout MS 设置超时(毫秒)
#@ assert CONDITION 断言检查
#@ ignore error 忽略当前请求错误
#> capture {"path": "var"} 响应捕获(单行,累积)
#> capture {"path": "var:type"} 带类型转换
#> expect {...} 响应结构验证(单行)
# COMMENT 单行注释

路径规则

形式 行为
/path 相对 #!base
http://... https://... 绝对路径
//... 协议相对
$.items[0] 数组索引

类型标记

语法 含义
<type> 仅类型检查
{"path": "var"} 捕获为字符串
{"path": "var:type"} 捕获并类型转换
FIELD? 可选字段

变量引用

语法 含义
${ENV} 环境变量
${ENV?=default} 环境变量,未定义时使用 default(JAF 语义)
@{var} 上下文变量

13. EBNF 语法

ebnf 复制代码
JAF           ::= { Line } EOF
Line          ::= Directive | Request | Comment | Empty
Directive     ::= "#!" ( Base | Var | Env | Unset | Default | Session )
              | "#@" ( Timeout | Assert | IgnoreError )
              | "#>" ( Capture | Expect )
Base          ::= "base" "=" URI
Var           ::= "var" IDENTIFIER "=" Primitive
Env           ::= "env" IDENTIFIER [ "?=" Primitive ]
Unset         ::= "unset" IDENTIFIER
Default       ::= "default" JSON_Object
Session       ::= "session" ( JSON_Object | "begin" | "end" )
Timeout       ::= "timeout" NUMBER
Assert        ::= "assert" Condition
IgnoreError   ::= "ignore" "error"
Capture       ::= "capture" Capture_Pair
Capture_Pair  ::= "{" ( STRING | Path ) ":" Capture_Target "}"
Capture_Target::= IDENTIFIER [ ":" Type ]
Expect        ::= "expect" JSON_Object
Request       ::= JSON_Object | JSON_Array
JSON_Object   ::= "{" [ JSON_Pair { "," JSON_Pair } ] "}"
JSON_Array    ::= "[" [ JSON_Value { "," JSON_Value } ] "]"
JSON_Pair     ::= STRING ":" JSON_Value
JSON_Value    ::= STRING | NUMBER | BOOLEAN | NULL | JSON_Object | JSON_Array | Variable
Variable      ::= "${" IDENTIFIER [ "?=" Primitive ] "}" 
              | "@{" ( IDENTIFIER | "_" IDENTIFIER "_" ) "}"
Path          ::= "$" [ "body" ] { "." PathSegment }
              | "$header." IDENTIFIER
              | "$status"
PathSegment   ::= IDENTIFIER | "[" NUMBER "]"
Type          ::= "string" | "number" | "boolean" | "array" | "object" | "null" | "any" [ "?" ]
Condition     ::= Comparable ( "==" | "!=" | ">" | "<" | ">=" | "<=" ) Comparable
Comparable    ::= Path | Primitive
Primitive     ::= STRING | NUMBER | BOOLEAN | NULL
Comment       ::= "#" { CHAR } NEWLINE
Empty         ::= NEWLINE
URI           ::= STRING
IDENTIFIER    ::= LETTER { LETTER | DIGIT | "_" } | "_" { LETTER | DIGIT | "_" }
NUMBER        ::= DIGIT { DIGIT }
STRING        ::= '"' { CHAR } '"'
BOOLEAN       ::= "true" | "false"
NULL          ::= "null"
CHAR          ::= LETTER | DIGIT | SYMBOL | SPACE
LETTER        ::= "a"..."z" | "A"..."Z"
DIGIT         ::= "0"..."9"
SYMBOL        ::= "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "-" | "+" | "=" | "[" | "]" | "{" | "}" | "|" | ";" | ":" | "," | "." | "<" | ">" | "/" | "?" | "~" | "`"
NEWLINE       ::= "\n" | "\r\n"
EOF           ::= /* 文件结束 */
相关推荐
pop_xiaoli1 小时前
effective-Objective-C 第四章阅读笔记
笔记·ios·objective-c·cocoa·xcode
四谎真好看2 小时前
SSM学习笔记(SpringMVC篇 Day01)
笔记·学习·学习笔记·ssm
嵌入式×边缘AI:打怪升级日志2 小时前
ARM Cortex-M 单片机启动流程与向量表深度解析(保姆级复习笔记)
arm开发·笔记·单片机
cqbzcsq2 小时前
MC Forge1.20.1 mod开发学习笔记(个人向)
笔记·学习·mod·mc·forge
蒸蒸yyyyzwd3 小时前
cpp学习笔记
笔记·学习
浅念-3 小时前
C++ STL vector
java·开发语言·c++·经验分享·笔记·学习·算法
qyhua3 小时前
春节怀旧:翻出 20 年前的 VB6 书籍与老 CPU 记忆
笔记·其他
winfreedoms3 小时前
ROS2主题通讯——黑马程序员ROS2课程上课笔记(2)
笔记
was17214 小时前
你的私有知识库:自托管 Markdown 笔记方案 NoteDiscovery
笔记·云原生·自部署