📚 目录

  1. 项目概述
  2. 环境准备
  3. 项目结构分析
  4. 核心功能实现
  5. 测试编写
  6. 编译与构建
  7. 发布与使用
  8. 最佳实践
  9. 常见问题

📖 项目概述

🤔 什么是 validator4cj?

validator4cj 是一个基于仓颉编程语言开发的字符串验证和清理库,完整迁移自 JavaScript 的 validator.js 库。该项目展示了如何:

  • 将成熟的 JavaScript 库迁移到仓颉语言
  • 设计类型安全的 API
  • 编写完整的单元测试
  • 构建可发布的仓颉三方库

✨ 项目特点

  • 📊 100+ 个验证函数:涵盖邮箱、URL、IP、日期、数字、身份证等多种格式验证
  • 🔒 类型安全:充分利用仓颉语言的强类型系统
  • 📦 模块化设计:每个验证函数独立文件,便于维护
  • ✅ 完整测试:32 个测试文件,按 HLT/LLT 分类
  • 🌍 标准兼容:遵循 ISO、RFC 等国际标准

🛠️ 环境准备

1️⃣ 安装仓颉语言工具链

确保已安装仓颉语言编译器和包管理器:

# 检查 cjc 版本
cjc --version

# 检查 cjpm 版本
cjpm --version

2️⃣ 克隆项目

git clone https://gitcode.com/cj-awaresome/validator4cj.git
cd validator4cj

3️⃣ 初始化依赖

cjpm update

📁 项目结构分析

📂 目录结构

validator4cj/
├── cjpm.toml              # 包配置文件
├── README.md              # 项目说明
├── CHANGELOG.md           # 版本变更记录
├── LICENSE                # 开源协议
├── src/                   # 源代码目录
│   ├── core.cj            # 包声明
│   ├── main.cj            # 入口文件
│   ├── util_string.cj     # 工具函数
│   ├── is*.cj             # 验证函数(80+ 个)
│   ├── to*.cj             # 转换函数(4 个)
│   ├── trim.cj             # 字符串修剪
│   ├── escape.cj          # HTML 转义
│   ├── contains.cj        # 字符串包含
│   ├── equals.cj          # 字符串相等
├── test/                  # 测试目录(按 HLT/LLT 分类)
│   ├── HLT/               # 高级测试
│   └── LLT/               # 低级测试
└── doc/                   # 文档目录
    ├── design.md          # 设计文档
    ├── feature_api.md     # API 文档
    └── TUTORIAL.md        # 本教程

⚙️ 包配置 (cjpm.toml)

[package]
  cjc-version = "1.0.3"
  name = "validator4cj"
  description = "nothing here"
  version = "1.0.0"
  target-dir = ""
  src-dir = ""
  output-type = "static"
  compile-option = ""
  override-compile-option = ""
  link-option = ""
  package-configuration = {}

🔑 关键配置说明:

  • output-type = "static":编译为静态库,允许其他包导入
  • name = "validator4cj":包名,用于导入
  • version = "1.0.0":版本号

💻 核心功能实现

1️⃣ 包声明 (core.cj)

所有源文件必须以包声明开始:

package validator4cj

2️⃣ 简单验证函数实现

equals 函数为例:

package validator4cj

func equals(str: String, comparison: String): Bool {
    return str == comparison
}

