Files
orm-cj/src/macros/seed_macro.cj
T

88 lines
3.4 KiB
Plaintext

/*
* 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 参数类型>` 转型后调用用户 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()]
}