/* * Copyright (c) 2025 SimcuTeam. All rights reserved. * @Seed 类级属性宏:把标注的普通类展开为数据填充(自动注册,无需手写注册表)。 * * 用法: * @Seed["初始化游戏服务器配置"] * public class InitGames { * public func run(ctx: DataContext): Unit { * // 用应用自己的 DbContext 直接操作 ORM,自行保证幂等 * } * } * * 展开行为: * - seedId 取类名(如 InitGames),属性参数仅作为描述 * - 保留原类(用户类不继承任何基类,run 参数类型为应用自己的 DbContext) * - 生成包装类 <: simcu::orm.seeds.Seed,override run(ctx: Any), * 内部 new 用户类并 `ctx as ` 转型后调用用户 run * - 生成顶层 let registerSeed(包装类实例),模块加载时自动注册 * * 约束: * - 属性参数为描述(不能包含双引号/反斜杠/换行) * - 类必须提供单参数方法 run(ctx) * - 应用侧需 import simcu::orm.macros.* 与 simcu::orm.seeds.* */ macro package simcu::orm.macros import std.ast.* import std.collection.* /// @Seed 属性宏:属性(attrTokens)为 ["描述"],input 为类声明,seedId 取类名。 public macro Seed(attrTokens: Tokens, inputTokens: Tokens): Tokens { let description = parseAttr(attrTokens.toString()) if (description.contains("\"") || description.contains("\\") || description.contains("\n")) { throw ASTException("@Seed 参数描述不能包含双引号、反斜杠或换行") } let cd = (parseDecl(inputTokens) as ClassDecl).getOrThrow() // 找 run 方法并提取第一个参数类型(任意类型,由包装类在应用侧转型) var ctxType: ?String = None for (d in cd.body.decls) { if (let fd: FuncDecl <- d) { if (fd.identifier.value == "run") { if (fd.funcParams.size == 1) { ctxType = Some(fd.funcParams[0].paramType.toTokens().toString()) } } } } let paramType = match (ctxType) { case Some(t) => t case None => throw ASTException("@Seed 类必须提供单参数方法 run(ctx),参数为应用自己的 DbContext 类型") } let className = cd.identifier.value let seedId = className let wrapperName = "${className}_SeedWrapper" let regName = "_seedRegistration_${className}" let code = """ public class ${wrapperName} <: Seed { public init() { super("${seedId}", "${description}") } public override func run(ctx: Any): Unit { let inst = ${className}() inst.run((ctx as ${paramType}).getOrThrow()) } } let ${regName} = registerSeed(${wrapperName}()) """ // inputTokens 末尾 } 与展开内容之间需换行分隔(cangjieLex 保留 NL) inputTokens + cangjieLex("\n" + code) } /// 解析属性 tokens 文本(形如 "描述")中的字符串字面量。 /// 按引号对子串定位后字节切片(引号为 ASCII,边界安全,中文等多字节内容原样保留)。 private func parseAttr(text: String): String { let open = text.indexOf("\"") if (open.isNone()) { throw ASTException("@Seed 需要一个字符串属性: @Seed[\"描述\"]") } let start = open.getOrThrow() + 1 let close = text.indexOf("\"", start) if (close.isNone()) { throw ASTException("@Seed 需要一个字符串属性: @Seed[\"描述\"]") } text[start..close.getOrThrow()] }