💡 设计要点:

  • 函数名清晰表达功能
  • 参数类型明确
  • 返回值类型明确(Bool

3️⃣ 带选项的验证函数实现

isEmail 函数为例,展示如何设计带选项的验证函数:

📝 步骤 1:定义选项类
package validator4cj

class EmailOptions {
    let allowDisplayName: Bool
    let requireDisplayName: Bool
    let allowUtf8LocalPart: Bool
    let requireTld: Bool
    let allowUnderscores: Bool
    let blacklistedChars: String
    let ignoreMaxLength: Bool
    let domainSpecificValidation: Bool
    let allowIpDomain: Bool
    let hostBlacklist: Array<String>
    let hostWhitelist: Array<String>

    public init(
        allowDisplayName: Bool, requireDisplayName: Bool, 
        allowUtf8LocalPart: Bool, requireTld: Bool, 
        allowUnderscores: Bool, blacklistedChars: String,
        ignoreMaxLength: Bool, domainSpecificValidation: Bool, 
        allowIpDomain: Bool, hostBlacklist: Array<String>, 
        hostWhitelist: Array<String>
    ) {
        this.allowDisplayName = allowDisplayName
        this.requireDisplayName = requireDisplayName
        this.allowUtf8LocalPart = allowUtf8LocalPart
        this.requireTld = requireTld
        this.allowUnderscores = allowUnderscores
        this.blacklistedChars = blacklistedChars
        this.ignoreMaxLength = ignoreMaxLength
        this.domainSpecificValidation = domainSpecificValidation
        this.allowIpDomain = allowIpDomain
        this.hostBlacklist = hostBlacklist
        this.hostWhitelist = hostWhitelist
    }
}

💡 设计要点:

  • 使用 class 封装配置选项
  • 所有字段使用 let 声明(不可变)
  • 提供 public init 构造函数
📝 步骤 2:实现验证函数
package validator4cj

func isEmail(str: String, options: EmailOptions): Bool {
    // 1. 基本格式检查
    if (str == "") { return false }
    
    // 2. 检查是否包含 @ 符号
    if (!contains(str, "@")) { return false }
    
    // 3. 分割本地部分和域名部分
    let parts = splitByAt(str)
    if (parts.size != 2) { return false }
    
    let localPart = parts[0]
    let domain = parts[1]
    
    // 4. 验证本地部分
    if (!isValidLocalPart(localPart, options)) {
        return false
    }
    
    // 5. 验证域名部分
    if (!isValidDomain(domain, options)) {
        return false
    }
    
    // 6. 检查黑名单和白名单
    if (options.hostBlacklist.size > 0) {
        if (arrayIncludes(options.hostBlacklist, toLower(domain))) {
            return false
        }
    }
    
    return true
}

4️⃣ 字符串处理技巧

🔤 使用 Rune 处理 Unicode

仓颉语言使用 Rune 类型处理 Unicode 字符:

// 字符串转 Rune 数组
let runes = s.toRuneArray()

// 遍历字符串中的字符
for (r in s.runes()) {
    if (r >= r'0' && r <= r'9') {
        // 处理数字字符
    }
}

// 获取字符数量
let count = s.runes() |> count
✂️ 字符串切片操作
let s = "Hello World"

// 从索引 2 到末尾
let sub1 = s[2..]  // "llo World"

// 从开头到索引 5(不包含 5)
let sub2 = s[..5]  // "Hello"

// 从索引 2 到索引 5(不包含 5)
let sub3 = s[2..5]  // "llo"

// 从索引 2 到索引 5(包含 5)
let sub4 = s[2..=5]  // "llo "

5️⃣ 类型转换函数实现

toInt 为例:

package validator4cj
import std.convert.*

func toInt(s: String): Int64 {
    // 处理空字符串
    if (s == "") { return 0 }
    
    // 处理前导 + 号
    var input = s
    if (s[0..1] == "+") {
        input = s[1..]
        if (input == "") { return 0 }
    }
    
    // 验证格式
    var hasDigit = false
    var i: Int64 = 0
    if (input != "" && input[0..1] == "-") { i = 1 }
    
    while (i < input.toRuneArray().size) {
        let r = input.toRuneArray()[i]
        if (r >= r'0' && r <= r'9') {
            hasDigit = true
        } else {
            return 0  // 包含非数字字符
        }
        i += 1
    }
    
    if (!hasDigit) { return 0 }
    
    // 尝试解析
    return Int64.parse(input)
}

💡 设计要点:

  • 失败时返回安全的默认值(0
  • 处理边界情况(空字符串、前导符号等)
  • 先验证格式再解析,避免异常

6️⃣ 使用 Option 类型

仓颉语言使用 Option<T> 处理可选值:

package validator4cj

class IsIntOptions {
    let allowLeadingZeroes: Bool
    let min: Option<Int64>
    let max: Option<Int64>
    let lt: Option<Int64>
    let gt: Option<Int64>
    
    public init(
        allowLeadingZeroes: Bool, 
        min: Option<Int64>, 
        max: Option<Int64>, 
        lt: Option<Int64>, 
        gt: Option<Int64>
    ) {
        this.allowLeadingZeroes = allowLeadingZeroes
        this.min = min
        this.max = max
        this.lt = lt
        this.gt = gt
    }
}

func isInt(s: String, options: IsIntOptions): Bool {
    // ... 验证逻辑 ...
    
    // 使用 Option 类型
    if (options.min.isSome()) {
        let minVal = options.min.getOrThrow()
        if (value < minVal) { return false }
    }
    
    if (options.max.isSome()) {
        let maxVal = options.max.getOrThrow()
        if (value > maxVal) { return false }
    }
    
    return true
}

🧪 测试编写

🔧 测试框架

使用仓颉语言的 std.unittest 框架:

package validator4cj.test
import std.unittest.*
import std.unittest.testmacro.*
import validator4cj.*

📊 测试分类

🔝 HLT (High Level Test) - 高级测试

测试复杂验证函数,位于 test/HLT/src/test/HLT_*.cj

package validator4cj.test
import std.unittest.*
import std.unittest.testmacro.*
import validator4cj.*

@Test
class TestIsEmail {
    // 辅助函数:创建默认选项
    func defaultEmailOptions(): EmailOptions {
        let emptyArray: Array<String> = []
        return EmailOptions(
            false,      // allowDisplayName
            false,      // requireDisplayName
            false,      // allowUtf8LocalPart
            true,       // requireTld
            false,      // allowUnderscores
            "",         // blacklistedChars
            true,       // ignoreMaxLength
            false,      // domainSpecificValidation
            false,      // allowIpDomain
            emptyArray, // hostBlacklist
            emptyArray  // hostWhitelist
        )
    }
    
    @TestCase
    func testValidEmails() {
        let opts = defaultEmailOptions()
        @Assert(isEmail("foo@bar.com", opts) == true)
        @Assert(isEmail("test@gmail.com", opts) == true)
        @Assert(isEmail("user+tag@example.com", opts) == true)
    }
    
    @TestCase
    func testInvalidEmails() {
        let opts = defaultEmailOptions()
        @Assert(isEmail("invalid.com", opts) == false)
        @Assert(isEmail("@invalid.com", opts) == false)
        @Assert(isEmail("user@", opts) == false)
    }
    
    @TestCase
    func testEmailWithOptions() {
        let opts = defaultEmailOptions()
        // 测试特定选项
        @Assert(isEmail("foo@bar.com", opts) == true)
    }
}
🔻 LLT (Low Level Test) - 低级测试

测试简单函数,位于 test/LLT/src/test/LLT_*.cj

package validator4cj.test
import std.unittest.*
import std.unittest.testmacro.*
import validator4cj.*

@Test
class TestTrim {
    @TestCase
    func testTrimWhitespace() {
        // 使用 None 表示默认空白字符
        @Assert(trim("  \r\n\tfoo  \r\n\t   ", None) == "foo")
        @Assert(trim("      \r", None) == "")
    }
    
    @TestCase
    func testTrimCustomChars() {
        @Assert(trim("010100201000", Some("01")) == "2")
        @Assert(ltrim("010100201000", Some("01")) == "201000")
        @Assert(rtrim("010100201000", Some("01")) == "0101002")
    }
}

📋 测试注解说明

  • @Test:标识测试类
  • @TestCase:标识测试用例方法
  • @Assert:断言测试结果

▶️ 运行测试

cd validator4cj
cjpm test

imgimg

🔨 编译与构建

🏗️ 编译项目

cjpm build

🧹 清理构建产物

cjpm clean

⚡ 增量编译

cjpm build -i

🐛 调试模式编译

cjpm build -g

⚠️ 查看编译警告

cjpm build 2>&1 | grep -i warning

📦 发布与使用

1️⃣ 作为库使用

📥 在其他项目中引用

cjpm.toml 中添加依赖:

[dependencies]
validator4cj = { path = "../validator4cj" }

或在项目中使用:

import validator4cj.*

main() {
    let emptyArray: Array<String> = []
    let opts = EmailOptions(
        false, false, false, true, false, "", true, false, false,
        emptyArray, emptyArray
    )
    
    if (isEmail("user@example.com", opts)) {
        println("Valid email")
    }
}

2️⃣ 导出函数

所有 package validator4cj 下的公共函数都可以被外部导入。确保:

  • 函数使用 public 修饰符(如果需要)
  • 类使用 public 修饰符
  • 选项类的构造函数使用 public init

3️⃣ 版本管理

CHANGELOG.md 中记录版本变更:

# [1.0.0] - 2025-11-05

⭐ 最佳实践

1️⃣ 代码组织

  • 一个文件一个函数:每个验证函数独立文件,便于维护
  • 工具函数集中:公共工具函数放在 util_string.cj
  • 测试分类清晰:按 HLT/LLT 分类测试

2️⃣ 命名规范

  • 函数名:使用动词开头,如 isEmail, toInt, trim
  • 类名:使用名词,首字母大写,如 EmailOptions, UrlOptions
  • 变量名:使用小写字母和下划线,如 local_part, domain_name

3️⃣ 错误处理

  • 验证函数:返回 Bool,不抛出异常
  • 转换函数:失败时返回安全的默认值
  • 边界检查:始终检查空字符串、空数组等边界情况

4️⃣ 性能优化

  • 避免重复计算:缓存字符串长度等计算结果
  • 提前返回:发现错误立即返回,避免不必要的计算
  • 使用高效算法:选择合适的字符串匹配算法

5️⃣ 文档编写

  • 函数注释:说明参数、返回值、异常情况
  • 示例代码:提供清晰的使用示例
  • API 文档:维护完整的 API 文档

6️⃣ 测试覆盖

  • 边界测试:测试空字符串、空值等边界情况
  • 正常测试:测试正常输入
  • 异常测试:测试无效输入
  • 选项测试:测试不同选项组合

❓ 常见问题

❓ Q1: 如何处理字符串中的 Unicode 字符?

✅ A: 使用 Rune 类型和 runes() 方法:

// 遍历字符串中的字符
for (r in s.runes()) {
    // 处理每个字符
}

// 转换为 Rune 数组
let runes = s.toRuneArray()

❓ Q2: 如何实现可选参数?

✅ A: 使用 Option<T> 类型:

func myFunction(str: String, optional: Option<String>): Bool {
    if (optional.isSome()) {
        let value = optional.getOrThrow()
        // 使用 value
    }
    // ...
}

❓ Q3: 如何避免编译错误?

✅ A:

  • 确保所有函数都有明确的返回类型
  • 检查 Option 类型的使用(使用 isSome()getOrThrow()
  • 处理所有可能的代码路径

❓ Q4: 测试无法导入主包怎么办?

✅ A: 确保 cjpm.tomloutput-type = "static"

[package]
  output-type = "static"

❓ Q5: 如何调试编译错误?

✅ A:

  • 使用 cjpm build -g 编译调试版本
  • 查看编译输出的错误信息
  • 检查类型匹配和函数签名

❓ Q6: 如何优化字符串操作性能?

✅ A:

  • 使用字符串切片而不是字符串拼接
  • 缓存字符串长度
  • 使用 toRuneArray() 进行多次访问

🎯 实战演练

📝 练习 1: 实现一个简单的验证函数

实现 isEven 函数,验证字符串是否为偶数:

package validator4cj
import std.convert.*

func isEven(str: String): Bool {
    let value = toInt(str)
    return value % 2 == 0
}

📝 练习 2: 实现带选项的验证函数

实现 isLength 函数,验证字符串长度:

package validator4cj

class LengthOptions {
    let min: Option<Int64>
    let max: Option<Int64>
    
    public init(min: Option<Int64>, max: Option<Int64>) {
        this.min = min
        this.max = max
    }
}

func isLength(str: String, options: LengthOptions): Bool {
    let len = str.runes() |> count
    
    if (options.min.isSome()) {
        if (len < options.min.getOrThrow()) {
            return false
        }
    }
    
    if (options.max.isSome()) {
        if (len > options.max.getOrThrow()) {
            return false
        }
    }
    
    return true
}

📝 练习 3: 编写测试用例

isLength 函数编写测试:

package validator4cj.test
import std.unittest.*
import std.unittest.testmacro.*
import validator4cj.*

@Test
class TestIsLength {
    @TestCase
    func testValidLength() {
        let opts = LengthOptions(Some(5), Some(10))
        @Assert(isLength("Hello", opts) == true)
        @Assert(isLength("Hello World", opts) == true)
    }
    
    @TestCase
    func testInvalidLength() {
        let opts = LengthOptions(Some(5), Some(10))
        @Assert(isLength("Hi", opts) == false)
        @Assert(isLength("Hello World!", opts) == false)
    }
}

📝 总结

通过 validator4cj 项目,我们学习了:

  1. 项目结构设计:如何组织仓颉三方库的代码结构
  2. 类型安全编程:利用仓颉语言的强类型系统
  3. 选项模式:使用类封装配置选项
  4. 测试编写:使用 std.unittest 编写完整的测试套件
  5. 字符串处理:使用 Rune 正确处理 Unicode
  6. 错误处理:设计安全的错误处理策略
  7. 最佳实践:遵循仓颉语言的编程规范

📚 参考资源


祝你编码愉快! 🚀

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