深入掌握 @typespec/xml 装饰器:从属性映射到 XML 命名空间的完整实战指南
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
导读
@typespec/xml是 TypeSpec 官方提供的 XML 编码库,它通过一组精炼的装饰器(Decorator)控制 TypeSpec 模型在序列化为 XML 时的表现形态:属性是编码为 XML 特性(attribute)还是子节点(node)、元素与属性的最终名称、命名空间(namespace)与前缀(prefix)如何声明,以及列表/文本内容是否需要包装节点。本文以官方参考文档 decorators.md 为核心骨架,结合@typespec/xml包的源码实现与测试用例,逐一拆解 5 个装饰器的签名、约束、底层原理与实战用法,读完即可在自己的 TypeSpec 定义中精确控制 XML 输出结构。
一、装饰器总览
@typespec/xml通过main.tsp(见 main.tsp)聚合导入types.tsp与decorators.tsp,对外暴露全部装饰器。其 TypeScript 侧的声明与实现分别位于 decorators.tsp(TypeSpec 声明)与 decorators.ts(运行时实现)。
官方参考文档共收录 5 个装饰器,汇总如下:
| 装饰器 | 目标类型(Target) | 参数 | 作用 |
|---|---|---|---|
@TypeSpec.Xml.name | unknown(任意类型) | name: valueof string | 指定 XML 元素/特性的名称,等价于@encodedName("application/xml", value) |
@TypeSpec.Xml.attribute | ModelProperty | 无 | 将目标属性编码为 XML 特性而非子节点 |
@TypeSpec.Xml.unwrapped | ModelProperty | 无 | 不为目标属性创建包装节点,用于扁平化数组或内联原始文本 |
@TypeSpec.Xml.ns | unknown | ns: string \| EnumMember,prefix?: valueof string | 为元素指定 XML 命名空间与前缀 |
@TypeSpec.Xml.nsDeclarations | Enum | 无 | 标记某个枚举为 XML 命名空间声明表,配合@ns使用 |
在 TypeSpec 源文件中,装饰器既可以写全限定名@TypeSpec.Xml.attribute,也可以在使用using TypeSpec.Xml;导入后简写为@attribute。下文示例沿用参考文档与官方测试中的简写形式。
二、@name:精确控制 XML 元素与特性名称
2.1 签名与语义
@TypeSpec.Xml.name(name: valueof string)@name的语义与@encodedName("application/xml", value)完全等价,即:声明该类型在 XML 编码下的最终名称。它的目标(Target)是unknown,意味着可以作用在模型、模型属性、标量等任意类型上。
官方参考文档中的完整示例:
@name("XmlBook") model Book { @name("XmlId") id: string; @encodedName("application/xml", "XmlName") name: string; content: string; }序列化结果:
<XmlBook> <XmlId>string</XmlId> <XmlName>string</XmlName> <content>string</content> </XmlBook>注意示例中id使用@name("XmlId")、name使用@encodedName("application/xml", "XmlName"),二者产出完全相同的 XML 元素名,直观印证了两者的等价性;未标注的content则保留原始属性名。
2.2 源码实现:一行转调
@name的实现非常简洁——它本质上是标准@encodedName装饰器在application/xml媒体类型上的语法糖。见 decorators.ts:
export const $name: NameDecorator = (context, target, name) => { context.call($encodedName, target, "application/xml", name); };2.3 测试验证:多目标支持
官方测试 decorators.test.ts 使用it.each参数化用例,分别验证@name作用于模型、模型属性与标量三种目标时,resolveEncodedName(program, type, "application/xml")均能解析出指定的XmlName,确认了其Target: unknown的通用性:
it.each([ ["model", `@test @Xml.name("XmlName") model Blob {}`], ["model prop", `model Blob {@Xml.name("XmlName") @test title:string}`], ["scalar", `@Xml.name("XmlName") @test scalar Blob extends string;`], ])("%s", async (_, code) => { const result = await runner.compile(t.code`${code}`); const curr = (result.Blob || result.title) as Model; expect(resolveEncodedName(runner.program, curr, "application/xml")).toEqual("XmlName"); });三、@attribute:把属性变成 XML 特性
3.1 默认行为 vs 特性编码
XML 中存在两种承载数据的方式:元素子节点(node)与特性(attribute)。默认情况下,模型属性会被编码为子节点。参考文档给出的对照示例:
默认编码:
model Blob { id: string; }<Blob> <id>abcdef</id> </Blob>使用@attribute之后:
model Blob { @attribute id: string; }<Blob id="abcdef"> </Blob>同一份数据,前者是<Blob>下的<id>子节点,后者则是Blob标签上的id="abcdef"特性,XML 文档结构完全不同。
3.2 目标与约束
- Target:
ModelProperty,只能作用于模型属性; - Parameters:无参数;
- 互斥约束:参考文档明确指出它不能与
@unwrapped同时使用(详见下文第 4 节)。
3.3 源码实现:状态标记
@attribute的实现是把目标属性登记进编译器的状态集合(state set)。见 decorators.ts:
export const $attribute: AttributeDecorator = (context, target) => { context.program.stateSet(XmlStateKeys.attribute).add(target); }; /** Check if the given property should be serialized as an attribute instead of a node. */ export function isAttribute(program: Program, target: ModelProperty): boolean { return program.stateSet(XmlStateKeys.attribute).has(target); }配套导出的isAttribute()查询函数会被序列化器(各语言 emitter)在输出阶段调用,判断某个属性应当渲染为特性。状态键attribute在库定义 lib.ts 中登记描述为 "Mark a model property to be serialized as xml attribute"。
3.4 测试验证
decorators.test.ts 验证:被@Xml.attribute标注的属性isAttribute(...)返回true,未标注的返回false。
四、@unwrapped:去掉包装节点
4.1 作用与互斥约束
@unwrapped指定目标属性不创建包装节点,常用于两种场景:
- 扁平化数组节点:让数组元素直接成为外层模型节点的子节点;
- 内联原始文本:让字符串内容直接落在模型节点内,而不是嵌套一层子节点。
参考文档明确:它不能与@attribute同时使用(一个属性要么是特性,要么去掉包装,两者语义冲突)。
4.2 数组属性:默认 vs 解包
默认情况下,数组属性会产生一层ItemsTags之类的包装节点(包装节点名称由数组属性名决定):
model Pet { tags: Tag[]; }<XmlPet> <ItemsTags> <XmlTag> <name>string</name> </XmlTag> </ItemsTags> </XmlPet>加上@unwrapped后,包装节点被移除,Tag元素直接挂在XmlPet下:
model Pet { @unwrapped tags: Tag[]; }<XmlPet> <XmlTag> <name>string</name> </XmlTag> </XmlPet>4.3 字符串属性:默认 vs 内联文本
对于字符串属性,默认会生成一层内容节点:
model BlobName { content: string; }<BlobName> <content> abcdef </content> </BlobName>使用@unwrapped后,字符串内容直接成为模型节点的文本内容:
model BlobName { @unwrapped content: string; }<BlobName> abcdef </BlobName>4.4 源码实现与测试
与@attribute对称,@unwrapped同样基于状态集合实现,见 decorators.ts:
export const $unwrapped: UnwrappedDecorator = (context, target) => { context.program.stateSet(XmlStateKeys.unwrapped).add(target); }; /** Check if the given property should be unwrapped in the XML containing node. */ export function isUnwrapped(program: Program, target: ModelProperty): boolean { return program.stateSet(XmlStateKeys.unwrapped).has(target); }测试 decorators.test.ts 验证了isUnwrapped()对标注/未标注属性的判别。库定义中状态键描述为 "Mark a model property to be serialized without a node wrapping the content"(见 lib.ts)。
五、@ns与@nsDeclarations:XML 命名空间体系
XML 命名空间是跨文档共享元素定义的基础设施,@typespec/xml提供了两套声明方式。
5.1@ns的两种用法
签名如下(参考文档原样):
@TypeSpec.Xml.ns(ns: string | EnumMember, prefix?: valueof string)| 参数 | 类型 | 说明 |
|---|---|---|
ns | string \| EnumMember | 命名空间 URI,或一个被@nsDeclaration装饰的枚举的成员 |
prefix | valueof string | 命名空间前缀;当ns以字符串传入时为必填 |
用法一:字符串 URI + 前缀
@ns("https://example.com/ns1", "ns1") model Foo { @ns("https://example.com/ns1", "ns1") bar: string; @ns("https://example.com/ns2", "ns2") bar: string; }用法二:引用@nsDeclarations枚举成员
@Xml.nsDeclarations enum Namespaces { ns1: "https://example.com/ns1", ns2: "https://example.com/ns2", } @Xml.ns(Namespaces.ns1) model Foo { @Xml.ns(Namespaces.ns1) bar: string; @Xml.ns(Namespaces.ns2) bar: string; }采用枚举方式时,前缀自动取枚举成员名(如ns1、ns2),无需也不能再手动传前缀。
5.2@nsDeclarations:命名空间声明表
@TypeSpec.Xml.nsDeclarations- Target:
Enum; - Parameters:无;
- 作用:将枚举标记为 XML 命名空间声明表。枚举成员的值必须是命名空间 URI 字符串,成员名即前缀。
5.3 源码实现与 5 类诊断规则
@ns的参数处理集中在 decorators.ts 的getData()中,其分支逻辑与错误校验清晰对应着参考文档的约束:
- 字符串 URI 必须携带前缀——缺失时抛出
ns-missing-prefix错误:"When using a string namespace you must provide a prefix as the 2nd argument."; - 枚举成员必须来自
@nsDeclarations枚举——否则抛出ns-enum-not-declaration:"Enum member used as namespace must be part of an enum marked with @nsDeclaration."; - 枚举成员值必须是字符串 URI——值为空或数字时抛出
invalid-ns-declaration-member:"Enum membernamemust have a value that is the XML namespace url."; - 枚举方式禁止再传前缀——抛出
prefix-not-allowed:"@ns decorator cannot have the prefix parameter set when using an enum member."; - 命名空间必须是合法 URI——通过
new URL(namespace)校验(见 validateNamespaceIsUri),非法时抛出ns-not-uri:"Namespacenamespaceis not a valid URI."。
上述诊断信息全部登记在库定义 lib.ts 中,错误码格式为@typespec/xml/<code>。
解析成功后,命名空间以{ namespace, prefix }结构存入状态映射(state map),数据模型即 types.ts 中的XmlNamespace接口:
export interface XmlNamespace { /** Namespace name */ readonly namespace: string; /** Namespace prefix */ readonly prefix: string; }外部查询通过getNs(program, target)获取(见 decorators.ts)。
5.4 测试验证:命名空间不向子级传递
decorators.test.ts 对@ns进行了全面覆盖,值得注意的用例包括:
- 字符串方式与枚举方式都能正确解析出
{ namespace, prefix }; - 命名空间不会自动向子级传递:在
Blob模型上标注@ns,其属性id查询getNs返回undefined,说明命名空间需要逐级显式声明; - 上述 5 类错误场景均有对应诊断测试,例如缺少第二参、给枚举方式传前缀、命名空间不是合法 URL 等。
六、综合实战示例
将 5 个装饰器组合使用,可以得到一份完整的 XML 映射模型(综合参考文档示例整理):
using TypeSpec.Xml; @nsDeclarations enum Namespaces { storage: "https://example.com/storage", } @name("XmlBook") @ns(Namespaces.storage) model Book { @name("XmlId") @attribute id: string; @name("XmlName") name: string; @unwrapped tags: string[]; @unwrapped content: string; }对应输出 XML 的形态如下(示意):
<XmlBook xmlns:storage="https://example.com/storage" storage:XmlId="string"> <storage:XmlName>string</storage:XmlName> <string>tag1</string> <string>tag2</string> string </XmlBook>要点回顾:
@name负责元素/特性命名,@attribute让id成为特性;@ns+@nsDeclarations声明命名空间与前缀;@unwrapped分别将数组元素直接平铺、将文本内容内联进模型节点。
七、验证与运行方式
仓库为@typespec/xml提供了完整的单元测试,核心位于 decorators.test.ts,涵盖@name、@attribute、@unwrapped、@ns、@nsDeclarations的全部行为与诊断分支。测试基于 test-host.ts 构建的Tester实例,并使用@typespec/compiler/testing提供的t测试辅助与expectDiagnostics断言工具。感兴趣的读者可以在仓库根目录执行对应包的测试命令(参见 package.json)进行本地验证。
此外,@typespec/xml还定义了 XML 编码枚举TypeSpec.Xml.Encoding(xmlDateTime、xmlDate、xmlTime、xmlDuration、xmlBase64Binary,见 types.tsp),配合编译器内置的@encode装饰器使用,相关编码实现位于 encoding.ts,可作为理解该库完整能力的下一步阅读材料。
结语
@typespec/xml的装饰器设计遵循"少而精"的原则:@name统一命名、@attribute控制特性形态、@unwrapped控制节点层级、@ns/@nsDeclarations管理命名空间。理解它们各自的 Target、参数与底层状态存储机制(state set / state map),以及 5 类编译期诊断规则,就能在编写 TypeSpec 时精准预测 XML 输出,为各语言 emitter 的正确序列化打下坚实基础。完整的签名、参数表与示例可随时查阅官方参考文档 decorators.md,以及源码 decorators.ts 与测试 decorators.test.ts。
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考