jsonschema生成_用F#手写TypeScript转C#类型绑定生成器

(给DotNet加星标,提升.Net技能)

转自:hez2010 cnblogs.com/hez2010/p/12246841.html

前言

我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代码,然后如果是 UI 组件的话,我们需要将其封装到一个 WebView 里面,然后通过 JavaScript 和 C# 的互操作功能来调用该组件的各种方法,支持该组件的各种事件等等。

但是这是一个苦力活,尤其是类型翻译这一步。

这个是我最近在帮助维护一个开源 UWP 项目 monaco-editor-uwp 所需要的,该项目将微软的 monaco 编辑器封装成了 UWP 组件。

然而它的 monaco.d.ts 足足有 1.5 mb,并且 API 经常会变化,如果人工翻译,不仅工作量十分大,还可能会漏掉新的变化,但是如果有一个自动生成器的话,那么人工的工作就会少很多。

目前 GitHub 上面有一个叫做 QuickType 的项目,但是这个项目对 TypeScript 的支持极其有限,仍然停留在 TypeScript 3.2,而且遇到不认识的类型就会报错,比如 DOM 类型等等。

因此我决定手写一个代码生成器 TypedocConverter:https://github.com/hez2010/TypedocConverter

构思

本来是打算从 TypeScript 词法和语义分析开始做的,但是发现有一个叫做 Typedoc 的项目已经帮我们完成了这一步,而且支持输出 JSON schema,那么剩下的事情就简单了:我们只需要将 TypeScript 的 AST 转换成 C# 的 AST,然后再将 AST 还原成代码即可。

那么话不多说,这就开写。

构建 Typescipt AST 类型绑定

借助于 F# 更加强大的类型系统,类型的声明和使用非常简单,并且具有完善的recursive pattern。pattern matching、option types 等支持,这也是该项目选用 F# 而不是 C# 的原因,虽然 C# 也支持这些,也有一定的 FP 能力,但是它还是偏 OOP,写起来会有很多的样板代码,非常的繁琐。

我们将 Typescipt 的类型绑定定义到 Definition.fs 中,这一步直接将 Typedoc 的定义翻译到 F# 即可:

首先是 ReflectionKind 枚举,该枚举表示了 JSON Schema 中各节点的类型:

type ReflectionKind = 
| Global = 0
| ExternalModule = 1
| Module = 2
| Enum = 4
| EnumMember = 16
| Variable = 32
| Function = 64
| Class = 128
| Interface = 256
| Constructor = 512
| Property = 1024
| Method = 2048
| CallSignature = 4096
| IndexSignature = 8192
| ConstructorSignature = 16384
| Parameter = 32768
| TypeLiteral = 65536
| TypeParameter = 131072
| Accessor = 262144
| GetSignature = 524288
| SetSignature = 1048576
| ObjectLiteral = 2097152
| TypeAlias = 4194304
| Event = 8388608
| Reference = 16777216

然后是类型修饰标志 ReflectionFlags,注意该 record 所有的成员都是 option 的

type ReflectionFlags = {
    
IsPrivate: bool option
IsProtected: bool option
IsPublic: bool option
IsStatic: bool option
IsExported: bool option
IsExternal: bool option
IsOptional: bool option
IsReset: bool option
HasExportAssignment: bool option
IsConstructorProperty: bool option
IsAbstract: bool option
IsConst: bool option
IsLet: bool option
}

然后到了我们的 Reflection,由于每一种类型的 Reflection 都可以由 ReflectionKind 来区分,因此我选择将所有类型的 Reflection 合并成为一个 record,而不是采用 Union Types,因为后者虽然看上去清晰,但是在实际 parse AST 的时候会需要大量 pattern matching 的代码。

由于部分 records 相互引用,因此我们使用 and 来定义 recursive records。

