/**
* JsonEncodingVisitor.ets
*
* JSON 编码 Visitor - Protobuf JSON mapping
*
* Protobuf JSON 映射规范:
* - int32/uint32/sint32 → number
* - int64/uint64/sint64 → string (避免精度丢失)
* - float/double → number (特殊值: NaN, Infinity, -Infinity)
* - string → string
* - bytes → base64 string
* - bool → boolean
* - enum → number
* - message → nested object
* - repeated → array
* - map<string, V> → object
* - map<K, V> (K非string) → array of {key, value}
*
* Version: 1.0.0
* ArkTS 2025 兼容
*/
import { Visitor } from './Visitor'
import { Message } from './Message'
/**
* JSON 编码 Visitor
*
* 将消息编码为 Protobuf JSON 格式
*
* 使用方式:
* ```typescript
* const visitor = new JsonEncodingVisitor()
* message.traverse(visitor)
* const json = visitor.finish()
* ```
*
* 递归深度保护:
* - 最大嵌套深度:100 层
* - 超过限制会抛出错误
*/
export class JsonEncodingVisitor implements Visitor {
private json: Record<string, Object> = {}
private recursionDepth: number = 0
private readonly maxRecursionDepth: number = 100
// 字段编号到字段名的映射
// 注意:生成的消息类会设置此映射
// 临时实现:使用字段编号作为 key
private fieldNames: Map<number, string> = new Map()
constructor() {
this.json = {}
}
/**
* 设置字段名映射(由生成的消息类调用)
*/
setFieldNames(names: Map<number, string>): void {
this.fieldNames = names
}
/**
* 获取字段名(如果没有映射,返回 "field_N")
*/
private getFieldName(fieldNumber: number): string {
const name = this.fieldNames.get(fieldNumber)
if (name !== undefined) {
return name
}
// 临时实现:使用 field_N 作为字段名
return `field_${fieldNumber}`
}
// ========== 标量字段 ==========
/**
* 访问 int32 字段
* JSON: number
*/
visitInt32(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 int64 字段
* JSON: string (避免精度丢失)
*/
visitInt64(value: bigint, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.toString() as Object
}
/**
* 访问 uint32 字段
* JSON: number
*/
visitUint32(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 uint64 字段
* JSON: string (避免精度丢失)
*/
visitUint64(value: bigint, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.toString() as Object
}
/**
* 访问 sint32 字段
* JSON: number
*/
visitSint32(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 sint64 字段
* JSON: string (避免精度丢失)
*/
visitSint64(value: bigint, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.toString() as Object
}
/**
* 访问 fixed32 字段
* JSON: number
*/
visitFixed32(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 fixed64 字段
* JSON: string (避免精度丢失)
*/
visitFixed64(value: bigint, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.toString() as Object
}
/**
* 访问 sfixed32 字段
* JSON: number
*/
visitSfixed32(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 sfixed64 字段
* JSON: string (避免精度丢失)
*/
visitSfixed64(value: bigint, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.toString() as Object
}
/**
* 访问 string 字段
* JSON: string
*/
visitString(value: string, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 bytes 字段
* JSON: base64 string
*/
visitBytes(value: Uint8Array, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
// 转换为 base64
const base64 = this.bytesToBase64(value)
this.json[fieldName] = base64 as Object
}
/**
* 访问 bool 字段
* JSON: boolean
*/
visitBool(value: boolean, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
/**
* 访问 float 字段
* JSON: number or "NaN"/"Infinity"/"-Infinity"
*/
visitFloat(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
if (isNaN(value)) {
this.json[fieldName] = "NaN" as Object
} else if (value === Infinity) {
this.json[fieldName] = "Infinity" as Object
} else if (value === -Infinity) {
this.json[fieldName] = "-Infinity" as Object
} else {
this.json[fieldName] = value as Object
}
}
/**
* 访问 double 字段
* JSON: number or "NaN"/"Infinity"/"-Infinity"
*/
visitDouble(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
if (isNaN(value)) {
this.json[fieldName] = "NaN" as Object
} else if (value === Infinity) {
this.json[fieldName] = "Infinity" as Object
} else if (value === -Infinity) {
this.json[fieldName] = "-Infinity" as Object
} else {
this.json[fieldName] = value as Object
}
}
// ========== 枚举字段 ==========
/**
* 访问 enum 字段
* JSON: number (可选:也可以是 string 枚举名)
*/
visitEnum(value: number, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value as Object
}
// ========== 嵌套消息(⭐ 自动递归) ==========
/**
* 访问嵌套消息字段
*
* ⭐ 核心方法:自动处理递归!
*
* 实现逻辑:
* 1. 检查递归深度(防止栈溢出)
* 2. 创建新的 JsonEncodingVisitor
* 3. 调用嵌套消息的 traverse() 方法(自动递归)
* 4. 获取嵌套消息的 JSON 对象
* 5. 设置到当前 JSON 对象中
*
* @param value 嵌套消息对象
* @param fieldNumber proto 字段编号
* @throws 如果递归深度超过限制
*/
visitMessage(value: Message, fieldNumber: number): void {
// 递归深度保护
this.recursionDepth++
if (this.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
try {
const fieldName = this.getFieldName(fieldNumber)
// 创建嵌套 Visitor,递归遍历
const nestedVisitor = new JsonEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth // 继承递归深度
value.traverse(nestedVisitor) // ⭐ 自动递归
const nestedJson = nestedVisitor.finish()
this.json[fieldName] = nestedJson as Object
} finally {
this.recursionDepth--
}
}
// ========== Repeated 字段 ==========
/**
* 访问 repeated int32 字段
* JSON: array of number
*/
visitRepeatedInt32(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated int64 字段
* JSON: array of string
*/
visitRepeatedInt64(value: bigint[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => v.toString()) as Object
}
/**
* 访问 repeated uint32 字段
* JSON: array of number
*/
visitRepeatedUint32(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated uint64 字段
* JSON: array of string
*/
visitRepeatedUint64(value: bigint[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => v.toString()) as Object
}
/**
* 访问 repeated sint32 字段
* JSON: array of number
*/
visitRepeatedSint32(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated sint64 字段
* JSON: array of string
*/
visitRepeatedSint64(value: bigint[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => v.toString()) as Object
}
/**
* 访问 repeated fixed32 字段
* JSON: array of number
*/
visitRepeatedFixed32(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated fixed64 字段
* JSON: array of string
*/
visitRepeatedFixed64(value: bigint[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => v.toString()) as Object
}
/**
* 访问 repeated sfixed32 字段
* JSON: array of number
*/
visitRepeatedSfixed32(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated sfixed64 字段
* JSON: array of string
*/
visitRepeatedSfixed64(value: bigint[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => v.toString()) as Object
}
/**
* 访问 repeated string 字段
* JSON: array of string
*/
visitRepeatedString(value: string[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated bytes 字段
* JSON: array of base64 string
*/
visitRepeatedBytes(value: Uint8Array[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = value.map(v => this.bytesToBase64(v)) as Object
}
/**
* 访问 repeated bool 字段
* JSON: array of boolean
*/
visitRepeatedBool(value: boolean[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated float 字段
* JSON: array of number
*/
visitRepeatedFloat(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
// 处理特殊值
const jsonValues = value.map(v => {
if (isNaN(v)) return "NaN"
if (v === Infinity) return "Infinity"
if (v === -Infinity) return "-Infinity"
return v
})
this.json[fieldName] = jsonValues as Object
}
/**
* 访问 repeated double 字段
* JSON: array of number
*/
visitRepeatedDouble(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
// 处理特殊值
const jsonValues = value.map(v => {
if (isNaN(v)) return "NaN"
if (v === Infinity) return "Infinity"
if (v === -Infinity) return "-Infinity"
return v
})
this.json[fieldName] = jsonValues as Object
}
/**
* 访问 repeated enum 字段
* JSON: array of number
*/
visitRepeatedEnum(value: number[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
this.json[fieldName] = [...value] as Object
}
/**
* 访问 repeated message 字段
* JSON: array of object
* ⭐ 每个消息都会递归调用 visitMessage
*/
visitRepeatedMessage(value: Message[], fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const jsonArray: Record<string, Object>[] = []
for (const item of value) {
const nestedVisitor = new JsonEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth + 1
if (nestedVisitor.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
item.traverse(nestedVisitor)
jsonArray.push(nestedVisitor.finish())
}
this.json[fieldName] = jsonArray as Object
}
// ========== Map 字段 ==========
/**
* 访问 map<string, int32> 字段
* JSON: object with string keys
*/
visitMapStringInt32(value: Map<string, number>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<string, int64> 字段
* JSON: object with string values (避免精度丢失)
*/
visitMapStringInt64(value: Map<string, bigint>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k] = v.toString() as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<string, string> 字段
* JSON: object with string keys and values
*/
visitMapStringString(value: Map<string, string>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<string, bytes> 字段
* JSON: object with base64 string values
*/
visitMapStringBytes(value: Map<string, Uint8Array>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k] = this.bytesToBase64(v) as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<string, bool> 字段
* JSON: object with boolean values
*/
visitMapStringBool(value: Map<string, boolean>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<string, message> 字段
* JSON: object with nested object values
*/
visitMapStringMessage(value: Map<string, Message>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
const nestedVisitor = new JsonEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth + 1
if (nestedVisitor.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
v.traverse(nestedVisitor)
obj[k] = nestedVisitor.finish() as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int32, int32> 字段
* JSON: object with string keys (int32 转为 string)
*/
visitMapInt32Int32(value: Map<number, number>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k.toString()] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int32, string> 字段
* JSON: object with string keys
*/
visitMapInt32String(value: Map<number, string>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k.toString()] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int32, message> 字段
* JSON: object with string keys
*/
visitMapInt32Message(value: Map<number, Message>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
const nestedVisitor = new JsonEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth + 1
if (nestedVisitor.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
v.traverse(nestedVisitor)
obj[k.toString()] = nestedVisitor.finish() as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int64, int64> 字段
* JSON: object with string keys and values
*/
visitMapInt64Int64(value: Map<bigint, bigint>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k.toString()] = v.toString() as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int64, string> 字段
* JSON: object with string keys
*/
visitMapInt64String(value: Map<bigint, string>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
obj[k.toString()] = v as Object
})
this.json[fieldName] = obj as Object
}
/**
* 访问 map<int64, message> 字段
* JSON: object with string keys
*/
visitMapInt64Message(value: Map<bigint, Message>, fieldNumber: number): void {
const fieldName = this.getFieldName(fieldNumber)
const obj: Record<string, Object> = {}
value.forEach((v, k) => {
const nestedVisitor = new JsonEncodingVisitor()
nestedVisitor.recursionDepth = this.recursionDepth + 1
if (nestedVisitor.recursionDepth > this.maxRecursionDepth) {
throw new Error(`Message nesting depth exceeds limit: ${this.maxRecursionDepth}`)
}
v.traverse(nestedVisitor)
obj[k.toString()] = nestedVisitor.finish() as Object
})
this.json[fieldName] = obj as Object
}
// ========== 完成编码 ==========
/**
* 完成编码,获取最终的 JSON 对象
*
* @returns Protobuf JSON 格式对象
*/
finish(): Record<string, Object> {
return this.json
}
// ========== 辅助方法 ==========
/**
* 将 Uint8Array 转换为 base64 字符串
*
* @param bytes 字节数组
* @returns base64 字符串
*/
private bytesToBase64(bytes: Uint8Array): string {
// Base64 字符表
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let result = ''
let i = 0
// 每 3 个字节编码为 4 个 base64 字符
for (; i < bytes.length - 2; i += 3) {
const b1 = bytes[i]
const b2 = bytes[i + 1]
const b3 = bytes[i + 2]
result += chars.charAt(b1 >> 2)
result += chars.charAt(((b1 & 0x03) << 4) | (b2 >> 4))
result += chars.charAt(((b2 & 0x0F) << 2) | (b3 >> 6))
result += chars.charAt(b3 & 0x3F)
}
// 处理剩余字节
if (i < bytes.length) {
const b1 = bytes[i]
result += chars.charAt(b1 >> 2)
if (i + 1 < bytes.length) {
const b2 = bytes[i + 1]
result += chars.charAt(((b1 & 0x03) << 4) | (b2 >> 4))
result += chars.charAt((b2 & 0x0F) << 2)
result += '='
} else {
result += chars.charAt((b1 & 0x03) << 4)
result += '=='
}
}
return result
}
}