本文记录 Yup 1.7.1 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在本地终端中通过真实 schema 转换、对象解析、嵌套校验、错误聚合、条件引用和异步测试验证 Yup 的仓颉实现。
项目地址: AtomGit/nutpi_cj/yup
上游仓库: jquense/yup
开发工具: 华为云码道
一、背景
1.1 为什么适配 Yup
Yup 是 JavaScript 生态中常用的 schema 构建和运行时校验库,可以把输入转换为目标类型,也可以对字符串、数字、日期、数组和对象执行组合校验。原库通过链式 API 构造 schema,支持默认值、条件校验、引用、异步 test、自定义错误和对象字段变换。
JavaScript 版本依赖动态值、对象字面量和 Promise,仓颉则强调静态类型、显式错误和编译期检查。直接逐行翻译 JavaScript,无法清楚表达 undefined、null、数组、对象和日期在转换过程中的区别。因此本适配先设计统一的 Value 值模型,再用仓颉 Schema 类和显式 ValidationError 重建 Yup 的主要调用习惯。
本项目保留 mixed、string、number、boolean、date、array、tuple、object、lazy 等熟悉的工厂名称,同时把输入值、转换值、校验错误和异步结果都落实为仓颉类型。
1.2 跨端三方库的适配边界
当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm build 和静态库输出 |
| 值模型 | Value 枚举表示 Missing、Null、Text、Number、Boolean、DateValue、Items 和 Fields |
| Schema 工厂 | mixed、string、number、boolean、date、array、tuple、object、lazy |
| 转换 API | cast、transform、defaultValue、defaultFactory、json 和对象字段变换 |
| 校验 API | validate、validateSync、validateAsync、isValid、validateAt 和标准结果适配 |
| 错误模型 | ValidationError、ValidationIssue、path、typeName、message、value 和 inner |
| JSON 能力 | 通过 json-c 解析 JSON 字符串和 UTF-8 内容 |
| 交付形态 | 仓颉静态库 libyup_cangjie.a 和接口文件 yup_cangjie.cjo |
| 验证方式 | test/main.cj 回归测试和 example/example.cj 终端示例 |
这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。当前版本聚焦 Yup 的 schema、解析和校验语义;JSON 解析通过本机 json-c 完成。只要目标平台提供兼容版本的仓颉 SDK、标准库和可用的 json-c,就可以按同一套源码和 cjpm 配置构建并消费这个库。
1.3 适配后的能力
当前导出的仓颉 API 由值模型、schema 工厂和 schema 方法组成,覆盖以下能力:
- 值构造和展示:
text、num、boolValue、dateValue、list、obj、get、getIn、asText、asNumber、deepClone、valuesEqual和printValue; - schema 工厂:
mixed、string、number、boolean、bool、date、array、tuple、object和lazy; - 解析和校验:
cast、validate、validateSync、validateAsync、isType、isValid、isValidSync、isValidAsync、standardValidate和异步结果适配; - 字符串、数值、日期、数组、元组和对象的转换与规则链;
when、ref、test、testAsync、addMethod、setLocale、reach和嵌套路径校验。
示例程序实际调用这些公开能力,并打印成功结果以及每条失败规则的路径和具体值,而不是只打印断言通过。
二、实现路线图
text
第 1 阶段:盘点 API ── 对照 Yup schema 生命周期,确定仓颉值模型和方法边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录、依赖和静态库输出
第 3 阶段:类型映射 ── 用 Value、Schema、Option 和显式异常替换动态值与 Promise
第 4 阶段:实现校验 ── 完成转换、默认值、嵌套对象、数组、条件、引用和错误聚合
第 5 阶段:测试与核对 ── 编译回归入口,核对真实返回值、错误路径和输入所有权
第 6 阶段:终端演示 ── 运行 example,输出全部功能结果并保存效果图
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 .cjpm-target/yup_cangjie,验证阶段得到 20 项仓颉回归测试全部通过,最终示例则输出 181 项来自真实库调用的结果。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
Yup 的 JavaScript API 接受任意输入值,schema 既可以执行转换,也可以执行校验。对象 schema 会递归处理字段,数组 schema 会递归处理元素;when 和 ref 还会读取同级字段或上下文值。仓颉没有与 JavaScript undefined 和动态对象完全等价的公共类型,因此本适配先把这些状态显式建模,再把规则执行过程固定为可检查的函数链。
Value.Missing 表示缺失值,Value.Null 表示显式空值,Value.Fields 和 Value.Items 分别表示对象和数组。schema 使用 allowMissing 与 allowNull 区分 optional、defined、nullable 和 required。每个规则失败时生成 ValidationIssue,最终由 ValidationError 聚合。
1.2 固定仓颉项目结构
以下目录树按照当前 Yup 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 Yup JavaScript、TypeScript 源码、测试和文档保持原位置:
text
yup/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ ├── value.cj
│ ├── json.cj
│ ├── schema.cj
│ ├── types.cj
│ └── util/paths.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
值模型位于 src/value.cj,JSON 解析位于 src/json.cj,schema 生命周期位于 src/schema.cj,类型规则位于 src/types.cj,路径分割工具位于 src/util/paths.cj。测试入口位于 test/main.cj,示例工程位于 example/。示例使用 src-dir = ".",但 target-dir 放在 example/ 外部,因此构建输出不会被再次当作源码扫描。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型,并把构建目录放到项目根目录之外:
toml
[package]
name = "yup_cangjie"
version = "1.7.1"
cjc-version = "1.1.3"
description = "Yup-style parsing and validation for Cangjie"
output-type = "static"
src-dir = "src"
target-dir = "../.cjpm-target/yup_cangjie"
link-option = "-L/opt/homebrew/lib -ljson-c"
示例工程使用本地路径依赖,并把构建目录放在 example/ 外部:
toml
[package]
name = "yup_example"
version = "1.0.0"
cjc-version = "1.1.3"
description = "Executable Yup Cangjie feature demonstrations"
output-type = "executable"
src-dir = "."
target-dir = "../../.cjpm-target/yup_example"
link-option = "-L/opt/homebrew/lib -ljson-c"
[dependencies]
yup_cangjie = { path = ".." }
这样运行 cjpm run 时,不会把 example/ 下的构建目录当作源码扫描目录,也不会出现 there is no '.cj' file ... target 警告。构建成功后,根包最重要的两个产物是:
text
../.cjpm-target/yup_cangjie/release/yup_cangjie/libyup_cangjie.a
../.cjpm-target/yup_cangjie/release/yup_cangjie/yup_cangjie.cjo
其中 .a 用于链接,.cjo 提供编译器需要的接口信息。构建输出只放在仓库外部,不提交到仓库。
第 3 阶段:把 JavaScript 动态配置映射为仓颉类型
3.1 用 Value 表达输入、转换值和错误值
JavaScript Yup 可以直接接收字符串、数字、布尔值、日期、数组和对象,仓颉版本通过 Value 统一承载这些数据:
cangjie
let input = obj([
("name", text(" Alice ")),
("age", text("24")),
("tags", list([text("cangjie"), text("yup")]))
])
let user = object([
("name", string().trim().required()),
("age", number().positive().integer()),
("tags", array(string()))
])
let output = user.validate(input)
println(printValue(output))
cast 只负责转换,validate 会在转换后执行规则并抛出 ValidationError。示例同时打印输入和转换结果,因此可以看到字符串 "24" 实际变为数值 24,而不是只看到断言成功。
3.2 用 Schema 和 Option 表达规则状态
schema 的可选状态通过 Option 和显式字段表达:optional() 接受 Missing,nullable() 接受 Null,defined() 拒绝 Missing,required() 同时拒绝缺失、空值和空字符串。默认值使用 defaultValue 或 defaultFactory,对象 schema 还会递归生成字段默认值:
cangjie
let schema = object([
("name", string().required()),
("age", number().defaultValue(num(18)))
])
let result = schema.validate(obj([("name", text("Alice"))]))
// {"age": 18, "name": "Alice"}
schema 构建默认返回副本,只有 withMutation 回调内才允许显式修改当前 schema。默认对象也会深复制,避免一次调用修改下一次调用的默认结果。
3.3 用 ValidationError 和 Future 表示失败
JavaScript Yup 会返回 Promise 或抛出 ValidationError。仓颉版本保留同步入口,同时提供 Future 异步入口:
cangjie
let schema = string().email()
try {
schema.validate(text("invalid"))
} catch (error: ValidationError) {
println(error.path)
for (issue in error.inner) { println(issue.toString()) }
}
let asyncSchema = string().testAsync("available", "username already taken",
{ value, _ => spawn { asText(value) != "admin" } })
let value = asyncSchema.validateAsync(text("alice")).get()
ValidationError 暴露 value、path、errors 和 inner;ValidationIssue 进一步保存规则类型、路径、信息和失败值。同步验证遇到异步规则会明确报错,避免把未完成的异步结果当作校验成功。
第 4 阶段:实现源码
4.1 文件 src/value.cj 和 src/json.cj
src/value.cj 实现 Value 枚举、深复制、结构比较、路径访问和稳定的展示格式。printValue 负责把实际返回结果打印到终端,getIn 支持 users[0].age 和 ["a.b"] 这样的嵌套路径。
src/json.cj 通过 json-c 的 tokener API 解析 JSON,并把原生 JSON 树复制为仓颉 Value。解析结束后释放原生对象,库对调用方只暴露仓颉值,不把 json-c 的指针生命周期泄漏给业务代码。数组、对象、布尔、数字、字符串、null、UTF-8 和尾部内容都由测试与示例覆盖。
4.2 文件 src/schema.cj
Schema 按 Yup 的调用生命周期组织:先解析条件和 lazy 分支,再处理默认值、强制转换和 transform,接着递归处理对象字段、数组元素和 tuple 成员,最后运行规则并聚合错误。ValidateOptions 支持 strict、abortEarly、recursive、stripUnknown 和 context。
对象字段会按引用依赖排序。when 读取同级字段或上下文,ref 支持同级、父值、自身和 $context 路径;检测到循环依赖时明确抛出错误。validateAt、validateSyncAt 和 validateAsyncAt 通过同一套路径解析逻辑定位嵌套 schema。
4.3 文件 src/types.cj
类型扩展集中在 src/types.cj:字符串提供 min、max、length、matches、email、url、uuid、datetime、trim、lowercase、uppercase 和 ensure;数字提供边界、正负数、整数和四种舍入模式;数组提供 of、json、长度边界、ensure 和 compact;对象提供 shape、pick、omit、partial、deepPartial、from、exact、noUnknown、stripUnknown 和大小写转换。
枚举规则保存已允许和已拒绝的值及引用。重复链式调用会合并成员,冲突项会从另一组移除,concat 也会合并两边的 membership 配置。这样 oneOf(["a"]).oneOf(["b"]) 和 notOneOf(["a"]) 的行为可以在实际返回值中观察到。
日期值通过仓颉 std.time.DateTime 解析 RFC3339 和 YYYY-MM-DD,并保存为 Unix 毫秒。字符串 datetime 只校验格式、时区和小数精度,不把字符串自动变成日期。JSON 能力依赖 json-c;仓颉源码不实现一套简化的 JSON 解析器,以避免转义、UTF-8 和数字边界出现不一致。当前仓颉实现聚焦纯 schema 和数据处理,不绑定网络、UI 或某个操作系统;只要目标端拥有仓颉标准库和兼容的 json-c,根包和示例就可以用同一套 cjpm 配置构建。
第 5 阶段:建立自动化测试
test/main.cj 是一个直接链接仓颉静态库的测试入口,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| 基础转换 | 字符串、数字、布尔和日期的实际 cast 结果 |
| Presence 和默认值 | optional、defined、required、nullable、notRequired、默认值隔离 |
| Schema 复用 | clone、concat、withMutation、输入不变和转换执行次数 |
| 字符串与数字 | email、url、uuid、datetime、长度、边界、整数和舍入 |
| 数组与对象 | of、json、compact、tuple、shape、pick、omit、partial 和 deepPartial |
| 嵌套错误 | 数组索引、对象字段、引用路径和 abortEarly / recursive |
| 条件与引用 | when、context、self reference、lazy 和循环依赖 |
| 扩展能力 | test、testAsync、exclusive、addMethod、setLocale 和标准结果 |
测试不是只检查一个布尔标记,而是比较实际 Value、错误数量、错误路径、规则类型、错误文本和输入对象的所有权。20 项测试全部通过时,测试运行器输出 PASSED: 20。
第 6 阶段:核对 Yup 适配 API 和行为
除了单项断言,还使用 example/example.cj 对公开工厂、规则链和结果入口逐项调用。当前示例包含字符串、数字、布尔、日期、mixed、presence、默认值、membership、对象、数组、tuple、条件、引用、lazy、异步、自定义错误、本地化、JSON 和标准 schema 结果,共输出 181 项真实结果。
这个检查说明当前示例覆盖了仓颉实现公开的主要能力,并且每次调用都把真实返回值或真实错误打印出来;它不代表 JavaScript Yup 的 TypeScript 类型推断、所有动态对象语义和全部生态插件已经被仓颉一比一复刻。
四、完整验证步骤
以下命令均以仓库根目录 yup/ 为当前目录。先确认本机已经安装仓颉 SDK、CJPM 和 json-c:
bash
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
pkg-config --libs json-c
然后构建静态库:
bash
cjpm build
编译并运行 20 项回归测试:
bash
yup_build_dir=../.cjpm-target/yup_cangjie/release/yup_cangjie
cjc --test test/main.cj \
--import-path "$yup_build_dir" \
-L "$yup_build_dir" \
-lyup_cangjie \
-lyup_cangjie.util \
-L /opt/homebrew/lib -ljson-c \
-o ../.cjpm-target/yup_cangjie/yup_tests
DYLD_LIBRARY_PATH="$CANGJIE_HOME/runtime/lib/darwin_aarch64_cjnative" \
../.cjpm-target/yup_cangjie/yup_tests --no-color
测试运行器会输出:
text
Summary: TOTAL: 20
PASSED: 20, SKIPPED: 0, ERROR: 0
FAILED: 0
再构建并运行独立的终端示例工程:
bash
cd example
cjpm build
cjpm run
示例直接调用库并输出真实结果,而不是打印预先写死的文本:
text
[175] async test rejection
parsed: "admin"
error: $: available: username already taken (value="admin")
[177] standardValidate error result
result: false
issue: age: moreThan: age must satisfy moreThan 0 (value=-1)
[178] standardValidate success result
result: {"age": 24}
[181] parseJSON UTF-8 and escapes
result: {"items": [1, true, null], "message": "你好\u0000世界"}
Displayed 181 real library results.
cjpm run finished
示例中的正常输入会显示转换后的值,异常输入会显示 parsed 和每条错误;如果库没有按预期抛出或返回,程序会以非零状态退出。构建目录位于仓库外部,运行 cjpm run 不会出现 there is no '.cj' file ... target 警告。
五、运行效果
下面的效果图来自 example/example.cj 的实际终端运行,包含静态库构建后的 Yup Cangjie schema 调用结果、具体错误路径和最终完成标记。