type Reflection = {
    
Id: int
Name: string
OriginalName: string
Kind: ReflectionKind
KindString: string option
Flags: ReflectionFlags
Parent: Reflection option
Comment: Comment option
Sources: SourceReference list option
Decorators: Decorator option
Decorates: Type list option
Url: string option
Anchor: string option
HasOwnDocument: bool option
CssClasses: string option
DefaultValue: string option
Type: Type option
TypeParameter: Reflection list option
Signatures: Reflection list option
IndexSignature: Reflection list option
GetSignature: Reflection list option
SetSignature: Reflection list option
Overwrites: Type option
InheritedFrom: Type option
ImplementationOf: Type option
ExtendedTypes: Type list option
ExtendedBy: Type list option
ImplementedTypes: Type list option
ImplementedBy: Type list option
TypeHierarchy: DeclarationHierarchy option
Children: Reflection list option
Groups: ReflectionGroup list option
Categories: ReflectionCategory list option
Reflections: Map<int, Reflection> option
Directory: SourceDirectory option
Files: SourceFile list option
Readme: string option
PackageInfo: obj option
Parameters: Reflection list option
}
and DeclarationHierarchy = {
Type: Type list
Next: DeclarationHierarchy option
IsTarget: bool option
}
and Type = {
Type: string
Id: int option
Name: string option
ElementType: Type option
Value: string option
Types: Type list option
TypeArguments: Type list option
Constraint: Type option
Declaration: Reflection option
}
and Decorator = {
Name: string
Type: Type option
Arguments: obj option
}
and ReflectionGroup = {
Title: string
Kind: ReflectionKind
Children: int list
CssClasses: string option
AllChildrenHaveOwnDocument: bool option
AllChildrenAreInherited: bool option
AllChildrenArePrivate: bool option
AllChildrenAreProtectedOrPrivate: bool option
AllChildrenAreExternal: bool option
SomeChildrenAreExported: bool option
Categories: ReflectionCategory list option
}
and ReflectionCategory = {
Title: string
Children: int list
AllChildrenHaveOwnDocument: bool option
}
and SourceDirectory = {
Parent: SourceDirectory option
Directories: Map<string, SourceDirectory>
Groups: ReflectionGroup list option
Files: SourceFile list
Name: string option
DirName: string option
Url: string option
}
and SourceFile = {
FullFileName: string
FileName: string
Name: string
Url: string option
Parent: SourceDirectory option
Reflections: Reflection list option
Groups: ReflectionGroup list option
}
and SourceReference = {
File: SourceFile option
FileName: string
Line: int
Character: int
Url: string option
}
and Comment = {
ShortText: string
Text: string option
Returns: string option
Tags: CommentTag list option
}
and CommentTag = {
TagName: string
ParentName: string
Text: string
}

这样,我们就简单的完成了类型绑定的翻译,接下来要做的就是将 Typedoc 生成的 JSON 反序列化成我们所需要的东西即可。

反序列化

虽然想着好像一切都很顺利,但是实际上 System.Text.Json、Newtonsoft.JSON 等均不支持 F# 的 option types,所需我们还需要一个 JsonConverter 处理 option types。

本项目采用 Newtonsoft.Json,因为 System.Text.Json 目前尚不成熟。得益于 F# 对 OOP 的兼容,我们可以很容易的实现一个 OptionConverter。

type OptionConverter() =
inherit JsonConverter()override __.CanConvert(objectType: Type) : bool =
match objectType.IsGenericType with
| false -> false
| true -> typedefof<_ option> = objectType.GetGenericTypeDefinition()override __.WriteJson(writer: JsonWriter, value: obj, serializer: JsonSerializer) : unit =
serializer.Serialize(writer, if isNull value then nullelse let _, fields = FSharpValue.GetUnionFields(value, value.GetType())
fields.[0]
)override __.ReadJson(reader: JsonReader, objectType: Type, _existingValue: obj, serializer: JsonSerializer) : obj = let innerType = objectType.GetGenericArguments().[0]let value =
serializer.Deserialize(
reader, if innerType.IsValueType then (typedefof<_ nullable>).MakeGenericType([|innerType|])else innerType
)let cases = FSharpType.GetUnionCases objectTypeif isNull value then FSharpValue.MakeUnion(cases.[0], [||])else FSharpValue.MakeUnion(cases.[1], [|value|])

这样所有的工作就完成了。

我们可以去 monaco-editor 仓库下载 monaco.d.ts 测试一下我们的 JSON Schema deserializer,可以发现 JSON Sechma 都被正确地反序列化了。

bb137174b7b9e76226fac818641b773c.png

反序列化结果

构建 C# AST 类型

当然,此 "AST" 非彼 AST,我们没有必要其细化到语句层面,因为我们只是要写一个简单的代码生成器,我们只需要构建实体结构即可。

我们将实体结构定义到 Entity.fs 中,在此我们只需支持 interface、class、enum 即可,对于 class 和 interface,我们只需要支持 method、property 和 event 就足够了。

当然,代码中存在泛型的可能,这一点我们也需要考虑。

type EntityBodyType = {
    
Type: string
Name: string option
InnerTypes: EntityBodyType list
}
type EntityMethod = {
Comment: string
Modifier: string list
Type: EntityBodyType
Name: string
TypeParameter: string list
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值