Yup 跨端三方库仓颉适配全流程

本文记录 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 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:

  1. 当前仓颉版本使用 Value 表达动态输入,不提供 TypeScript InferType、泛型推断、声明合并和 JavaScript 原型动态扩展。
  2. JavaScript 的 undefined 对应 Value.Missing,显式 null 对应 Value.Null;业务代码需要通过 optional、defined 和 nullable 明确区分两者。
  3. testAsync 使用仓颉 Future<Bool>,同步校验遇到异步规则会抛出明确错误;未实现 JavaScript Promise 的完整链式组合和取消协议。
  4. JSON 功能依赖 json-c,构建时需要在目标平台提供对应头文件、库文件和运行时链接路径;当前示例配置中的 /opt/homebrew/lib 适用于本机 Apple Silicon Homebrew。
  5. 日期解析支持 RFC3339 和 YYYY-MM-DD,不复现 JavaScript 宽松日期格式、数字日期转换、溢出自动进位和本地时区推断。
  6. URL、正则和 Unicode 大小写行为使用仓颉标准库实现,复杂 IDN URL、完整 JavaScript RegExp 语义和所有 Unicode 大小写边界不承诺逐位一致。
  7. Value 的枚举成员按结构比较,因此对象 membership 与 JavaScript 对象身份比较不同;调用方应使用稳定的文本、数值或结构值。
  8. 当前版本没有实现浏览器 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 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
运维行者_1 小时前
网络监控工具怎么选?从设备监控到智能运维的能力清单
运维·服务器·开发语言·网络·人工智能·支持向量机·php
用户3126874877202 小时前
Java 对象内存布局:压缩指针、对齐填充与 JOL 实测
java
东华果汁哥2 小时前
【Spring AI 实战】从零开始调用大模型接口,构建 Java AI 应用
java·人工智能·spring
“AI国潮设计-小江”2 小时前
【AIGC实战】Python+SDXL打造“潮汕英歌舞IP”万能立牌插件,批量生成“发财元宝蛋糕”视觉营销方案(附源码)
开发语言·人工智能·python·prompt·aigc
Song_RsHt2 小时前
Redis 与微服务,什么场景下无可替代
java·redis·后端·学习·微服务
学长毕设3 小时前
基于SpringBoot的建筑材料管理系统-附源码
java·spring boot·后端
lhbweilai3 小时前
分析FreeRTOS内核对象API的源码实现(八)
c语言·开发语言·stm32·单片机
努力努力再努力wz3 小时前
【C++高阶系列】C++20 协程底层原理:函数为什么能暂停?从状态机到 Task,彻底串起挂起、恢复与协程调度
开发语言·c++·c++20·1024程序员节
摸爬滚打的小李3 小时前
关于 Arthas
java·后端