diff --git a/.claude/skills/translate-docs-zh/references/terminology.md b/.claude/skills/translate-docs-zh/references/terminology.md index f7ecbf4c597..7ace8256420 100644 --- a/.claude/skills/translate-docs-zh/references/terminology.md +++ b/.claude/skills/translate-docs-zh/references/terminology.md @@ -39,7 +39,8 @@ Use these terms consistently across all translated docs. | cross-language | 跨语言 | | | object graph | 对象图 | Use for graph-shaped data models with identity, shared refs, or cycles. | | domain object | 领域对象 | Use for generated host-language models used directly in application code. | -| sum type | 和类型 | Prefer when describing `union`-style algebraic variants across languages. | +| sum type | 联合类型 | Use for `union`-style algebraic variants across languages. | +| custom serializer | 自定义序列化器 | Keep filenames, document IDs, and links aligned with the current English document owner. | ## Terms to Avoid (Unless User Explicitly Requests) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2dd7e4b7e57..d6a32934f0d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,6 +23,10 @@ on: - main pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 76ebc92796d..ec7c9f6c7a6 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -8,6 +8,10 @@ on: schedule: - cron: "0 5 * * *" +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + jobs: check: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bb92a7b4574..da84e55e42c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,8 +19,11 @@ name: ❄️ Lint on: [pull_request] -jobs: +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true +jobs: markdownlint: name: 🍇 Markdown runs-on: ubuntu-latest diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/csharp/custom-serializers.md b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/csharp/custom-serializers.md index af684393c2a..2c85502b7f2 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/csharp/custom-serializers.md +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/csharp/custom-serializers.md @@ -1,6 +1,6 @@ --- title: 自定义序列化器 -sidebar_position: 4 +sidebar_position: 11 id: custom_serializers license: | Licensed to the Apache Software Foundation (ASF) under one or more @@ -21,6 +21,8 @@ license: | 当某个类型不是通过 `[ForyStruct]` 生成,或者需要专门的编码方式时,可以使用自定义序列化器。 +[外部类型序列化](external_types)可为可变的第三方目标类型生成序列化器,包括精确映射私有字段或已重命名字段。对于不可变、仅能通过构造函数或工厂创建、包含 `readonly` 或 `init-only` 成员、需要类型转换或采用自定义编码格式的目标类型,请使用自定义序列化器。在 .NET 8 上,如果某个外部私有编码字段的声明所在类型或访问器签名使用了泛型,也需要使用自定义序列化器。只用于存储的精确映射不会生成私有访问器。 + ## 实现 `Serializer` ```csharp @@ -64,6 +66,12 @@ byte[] payload = fory.Serialize(value); Point decoded = fory.Deserialize(payload); ``` +当对端使用类型名称而不是数字 ID 来标识类型时,请使用接受名称参数的重载: + +```csharp +fory.Register("com.example.Point"); +``` + ## 序列化器行为说明 - `WriteData` / `ReadData` 只负责处理载荷内容。 @@ -80,5 +88,5 @@ Point decoded = fory.Deserialize(payload); ## 相关主题 - [类型注册](type-registration.md) -- [字段配置](schema-metadata.md) +- [Schema 元数据](schema-metadata.md) - [故障排查](troubleshooting.md) diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/basic-serialization.md b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/basic-serialization.md index 2813e34dabf..75d9217b355 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/basic-serialization.md +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/basic-serialization.md @@ -116,11 +116,11 @@ assert_eq!(person, decoded); ### 日期和时间 -| Rust 类型 | 描述 | -| ----------- | -------------------------------- | +| Rust 类型 | 描述 | +| ----------- | --------------------------------- | | `Date` | 不带时区的日期,存储为 epoch 天数 | | `Timestamp` | 时间点,存储为 epoch 秒和纳秒 | -| `Duration` | 有符号时长,存储为秒和规范化纳秒 | +| `Duration` | 有符号时长,存储为秒和规范化纳秒 | 内置承载类型提供无额外依赖的构造器、访问器、转换和 checked 算术: @@ -188,4 +188,4 @@ let decoded: MyStruct = fory.deserialize_from(&mut reader)?; - [类型注册](type-registration.md) - 注册类型 - [引用](references.md) - 共享引用和循环引用 -- [自定义序列化器](custom-serializers.md) - 手动序列化 +- [自定义序列化器](custom-serializers.md) - 自定义序列化 diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/custom-serializers.md b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/custom-serializers.md index a51d0b9b3b6..b48eb32d4f7 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/custom-serializers.md +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/rust/custom-serializers.md @@ -1,6 +1,6 @@ --- title: 自定义序列化器 -sidebar_position: 4 +sidebar_position: 10 id: custom_serializers license: | Licensed to the Apache Software Foundation (ASF) under one or more @@ -19,129 +19,165 @@ license: | limitations under the License. --- -对于不支持 `#[derive(ForyObject)]` 的类型,可以手动实现 `Serializer` trait。 +当派生宏无法表达类型的序列化表示,或者需要特意使用不透明编码时,请使用自定义序列化器。序列化器是一种类型级实现,通过 `Target` 指定其处理的值类型。 -## 何时使用自定义序列化器 +对于需要保持结构化 Schema 的公开第三方结构体或枚举,优先使用[外部类型序列化](external_types)。 -- 来自其他 crate 的外部类型 -- 具有特殊序列化要求的类型 -- 旧数据格式兼容性 -- 性能关键的自定义编码 +## 实现自定义序列化器 -## 实现 Serializer Trait +以下示例使用本地类型自身作为序列化器: ```rust -use fory::{Fory, ReadContext, WriteContext, Serializer, ForyDefault, Error}; -use std::any::Any; +use fory::{Error, Fory, ReadContext, Serializer, WriteContext}; #[derive(Debug, PartialEq)] -struct CustomType { +struct Point { value: i32, - name: String, } -impl Serializer for CustomType { - fn fory_write_data(&self, context: &mut WriteContext, is_field: bool) { - context.writer.write_i32(self.value); - context.writer.write_varuint32(self.name.len() as u32); - context.writer.write_utf8_string(&self.name); - } +impl Serializer for Point { + type Target = Self; - fn fory_read_data(context: &mut ReadContext, is_field: bool) -> Result { - let value = context.reader.read_i32(); - let len = context.reader.read_varuint32() as usize; - let name = context.reader.read_utf8_string(len); - Ok(Self { value, name }) + fn write_data(value: &Self, context: &mut WriteContext) -> Result<(), Error> { + context.writer.write_i32(value.value); + Ok(()) } - fn fory_type_id_dyn(&self, type_resolver: &TypeResolver) -> u32 { - Self::fory_get_type_id(type_resolver) + fn read_data(context: &mut ReadContext) -> Result { + Ok(Self { + value: context.reader.read_i32()?, + }) } - fn as_any(&self) -> &dyn Any { - self + fn default_value(_context: &mut ReadContext) -> Result { + Ok(Self { value: 0 }) } } -impl ForyDefault for CustomType { - fn fory_default() -> Self { - Self::default() +let mut fory = Fory::builder().xlang(false).build(); +fory.register_serializer::(100)?; + +let value = Point { value: 42 }; +let bytes = fory.serialize(&value)?; +let decoded: Point = fory.deserialize(&bytes)?; +assert_eq!(decoded, value); +# Ok::<(), Error>(()) +``` + +`write_data` 和 `read_data` 处理 EXT 主体。Fory 的完整值 `write` 和 `read` 操作会为根值或字段补充引用帧和类型信息帧。 + +`default_value` 是可选的。仅当兼容字段为空或缺失时存在有意义的取值,才实现此方法。它会接收当前的 `ReadContext`,因此需要分配内存的默认值可以应用与普通读取相同的反序列化限制。 + +## 序列化第三方不透明类型 + +可以用独立的序列化器处理其他 crate 中的类型: + +```rust +use fory::{Error, ReadContext, Serializer, WriteContext}; + +struct UuidSerializer; + +#[cold] +#[inline(never)] +fn invalid_uuid(error: uuid::Error) -> Error { + Error::invalid_data(error.to_string()) +} + +impl Serializer for UuidSerializer { + type Target = uuid::Uuid; + + fn write_data( + value: &uuid::Uuid, + context: &mut WriteContext, + ) -> Result<(), Error> { + context.writer.write_bytes(value.as_bytes()); + Ok(()) + } + + fn read_data(context: &mut ReadContext) -> Result { + let bytes = context.reader.read_bytes(16)?; + uuid::Uuid::from_slice(bytes).map_err(invalid_uuid) } } ``` -## 注册自定义序列化器 +注册序列化器,然后在根值或字段上选择使用它: ```rust -let mut fory = Fory::default(); -fory.register_serializer::(100); - -let custom = CustomType { - value: 42, - name: "test".to_string(), -}; -let bytes = fory.serialize(&custom); -let decoded: CustomType = fory.deserialize(&bytes)?; -assert_eq!(custom, decoded); +fory.register_serializer::(101)?; + +let bytes = fory.serialize_with::(&uuid)?; +let decoded = + fory.deserialize_with::(&bytes)?; ``` -## WriteContext 和 ReadContext +```rust +#[derive(ForyStruct)] +struct Request { + #[fory(with = UuidSerializer)] + id: uuid::Uuid, +} +``` -`WriteContext` 和 `ReadContext` 提供对以下内容的访问: +自定义序列化器的主体是不透明的。兼容模式不会映射其中的字段。 -- **writer/reader**:二进制缓冲区操作 -- **type_resolver**:类型注册信息 -- **ref_resolver**:引用跟踪(用于共享/循环引用) +## 支持 `Arc` -### 常用 Writer 方法 +如果必须将自定义序列化器的目标具体化为 `Arc`,或具体化为某种同步的应用程序 trait,请实现 `read_arc_any`: ```rust -// 原始类型 -context.writer.write_i8(value); -context.writer.write_i16(value); -context.writer.write_i32(value); -context.writer.write_i64(value); -context.writer.write_f32(value); -context.writer.write_f64(value); -context.writer.write_bool(value); +use std::any::Any; +use std::sync::Arc; + +impl Serializer for Point { + type Target = Self; + + // 按照上面的方式实现 write_data、read_data 和所需的默认值。 + + fn read_arc_any( + context: &mut ReadContext, + ) -> Result, Error> { + Ok(Arc::new(Self::read_data(context)?)) + } +} +``` + +目标必须实现 `Send + Sync`。如果省略此方法,带具体类型的操作以及 `Box`、`Rc` 操作仍然可用,而将值具体化为同步 `Arc` 时会返回错误。 + +## 按名称注册 -// 变长整数 -context.writer.write_varint32(value); -context.writer.write_varuint32(value); +当序列化后的标识是限定名称时,请使用名称注册: -// 字符串 -context.writer.write_utf8_string(&string); +```rust +fory.register_serializer_by_name::( + "example.Uuid", +)?; ``` -### 常用 Reader 方法 +对于同一个目标,一个 `Fory` 实例最多只能注册一个序列化器。 + +## 上下文访问 + +`WriteContext` 和 `ReadContext` 会公开二进制 writer 和 reader: ```rust -// 原始类型 -let value = context.reader.read_i8(); -let value = context.reader.read_i16(); -let value = context.reader.read_i32(); -let value = context.reader.read_i64(); -let value = context.reader.read_f32(); -let value = context.reader.read_f64(); -let value = context.reader.read_bool(); - -// 变长整数 -let value = context.reader.read_varint32(); -let value = context.reader.read_varuint32(); - -// 字符串 -let string = context.reader.read_utf8_string(len); +context.writer.write_i8(value); +context.writer.write_i32(value); +context.writer.write_var_u32(value); +context.writer.write_f64(value); + +let value = context.reader.read_i8()?; +let value = context.reader.read_i32()?; +let value = context.reader.read_var_u32()?; +let value = context.reader.read_f64()?; ``` -## 最佳实践 +对于大小可变的主体,请先验证可读字节数和对象图内存限制,再根据编码后的长度分配内存。 -1. **使用变长编码**:对可能较小的整数使用变长编码 -2. **首先写入长度**:对变长数据先写入长度 -3. **正确处理错误**:在 read 方法中正确处理错误 -4. **实现 ForyDefault**:以支持 schema 演化 +当自定义序列化器被用作大小可变容器的子项时,容器必须在元素数或映射条目数之后,按总量计算为每个已声明的元素或映射条目至少写入一个字节。如果容器的完整头部、元数据、帧和子项主体的总长度小于该计数,Fory 会拒绝序列化,从而绝不会写出与之配对的分配安全检查无法读取的字节。定长数组不受此限制,因为其经过验证的计数不控制内存分配;`Vec`、`VecDeque` 和 `BinaryHeap` 中的零大小元素也不受此限制,因为这些容器不会为它们分配后备存储空间。 ## 相关主题 -- [类型注册](type-registration.md) - 注册序列化器 -- [基础序列化](basic-serialization.md) - 使用 ForyObject derive -- [Schema 演化](schema-evolution.md) - Compatible 模式 +- [外部类型序列化](external_types) +- [类型注册](type-registration.md) +- [Schema 演进](schema-evolution.md) diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/swift/custom-serializers.md b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/swift/custom-serializers.md index e5c47e340db..e357449d79a 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/swift/custom-serializers.md +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current/guide/swift/custom-serializers.md @@ -1,6 +1,6 @@ --- title: 自定义序列化器 -sidebar_position: 4 +sidebar_position: 10 id: custom_serializers license: | Licensed to the Apache Software Foundation (ASF) under one or more @@ -19,66 +19,260 @@ license: | limitations under the License. --- -对于不适合使用 `@ForyObject` 的类型,或需要特殊编码逻辑的场景,可以手动实现 `Serializer`。 +当目标类型需要自定义编码,或无法满足[外部结构化序列化器](external_types)的直接访问要求时,请使用自定义序列化器。 -## 适用场景 +自定义序列化器并非只能用于外部类型: -- 外部类型需要严格的编码兼容性 -- 需要更紧凑或特殊的编码布局 -- 旧载荷迁移路径 -- 性能敏感的热点路径 +- 如果目标类型自身遵循 `Serializer`,并且 `Target == Self`,则该实现会在所有位置被隐式选中。 +- 如果单独定义的序列化器以另一种类型作为 `Target`,则必须在每个需要它的位置显式选择该序列化器。 -## 实现 `Serializer` +根值、生成字段、可选值、数组、集合和字典都遵循相同的选择规则。请注册单独定义的序列化器,并在使用它的位置显式选择它。 + +## 何时使用自定义序列化器 + +- 目标类型包含私有或不可变状态。 +- 目标类型必须强制执行构造不变量。 +- 目标类型需要专用的紧凑编码。 +- 目标类型需要自定义校验或构造逻辑。 +- 外部枚举无法进行穷尽式 `switch`。 +- 外部联合类型无法表示 `UnknownCase`。 + +## 用户自有的目标类型 + +如果目标类型由你维护,请让目标类型自身实现 `Serializer`,并将 `Target` 设为相同类型: + +```swift +import Fory + +struct AccountID: Serializer, Equatable { + typealias Target = AccountID + + let rawValue: UInt64 + + static var staticTypeId: TypeId { + .ext + } + + static func writeData( + _ value: AccountID, + _ context: WriteContext + ) throws { + try UInt64.writeData(value.rawValue, context) + } + + static func readData( + _ context: ReadContext + ) throws -> AccountID { + AccountID(rawValue: try UInt64.readData(context)) + } +} +``` + +通过常规的根值 API 注册并使用该类型: + +```swift +let fory = Fory() +try fory.register(AccountID.self, id: 300) + +let input = AccountID(rawValue: 42) +let data = try fory.serialize(input) +let output: AccountID = try fory.deserialize(data) + +assert(input == output) +``` + +由于 `AccountID.Target == AccountID`,因此不需要 `with:` 参数。 + +## 为外部类型定义一个全局序列化器 + +Swift 允许应用通过追溯遵循(retroactive conformance)让外部类型实现 `Serializer`: ```swift import Foundation import Fory -struct UUIDBox: Serializer, Equatable { - var value: UUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000")! +extension UUID: @retroactive Serializer { + public typealias Target = UUID + + public static var staticTypeId: TypeId { + .ext + } + + public static func defaultValue( + _ context: ReadContext + ) throws -> UUID { + _ = context + return UUID( + uuidString: "00000000-0000-0000-0000-000000000000" + )! + } + + public static func writeData( + _ value: UUID, + _ context: WriteContext + ) throws { + try String.writeData(value.uuidString, context) + } - static func foryDefault() -> UUIDBox { - UUIDBox() + public static func readData( + _ context: ReadContext + ) throws -> UUID { + let raw = try String.readData(context) + guard let uuid = UUID(uuidString: raw) else { + throw ForyError.invalidData("invalid UUID string: \(raw)") + } + return uuid } +} +``` + +注册外部类型本身: + +```swift +try fory.register(UUID.self, id: 300) + +let input = UUID() +let data = try fory.serialize(input) +let output: UUID = try fory.deserialize(data) +``` + +由于 `UUID.Target == UUID`,没有添加注解的生成字段和普通容器也会选用该实现: - static var staticTypeId: ForyTypeId { +```swift +@ForyStruct +struct Request { + var requestID: UUID +} + +let input = [UUID(), UUID()] +let data = try fory.serialize(input) +let output: [UUID] = try fory.deserialize(data) +``` + +追溯遵循会作用于整个进程。对于一个给定类型,Swift 只允许存在一个 `Serializer` 遵循;`@retroactive` 只是表明你已知晓编译器警告,并不能让相互冲突的多个遵循变得安全。只有当应用有意选择唯一的全局实现时,才应采用这种形式。公共库通常应改为提供单独定义的序列化器。 + +## 单独定义的序列化器 + +当公共库不应声明作用于整个进程的遵循,或应用需要多个实现或替代实现时,请单独定义序列化器。目标类型既可以是外部类型,也可以是用户自有类型: + +```swift +import Foundation +import Fory + +public enum UUIDStringSerializer: Serializer { + public typealias Target = UUID + + public static var staticTypeId: TypeId { .ext } - func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws { - _ = hasGenerics - try value.uuidString.foryWriteData(context, hasGenerics: false) + public static func defaultValue( + _ context: ReadContext + ) throws -> UUID { + _ = context + return UUID( + uuidString: "00000000-0000-0000-0000-000000000000" + )! + } + + public static func writeData( + _ value: UUID, + _ context: WriteContext + ) throws { + try String.writeData(value.uuidString, context) } - static func foryReadData(_ context: ReadContext) throws -> UUIDBox { - let raw = try String.foryReadData(context) + public static func readData( + _ context: ReadContext + ) throws -> UUID { + let raw = try String.readData(context) guard let uuid = UUID(uuidString: raw) else { - throw ForyError.invalidData("invalid UUID string: \\(raw)") + throw ForyError.invalidData("invalid UUID string: \(raw)") } - return UUIDBox(value: uuid) + return uuid } } ``` -## 注册并使用 +注册单独定义的序列化器,并在根值处显式选择它: ```swift let fory = Fory() -fory.register(UUIDBox.self, id: 300) +try fory.register(UUIDStringSerializer.self, id: 300) -let input = UUIDBox(value: UUID()) -let data = try fory.serialize(input) -let output: UUIDBox = try fory.deserialize(data) +let input = UUID() +let data = try fory.serialize(input, with: UUIDStringSerializer.self) +let output = try fory.deserialize(data, with: UUIDStringSerializer.self) assert(input == output) ``` -## 如何选择 `staticTypeId` +另一个声明(例如 `UUIDBytesSerializer`)可以使用不同的主体编码处理同一目标类型。Fory 无法自动在多个单独定义的序列化器中进行选择。请在根值处通过 `with:` 选择所需的序列化器,并在字段上使用与之匹配的注解。一个 `Fory` 实例只能为该目标类型注册一种实现。 + +直接以 `Any` 和 `AnyObject` 作为根值的便捷 API 仍然是动态操作。以 `Any` 形式传入具体值时,可能会使用已经注册的序列化器,但使用静态类型的根值和字段仍然需要通过 `with:` 指定单独定义的序列化器。 + +## 字段和容器 + +如果字段类型直接实现了 `Serializer`,并且 `Target == Self`,则无需指定选择器: + +```swift +@ForyStruct +struct Request { + var accountID: AccountID +} +``` + +单独定义的序列化器必须显式选择: + +```swift +@ForyStruct +struct ExternalRequest { + @ForyField(with: UUIDStringSerializer.self) + var requestID: UUID +} +``` + +如果普通容器所包含的类型直接实现了 `Serializer`,则无需指定选择器。这也包括有意添加的追溯遵循: + +```swift +let accountIDs = [ + AccountID(rawValue: 1), + AccountID(rawValue: 2), +] +let data = try fory.serialize(accountIDs) +let output: [AccountID] = try fory.deserialize(data) +``` + +如果元素使用单独定义的序列化器,请在容器注解中指定该序列化器: + +```swift +@ListField(element: .with(UUIDStringSerializer.self)) +var requestIDs: [UUID] +``` + +在根值处,请使用与之匹配的容器序列化器: + +```swift +let data = try fory.serialize( + requestIDs, + with: ArraySerializer.self +) +``` + +## 自定义序列化器规则 + +自定义序列化器的 `staticTypeId` 必须返回 `.ext`。`.structType`、`.enumType` 和 `.typedUnion` 分别保留给 `@ForyStruct`、`@ForyEnum` 和 `@ForyUnion`。 + +`writeData` 和 `readData` 只处理目标值的编码主体。请勿在这两个操作中调用根值的 `serialize` 或 `deserialize` 方法。 + +## 默认值 + +仅当目标类型存在可用于空字段或缺失字段的有效值时,才实现 `defaultValue(_:)`。 + +## 输入校验 -手动实现的自定义类型,需要让 `staticTypeId` 与实际编码形态匹配。 +请使用适当的 `ForyError` 拒绝无效输入。 -常见选择: +## 自定义类序列化器 -- `.structType`:常规结构化对象 -- `.enumType` / `.typedUnion`:枚举或联合类型 -- `.ext`:扩展或自定义类型 +对于存在循环引用的类,请重写读取完整值的 `read` 操作,并使用 Fory 的引用 API,以确保重复引用解析为同一个对象。