* ⭐ 设计说明:为什么 Binary 用 Visitor 而 JSON 直接生成?
*
* 本项目采用**混合序列化策略**:
*
* 1. Binary(Protobuf wire format)- 使用 Visitor 模式
* 优势:
* - 未来可轻松扩展(TextFormat, Hash, Debug 等)
* - 代码复用:Visitor 逻辑可在不同格式间共享
* - 架构一致性:遵循 SwiftProtobuf 成熟设计
*
* 2. JSON - 直接代码生成(本文件负责)
* 优势:
* - 性能至上:JSON 最常用,避免 Visitor 间接调用开销(~30-50% 提升)
* - 特殊逻辑:枚举名称映射、bigint→string、Base64 编码等
* - 代码清晰:生成的代码更易阅读和调试
*
* 生成的 JSON 方法:
* - static toJson(m: MessageType): MessageTypeJSON
* - static fromJson(j: MessageTypeJSON): MessageType
*
* 注意:不使用 JsonEncodingVisitor,而是直接生成方法体。
*
* 详见:ARCHITECTURE.md > 混合序列化策略
*/
* Generate traverse() method for Visitor pattern
*
* @param {protobuf.Type} msg - Message type
* @param {Array} fields - Message fields
* @param {string} int64Mode - 'bigint' or 'number'
* @param {Map} aliasMap - Type alias mappings
* @returns {Array<string>} Generated code lines
*/
function generateTraverseMethod(msg, fields, oneofs, int64Mode, aliasMap) {
const lines = []
lines.push(' /**')
lines.push(' * ⭐ Visitor pattern: traverse all fields')
lines.push(' */')
lines.push(' traverse(v: Visitor): void {')
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
const fieldNum = f.id
if (f.map) {
const keyType = mapKeyType(f, int64Mode)
const valueType = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : typeFor(f, int64Mode)
const keyTypeSimple = keyType === 'string' ? 'String' : (keyType === 'number' ? 'Int32' : 'Int64')
const valueTypeSimple = valueType === 'string' ? 'String' :
valueType === 'number' ? 'Int32' :
valueType === 'bigint' ? 'Int64' :
valueType === 'boolean' ? 'Bool' :
valueType === 'Uint8Array' ? 'Bytes' :
'Message'
lines.push(` if (this.${fnameSafe}.size > 0) {`)
lines.push(` v.visitMap${keyTypeSimple}${valueTypeSimple}(this.${fnameSafe}, ${fieldNum})`)
lines.push(' }')
} else if (f.repeated) {
const elemType = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : typeFor(f, int64Mode)
const isEnum = f.resolvedType && (f.resolvedType instanceof protobuf.Enum)
const isMessage = f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)
let visitMethod
if (isEnum) {
visitMethod = 'visitRepeatedEnum'
} else if (isMessage) {
visitMethod = 'visitRepeatedMessage'
} else {
visitMethod = `visitRepeated${capitalize(f.type)}`
}
lines.push(` if (this.${fnameSafe}.length > 0) {`)
lines.push(` v.${visitMethod}(this.${fnameSafe}, ${fieldNum})`)
lines.push(' }')
} else {
const isEnum = f.resolvedType && (f.resolvedType instanceof protobuf.Enum)
const isMessage = f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)
if (isMessage) {
lines.push(` if (this.${fnameSafe} !== null) {`)
lines.push(` v.visitMessage(this.${fnameSafe}, ${fieldNum})`)
lines.push(' }')
} else if (isEnum) {
lines.push(` if (this.${fnameSafe} !== 0) {`)
lines.push(` v.visitEnum(this.${fnameSafe}, ${fieldNum})`)
lines.push(' }')
} else {
const defaultValue = getDefaultValue(f, int64Mode)
const condition = getDefaultCheckCondition(f, fnameSafe, defaultValue, int64Mode)
let visitMethod = `visit${capitalize(f.type)}`
lines.push(` if (${condition}) {`)
lines.push(` v.${visitMethod}(this.${fnameSafe}, ${fieldNum})`)
lines.push(' }')
}
}
}
for (const g of oneofs) {
lines.push(` // oneof: ${g.name}`)
lines.push(` if (this.${g.name} !== undefined) {`)
lines.push(` switch (this.${g.name}.kind) {`)
for (const n of g.names) {
const f = msg.fields[n]
const fieldNum = f.id
const isEnum = f.resolvedType && (f.resolvedType instanceof protobuf.Enum)
const isMessage = f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)
lines.push(` case '${n}': {`)
if (isMessage) {
lines.push(` v.visitMessage(this.${g.name}.${n}, ${fieldNum})`)
} else if (isEnum) {
lines.push(` v.visitEnum(this.${g.name}.${n}, ${fieldNum})`)
} else {
lines.push(` v.visit${capitalize(f.type)}(this.${g.name}.${n}, ${fieldNum})`)
}
lines.push(' break')
lines.push(' }')
}
lines.push(' }')
lines.push(' }')
}
lines.push(' }')
return lines
}
* Generate decodeFrom() method
*/
function generateDecodeFromMethod(msg, fields, oneofs, int64Mode, aliasMap) {
const lines = []
lines.push(' /**')
lines.push(' * Decode from Reader (internal)')
lines.push(' */')
lines.push(' protected decodeFrom(r: Reader, len?: number): void {')
lines.push(' const endPos = len === undefined ? r.len : r.pos + len')
lines.push('')
lines.push(' while (r.pos < endPos) {')
lines.push(' const tag = r.uint32()')
lines.push(' const fieldNum = tag >>> 3')
lines.push('')
lines.push(' switch (fieldNum) {')
for (const f of fields) {
const fnameSafe = safeIdent(f.name)
const fieldNum = f.id
const alias = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : undefined
lines.push(` case ${fieldNum}: {`)
if (f.map) {
lines.push(' const l = r.uint32()')
lines.push(' const end2 = r.pos + l')
lines.push(' let k: ' + mapKeyType(f, int64Mode) + ' = ' + (mapKeyType(f, int64Mode) === 'string' ? `''` : '0'))
const vType = f.resolvedType ? (aliasMap.get((f.resolvedType.fullName||'').replace(/^\./,''))?.local || f.resolvedType.name) : typeFor(f, int64Mode)
lines.push(' let v: ' + vType + ' = ' + getDefaultValue({type: f.type.replace('map<', '').replace('>', '').split(',')[1].trim()}, int64Mode))
lines.push(' while (r.pos < end2) {')
lines.push(' const mapTag = r.uint32()')
lines.push(' const mapFieldNum = mapTag >>> 3')
lines.push(' switch (mapFieldNum) {')
lines.push(' case 1:')
lines.push(' k = ' + readerCall(mapKeyType(f, int64Mode) === 'string' ? 'string' : 'int32', int64Mode, 'r'))
lines.push(' break')
lines.push(' case 2:')
if (f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)) {
lines.push(' {')
lines.push(' const msgLen = r.uint32()')
lines.push(` v = new ${alias}()`)
lines.push(' v.decodeFrom(r, msgLen)')
lines.push(' }')
} else {
lines.push(' v = ' + readerCall(f.type.replace('map<', '').replace('>', '').split(',')[1].trim(), int64Mode, 'r'))
}
lines.push(' break')
lines.push(' default:')
lines.push(' r.skipType(mapTag & 7)')
lines.push(' }')
lines.push(' }')
lines.push(` this.${fnameSafe}.set(k, v)`)
lines.push(' break')
lines.push(' }')
} else if (f.repeated) {
const isMessage = f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)
if (isMessage) {
lines.push(' {')
lines.push(' const msgLen = r.uint32()')
lines.push(` const msg = new ${alias}()`)
lines.push(' msg.decodeFrom(r, msgLen)')
lines.push(` this.${fnameSafe}.push(msg)`)
lines.push(' }')
} else {
const rm = readerMethodName(f, int64Mode)
lines.push(` this.${fnameSafe}.push(r.${rm}()) // ⚠️ push, not replace!`)
}
lines.push(' break')
lines.push(' }')
} else {
const isMessage = f.resolvedType && !(f.resolvedType instanceof protobuf.Enum)
if (isMessage) {
lines.push(' {')
lines.push(' const msgLen = r.uint32()')
if (f.partOf) {
lines.push(` const msg = new ${alias}()`)
lines.push(' msg.decodeFrom(r, msgLen)')
lines.push(` this.${f.partOf.name} = { kind: '${f.name}', ${f.name}: msg }`)
} else {
lines.push(` this.${fnameSafe} = new ${alias}()`)
lines.push(` this.${fnameSafe}.decodeFrom(r, msgLen)`)
}
lines.push(' }')
} else {
const rm = readerMethodName(f, int64Mode)
if (f.partOf) {
lines.push(` this.${f.partOf.name} = { kind: '${f.name}', ${f.name}: r.${rm}() }`)
} else {
lines.push(` this.${fnameSafe} = r.${rm}()`)
}
}
lines.push(' break')
lines.push(' }')
}
}
lines.push(' default: {')
lines.push(' // Save unknown fields')
lines.push(' const wireType = tag & 7')
lines.push(' const start = r.pos')
lines.push(' r.skipType(wireType)')
lines.push(' this.unknownFields.append(r.view(start, r.pos))')
lines.push(' }')
lines.push(' }')
lines.push(' }')
lines.push(' }')
return lines
}
* Helper: capitalize first letter
*/
function capitalize(str) {
if (!str) return ''
if (str === 'int32') return 'Int32'
if (str === 'int64') return 'Int64'
if (str === 'uint32') return 'Uint32'
if (str === 'uint64') return 'Uint64'
if (str === 'sint32') return 'Sint32'
if (str === 'sint64') return 'Sint64'
if (str === 'fixed32') return 'Fixed32'
if (str === 'fixed64') return 'Fixed64'
if (str === 'sfixed32') return 'Sfixed32'
if (str === 'sfixed64') return 'Sfixed64'
if (str === 'string') return 'String'
if (str === 'bytes') return 'Bytes'
if (str === 'bool') return 'Bool'
if (str === 'float') return 'Float'
if (str === 'double') return 'Double'
return str.charAt(0).toUpperCase() + str.slice(1)
}
* Helper: get default value for a field
*/
function getDefaultValue(field, int64Mode) {
switch (field.type) {
case 'string': return `''`
case 'bool': return 'false'
case 'bytes': return 'new Uint8Array(0)'
case 'int32':
case 'uint32':
case 'sint32':
case 'fixed32':
case 'sfixed32':
case 'float':
case 'double':
return '0'
case 'int64':
case 'uint64':
case 'sint64':
case 'fixed64':
case 'sfixed64':
return int64Mode === 'number' ? '0' : '0n'
default:
return '0'
}
}
* Helper: get condition to check if field is not default
*/
function getDefaultCheckCondition(field, fieldName, defaultValue, int64Mode) {
switch (field.type) {
case 'string':
return `${fieldName} !== ''`
case 'bool':
return `${fieldName} !== false`
case 'bytes':
return `${fieldName}.length > 0`
default:
return `${fieldName} !== ${defaultValue}`
}
}
* Helper: generate reader method call
*/
function readerCall(type, int64Mode, readerVar) {
const rm = readerMethodName({type}, int64Mode)
return `${readerVar}.${rm}()`
}
module.exports = {
generateTraverseMethod,
generateDecodeFromMethod
}