这张图可以作为文章或项目说明中的验收证据:终端展示了异步规则拒绝、同步入口拒绝异步测试、标准 schema 失败与成功结果、本地化错误和 JSON UTF-8 解析,并以 Displayed 181 real library results. 作为完成标记。图中内容来自真实的仓颉可执行程序,不是模拟页面或只显示断言通过。
六、关键决策说明
6.1 为什么使用 Value 而不是无类型 Any
Yup 的输入可以是多种运行时类型,但每种类型在转换和校验时都有明确行为。若把全部输入退化成无类型容器,虽然表面上接近 JavaScript,却会失去 Missing、Null、日期、数组和对象之间的边界。因此项目使用 Value 枚举,让 schema 在编译期拥有统一入口,在运行时仍能保留实际类型。
6.2 为什么保留 Yup 的类型和方法名称
保留 string、number、object、validate、cast、oneOf、when、ref 和 ValidationError 等名称可以降低迁移成本。熟悉 Yup 的开发者只需要适应仓颉的 Value 构造器、命名参数和异常捕获,不必重新记忆一套完全不同的 schema API。对暂时无法表达的 JavaScript 动态类型和 TypeScript 推断,文档明确列出差异,而不是伪造完全兼容。
6.3 为什么把测试和示例分开
测试程序负责断言实际值、错误细节和输入所有权,适合自动化执行;示例工程负责展示每个功能的输入、转换结果和失败原因,适合在终端中观察和截图。两者都使用同一个 yup_cangjie 静态库,因此效果图不是模拟页面,而是库的实际运行输出。
七、兼容边界和遗留问题
JavaScript Yup 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:
- 当前仓颉版本使用
Value表达动态输入,不提供 TypeScriptInferType、泛型推断、声明合并和 JavaScript 原型动态扩展。 - JavaScript 的
undefined对应Value.Missing,显式null对应Value.Null;业务代码需要通过optional、defined和nullable明确区分两者。 testAsync使用仓颉Future<Bool>,同步校验遇到异步规则会抛出明确错误;未实现 JavaScript Promise 的完整链式组合和取消协议。- JSON 功能依赖 json-c,构建时需要在目标平台提供对应头文件、库文件和运行时链接路径;当前示例配置中的
/opt/homebrew/lib适用于本机 Apple Silicon Homebrew。 - 日期解析支持 RFC3339 和
YYYY-MM-DD,不复现 JavaScript 宽松日期格式、数字日期转换、溢出自动进位和本地时区推断。 - URL、正则和 Unicode 大小写行为使用仓颉标准库实现,复杂 IDN URL、完整 JavaScript RegExp 语义和所有 Unicode 大小写边界不承诺逐位一致。
Value的枚举成员按结构比较,因此对象 membership 与 JavaScript 对象身份比较不同;调用方应使用稳定的文本、数值或结构值。- 当前版本没有实现浏览器 UI、网络请求、文件流、二进制 Blob、AbortSignal 和 Yup 生态插件;这些能力属于宿主应用或后续适配范围。
这些不是构建错误。只要 cjpm build 成功、20 项回归测试通过、示例输出 181 项真实结果,就说明当前版本已经完成本地编译和 schema 行为验收。
八、接入方式
在另一个仓颉项目中,可以将本仓库作为本地依赖或发布包使用。最直接的本地验证方式是先在本仓库根目录生成 .cjpm-target/yup_cangjie,再把相同的 --import-path 和 -L 参数传给调用方的 cjc 命令。调用代码只需要导入:
cangjie
import yup_cangjie.*
main(): Int64 {
let schema = object([
("name", string().trim().required()),
("age", number().positive().integer())
])
let input = obj([
("name", text(" Alice ")),
("age", text("24"))
])
try {
println(printValue(schema.validate(input)))
} catch (error: ValidationError) {
println(error.message)
return 1
}
return 0
}
如果使用 cjpm 管理依赖,则将 yup_cangjie = { path = ".." } 或发布后的仓颉包加入项目依赖配置,再由业务工程统一构建。跨端项目可以复用相同的 schema、转换、错误处理和测试思路;具体的 JSON 链接路径、线程、文件系统和 UI 适配由宿主平台负责。
九、总结
本次适配完成了 Yup schema 模型到仓颉静态类型模型的迁移:输入值使用 Value 枚举,schema 使用统一的 Schema 类,字符串、数字、布尔、日期、数组、元组和对象规则通过扩展方法实现,默认值、条件、引用、自定义 test 和异步 test 通过显式 API 交付,错误通过 ValidationError 和 ValidationIssue 结构化返回。
验证链路包括 cjpm build、20 项回归测试、181 项终端示例结果、对象和数组转换、嵌套错误路径、JSON UTF-8 解析、条件引用、异步校验和本地化消息。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库返回的转换值和具体错误,而不是只显示断言通过。对于无法一比一映射 JavaScript Yup 的 TypeScript 推断、动态原型和部分运行时边界,项目通过静态签名和文档边界保持行为可解释,方便后续继续扩展。
适配过程 FAQ
以下问题来自仓颉工程创建、
cjpm依赖、与 HarmonyOS 设备联调的实际流程。
Q1:cjpm 依赖解析失败或拉取超时?
- 检查
cjpm.toml中的依赖坐标与版本号; - 配置可用的仓颉包仓库镜像或代理;
- 删除
target、cjpm.lock后重新执行cjpm update/cjpm build。
Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?
- 设置
CANGJIE_HOME指向仓颉 SDK 根目录; - 确认
cjc --version、cjpm --version与工程要求的版本一致; - 清理构建缓存后重新生成。
Q3:与 HarmonyOS 设备联调时安装失败?
- 用
hdc list targets确认设备在线; - 确认 HAP 已签名,
bundleName与签名 profile 一致; - 设备需加入调试 profile 的
deviceIds。
Q4:动态库或 FFI 符号找不到?
- 检查导出符号名称、ABI 与打包路径;
- 确认
.so已随 HAP 一起打包到libs/<abi>/; - 用
llvm-readelf -d检查依赖库是否齐全。
Q5:测试用例在设备上失败,但本机通过?
- 对比设备与主机的架构、SDK 版本和环境变量;
- 用
hdc hilog抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。