From e83bcaf68c9c7d5754fd0a1f8842c31993c6954b Mon Sep 17 00:00:00 2001 From: xRain Date: Mon, 17 Aug 2026 12:11:35 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=96=B0=E5=A2=9E=20JsonSerializer=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=88cjpm=20test?= =?UTF-8?q?=EF=BC=8C13=20=E7=94=A8=E4=BE=8B=EF=BC=89=EF=BC=9Bfix:=20?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=20Deserialize<=3FT>(null)=20=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=B1=BB=E5=9E=8B=E5=8C=96=20None?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 覆盖:基础类型、对象字段输出(下划线保留/注解/忽略/None→null/继承限制)、 Array/ArrayList/HashSet/HashMap 往返、枚举、命名策略、Option 字段、 宽松类型转换、maxDepth - 修复 JsonReader:Option 反序列化 null 时用反射构造目标类型 None 构造器 (反射层 Option 构造器统一按 1 参数槽登记),而非返回裸 None 导致 read 类型不匹配异常 --- src/json/JsonReader.cj | 40 +++++ src/json/JsonSerializer_test.cj | 289 ++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 src/json/JsonSerializer_test.cj diff --git a/src/json/JsonReader.cj b/src/json/JsonReader.cj index 281ca01..378d83e 100644 --- a/src/json/JsonReader.cj +++ b/src/json/JsonReader.cj @@ -55,6 +55,15 @@ public class JsonReader { // Option:null → None;否则读取内层并用枚举构造器包 Some if (typeName.startsWith("Option<")) { if (value is JsonNull) { + // 必须用目标类型 Option 的 None 构造器创建,保证运行时类型匹配。 + // 反射层将 Option 构造器统一登记为 1 参数槽(None 的参数类型是内层 X), + // 传一个 X 的占位值即可(None 构造器忽略实参语义)。 + if (let et: EnumTypeInfo <- typeInfo) { + let ctor = et.getConstructor("None", argsCount: 1) + let innerName = JsonWriter.extractTypeArgs(typeName)[0] + let placeholder = defaultValueFor(innerName) + return ctor.apply([placeholder]) + } return None } let innerName = JsonWriter.extractTypeArgs(typeName)[0] @@ -240,4 +249,35 @@ public class JsonReader { } throw Exception("JsonSerializer: 无法转换为 Char") } + + /// 生成某类型的占位默认值(用于构造 Option 的 None:None 的参数槽类型是 X)。 + private static func defaultValueFor(typeName: String): Any { + match (typeName) { + case "String" => return "" + case "Bool" => return false + case "Int64" => return Int64(0) + case "Int32" => return Int32(0) + case "Int16" => return Int16(0) + case "Int8" => return Int8(0) + case "UInt64" => return UInt64(0) + case "UInt32" => return UInt32(0) + case "UInt16" => return UInt16(0) + case "UInt8" => return UInt8(0) + case "Float64" => return Float64(0.0) + case "Float32" => return Float32(0.0) + case "Rune" => return Rune(0) + case _ => () + } + // 集合/对象/枚举:反射无参构造;失败则抛异常 + if (let ct: ClassTypeInfo <- TypeInfo.get(typeName)) { + return ct.construct([]) + } + if (let et: EnumTypeInfo <- TypeInfo.get(typeName)) { + // constructors 是 Collection:取第一个构造器名 + let first = et.constructors.iterator().next().getOrThrow() + let ctor = et.getConstructor(first.name, argsCount: 0) + return ctor.apply([]) + } + throw Exception("JsonSerializer: 无法为 Option 占位值构造类型 ${typeName}") + } } diff --git a/src/json/JsonSerializer_test.cj b/src/json/JsonSerializer_test.cj new file mode 100644 index 0000000..a03041b --- /dev/null +++ b/src/json/JsonSerializer_test.cj @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2025 SimcuTeam. All rights reserved. + * JsonSerializer 单元测试(cjpm test)。 + * 覆盖:基础类型 / 对象字段输出 / 注解(JsonPropertyName、JsonIgnore)/ Option / + * 集合(Array、ArrayList、HashSet)/ HashMap / 枚举 / 命名策略 / + * 继承(平台限制)/ maxDepth / 宽松类型转换。 + * + * 运行:cjpm test + */ + +package simapi_serialization.json + +import std.collection.* +import std.unittest.* +import std.unittest.testmacro.* + +enum Role { + | Admin + | User + | Guest +} + +// 混合测试:_ 前缀字段 与 无下划线字段 同时存在 +class Address { + public var _city: String = "" // 下划线字段 → 原样输出 _city + public var zip: String = "" // 无下划线 → 原样输出 zip +} + +// 继承测试:父类字段(_id/_createdAt)受仓颉反射「声明类严格校验」限制 +open class BaseUser { + public var _id: String = "" + public var _createdAt: String = "" +} + +class User <: BaseUser { + public var _name: String = "" // 下划线字段 + public var _age: Int64 = 0 // 下划线字段 + public var score: Float64 = 0.0 // 无下划线 + public var active: Bool = true // 无下划线 + public var _role: Role = Role.User // 下划线字段(枚举) + public var opt: ?String = None // 无下划线(Option=Some) + public var remark: ?String = None // 无下划线(Option=None → null) + public var _tags: Array = [] // 下划线字段(集合) + public var addr: Address = Address() // 无下划线(嵌套对象) + @JsonPropertyName["user_alias"] + public var _alias: String = "" // 下划线 + 注解覆盖 + @JsonIgnore + public var temp: String = "secret" // 无下划线 + 忽略 +} + +func makeUser(): User { + let u = User() + u._name = "alice" + u._age = 30 + u.score = 88.5 + u.active = true + u._role = Role.Admin + u.opt = Some("hello") + u._tags = ["a", "b", "c"] + u.addr._city = "beijing" + u.addr.zip = "100000" + u._alias = "xiaoming" + u.temp = "hidden" + u +} + +func roleStr(r: Role): String { + match (r) { + case Role.Admin => "Admin" + case Role.User => "User" + case Role.Guest => "Guest" + } +} + +func toStr(x: Any): String { + if (let s: String <- x) { return s } + if (let s: ToString <- x) { return s.toString() } + "?" +} + +@Test +public class JsonSerializerTests { + /// 基础类型序列化 + @TestCase + public func basicTypes() { + @Expect(JsonSerializer.Serialize(Int64(42)), "42") + @Expect(JsonSerializer.Serialize(Int32(7)), "7") + @Expect(JsonSerializer.Serialize(Int16(3)), "3") + @Expect(JsonSerializer.Serialize(UInt32(9)), "9") + @Expect(JsonSerializer.Serialize("hi"), "\"hi\"") + @Expect(JsonSerializer.Serialize(true), "true") + @Expect(JsonSerializer.Serialize(false), "false") + @Expect(JsonSerializer.Serialize(Float64(3.5)), "3.500000") + } + + /// 对象序列化:下划线保留、注解生效、忽略生效、None→null、父类字段不输出 + @TestCase + public func objectSerialize() { + let json = JsonSerializer.Serialize(makeUser()) + // 下划线字段原样输出 + @Expect(json.contains("\"_name\": \"alice\""), true) + @Expect(json.contains("\"_age\": 30"), true) + // 无下划线字段原样输出 + @Expect(json.contains("\"score\""), true) + @Expect(json.contains("\"active\": true"), true) + // @JsonPropertyName 覆盖字段名 + @Expect(json.contains("\"user_alias\": \"xiaoming\""), true) + @Expect(json.contains("\"_alias\""), false) + // @JsonIgnore 字段不输出 + @Expect(json.contains("temp"), false) + @Expect(json.contains("\"secret\""), false) + // Option:Some→值,None→null + @Expect(json.contains("\"opt\": \"hello\""), true) + @Expect(json.contains("\"remark\": null"), true) + // 枚举 → 名字字符串 + @Expect(json.contains("\"_role\": \"Admin\""), true) + // 嵌套对象 + @Expect(json.contains("\"_city\": \"beijing\""), true) + @Expect(json.contains("\"zip\": \"100000\""), true) + // 继承限制:父类字段不序列化 + @Expect(json.contains("_id"), false) + @Expect(json.contains("_createdAt"), false) + } + + /// 对象反序列化往返 + @TestCase + public func objectRoundTrip() { + let json = JsonSerializer.Serialize(makeUser()) + let u2 = JsonSerializer.Deserialize(json) + @Expect(u2._name, "alice") + @Expect(u2._age, Int64(30)) + @Expect(u2.active, true) + @Expect(roleStr(u2._role), "Admin") + @Expect(u2.addr._city, "beijing") + @Expect(u2.addr.zip, "100000") + @Expect(u2._alias, "xiaoming") + @Expect(u2.temp, "secret") // @JsonIgnore:反序列化不改动默认值 + // Option 往返 + let optVal: ?String = u2.opt + @Expect(optVal.isSome(), true) + if (let Some(v) <- optVal) { + @Expect(v, "hello") + } + let remarkVal: ?String = u2.remark + @Expect(remarkVal.isNone(), true) + // 集合往返 + @Expect(u2._tags.size, Int64(3)) + @Expect(u2._tags[0], "a") + @Expect(u2._tags[1], "b") + @Expect(u2._tags[2], "c") + } + + /// Array 往返 + @TestCase + public func arrayRoundTrip() { + let json = JsonSerializer.Serialize(["x", "y", "z"]) + let arrBack = JsonSerializer.Deserialize>(json) + @Expect(arrBack.size, Int64(3)) + @Expect(arrBack[0], "x") + @Expect(arrBack[2], "z") + } + + /// ArrayList 往返 + @TestCase + public func arrayListRoundTrip() { + var list = ArrayList() + list.add("a") + list.add("b") + let json = JsonSerializer.Serialize(list) + let back = JsonSerializer.Deserialize>(json) + @Expect(back.size, Int64(2)) + @Expect(back[0], "a") + @Expect(back[1], "b") + } + + /// HashSet 往返 + @TestCase + public func hashSetRoundTrip() { + var set = HashSet() + set.add("x") + set.add("y") + let json = JsonSerializer.Serialize(set) + let back = JsonSerializer.Deserialize>(json) + @Expect(back.contains("x"), true) + @Expect(back.contains("y"), true) + } + + /// HashMap 往返 + @TestCase + public func hashMapRoundTrip() { + let m = HashMap() + m["k1"] = "v1" + m["k2"] = Int64(42) + let json = JsonSerializer.Serialize(m) + let mBack = JsonSerializer.Deserialize>(json) + @Expect(toStr(mBack["k1"]), "v1") + @Expect(toStr(mBack["k2"]), "42") + } + + /// 命名策略:NONE / CAMEL / SNAKE 对当前模型(全小写+下划线)输出一致 + @TestCase + public func namingPolicies() { + let optNone = JsonOption() + optNone.propertyNamingPolicy = PropertyNamingPolicy.None + let jsonNone = JsonSerializer.Serialize(makeUser(), optNone) + + let optCamel = JsonOption() + optCamel.propertyNamingPolicy = PropertyNamingPolicy.CamelCase + let jsonCamel = JsonSerializer.Serialize(makeUser(), optCamel) + + let optSnake = JsonOption() + optSnake.propertyNamingPolicy = PropertyNamingPolicy.SnakeCase + let jsonSnake = JsonSerializer.Serialize(makeUser(), optSnake) + + // 模型字段全为小写/下划线,三种策略结果应一致 + @Expect(jsonNone, jsonCamel) + @Expect(jsonNone, jsonSnake) + } + + /// 命名策略转换规则(直接单测) + @TestCase + public func applyNamingPolicy() { + // SnakeCase:大写转 _小写 + @Expect(ReflectionCache.applyNamingPolicy("userName", PropertyNamingPolicy.SnakeCase), "user_name") + @Expect(ReflectionCache.applyNamingPolicy("userNameAge", PropertyNamingPolicy.SnakeCase), "user_name_age") + // 下划线不剥除、不双写 + @Expect(ReflectionCache.applyNamingPolicy("_name", PropertyNamingPolicy.SnakeCase), "_name") + // CamelCase:仅首字母小写 + @Expect(ReflectionCache.applyNamingPolicy("Name", PropertyNamingPolicy.CamelCase), "name") + @Expect(ReflectionCache.applyNamingPolicy("_name", PropertyNamingPolicy.CamelCase), "_name") + // None:原样 + @Expect(ReflectionCache.applyNamingPolicy("Name", PropertyNamingPolicy.None), "Name") + } + + /// 枚举序列化/反序列化 + @TestCase + public func enumSerialize() { + @Expect(JsonSerializer.Serialize(Role.Admin), "\"Admin\"") + @Expect(JsonSerializer.Serialize(Role.Guest), "\"Guest\"") + let u = User() + u._role = Role.Guest + let back = JsonSerializer.Deserialize(JsonSerializer.Serialize(u)) + @Expect(roleStr(back._role), "Guest") + } + + /// Option 字段:null → None,值 → Some + @TestCase + public func optionField() { + let none: ?String = JsonSerializer.Deserialize("null") + @Expect(none.isNone(), true) + let some: ?String = JsonSerializer.Deserialize("\"hi\"") + @Expect(some.isSome(), true) + if (let Some(v) <- some) { + @Expect(v, "hi") + } + } + + /// 宽松类型转换:JSON 数字 ↔ 字符串字段互转 + @TestCase + public func looseConversion() { + // 数字 → String 字段 + let s: String = JsonSerializer.Deserialize("42") + @Expect(s, "42") + // 字符串 → Int64 字段 + let i: Int64 = JsonSerializer.Deserialize("\"42\"") + @Expect(i, Int64(42)) + // 浮点 → Int64(截断) + let j: Int64 = JsonSerializer.Deserialize("3.9") + @Expect(j, Int64(3)) + // 字符串 → Bool + let b: Bool = JsonSerializer.Deserialize("\"true\"") + @Expect(b, true) + } + + /// 深度限制:超过 maxDepth 抛异常 + @TestCase + public func maxDepth() { + let opt = JsonOption() + opt.maxDepth = 0 + let u = User() + let threw = try { + JsonSerializer.Serialize(u, opt) + false + } catch (_: Exception) { + true + } + @Expect(threw, true) + } +}