咱們常常會遇到這樣的事情:有時候咱們找到了一個庫,可是這個庫是用 TypeScript 寫的,可是咱們想在 C# 調用,因而咱們須要設法將原來的 TypeScript 類型聲明翻譯成 C# 的代碼,而後若是是 UI 組件的話,咱們須要將其封裝到一個 WebView 裏面,而後經過 JavaScript 和 C# 的互操做功能來調用該組件的各類方法,支持該組件的各類事件等等。node
可是這是一個苦力活,尤爲是類型翻譯這一步。git
這個是我最近在幫助維護一個開源 UWP 項目 monaco-editor-uwp 所須要的,該項目將微軟的 monaco 編輯器封裝成了 UWP 組件。github
然而它的 monaco.d.ts 足足有 1.5 mb,而且 API 常常會變化,若是人工翻譯,不只工做量十分大,還可能會漏掉新的變化,可是若是有一個自動生成器的話,那麼人工的工做就會少不少。typescript
目前 GitHub 上面有一個叫作 QuickType 的項目,可是這個項目對 TypeScript 的支持極其有限,仍然停留在 TypeScript 3.2,並且遇到不認識的類型就會報錯,好比 DOM 類型等等。數組
所以我決定手寫一個代碼生成器 TypedocConverter:https://github.com/hez2010/TypedocConverterapp
原本是打算從 TypeScript 詞法和語義分析開始作的,可是發現有一個叫作 Typedoc 的項目已經幫咱們完成了這一步,並且支持輸出 JSON schema,那麼剩下的事情就簡單了:咱們只須要將 TypeScript 的 AST 轉換成 C# 的 AST,而後再將 AST 還原成代碼便可。編輯器
那麼話很少說,這就開寫。ide
藉助於 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 null else 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 objectType if isNull value then FSharpValue.MakeUnion(cases.[0], [||]) else FSharpValue.MakeUnion(cases.[1], [|value|])
這樣全部的工做就完成了。
咱們能夠去 monaco-editor 倉庫下載 monaco.d.ts 測試一下咱們的 JSON Schema deserializer,能夠發現 JSON Sechma 都被正確地反序列化了。
反序列化結果
固然,此 "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 Parameter: EntityBodyType list } type EntityProperty = { Comment: string Modifier: string list Name: string Type: EntityBodyType WithGet: bool WithSet: bool IsOptional: bool InitialValue: string option } type EntityEvent = { Comment: string Modifier: string list DelegateType: EntityBodyType Name: string IsOptional: bool } type EntityEnum = { Comment: string Name: string Value: int64 option } type EntityType = | Interface | Class | Enum | StringEnum type Entity = { Namespace: string Name: string Comment: string Methods: EntityMethod list Properties: EntityProperty list Events: EntityEvent list Enums: EntityEnum list InheritedFrom: EntityBodyType list Type: EntityType TypeParameter: string list Modifier: string list }
文檔化註釋也是少不了的東西,能極大方便開發者後續使用生成的類型綁定,而無需參照原 typescript 類型聲明上的註釋。
代碼很簡單,只須要將文本處理成 xml 便可。
let escapeSymbols (text: string) = if isNull text then "" else text .Replace("&", "&") .Replace("<", "<") .Replace(">", ">") let toCommentText (text: string) = if isNull text then "" else text.Split "\n" |> Array.map (fun t -> "/// " + escapeSymbols t) |> Array.reduce(fun accu next -> accu + "\n" + next) let getXmlDocComment (comment: Comment) = let prefix = "/// <summary>\n" let suffix = "\n/// </summary>" let summary = match comment.Text with | Some text -> prefix + toCommentText comment.ShortText + toCommentText text + suffix | _ -> match comment.ShortText with | "" -> "" | _ -> prefix + toCommentText comment.ShortText + suffix let returns = match comment.Returns with | Some text -> "\n/// <returns>\n" + toCommentText text + "\n/// </returns>" | _ -> "" summary + returns
Typescript 的類型系統較爲靈活,包括 union types、intersect types 等等,這些即便是目前的 C# 8 都不能直接表達,須要等到 C# 9 才行。固然咱們能夠生成一個 struct 併爲其編寫隱式轉換操做符重載,支持 union types,可是目前還沒有實現,咱們就先用 union types 中的第一個類型代替,而對於 intersect types,咱們姑且先使用 object。
然而 union types 有一個特殊狀況:string literals types alias。就是這樣的東西:
type Size = "XS" | "S" | "M" | "L" | "XL";
即純 string 值組合的 type alias,這個咱們仍是有必要支持的,由於在 typescript 中用的很是普遍。
C# 在沒有對應語法的時候要怎麼支持呢?很簡單,咱們建立一個 enum,該 enum 包含該類型中的全部元素,而後咱們爲其編寫 JsonConverter,這樣就能確保序列化後,typescript 方能正確識別類型,而在 C# 又有 type sound 的編碼體驗。
另外,咱們須要提供一些經常使用的類型轉換:
Array<T>
-> T[]
Set<T>
-> System.Collections.Generic.ISet<T>
Map<T>
-> System.Collections.Generic.IDictionary<T>
Promise<T>
-> System.Threading.Tasks.Task<T>
System.Func<T...>
, System.Action<T...>
Uint32Array
<void>
,咱們須要解除泛型,即 T<void>
-> T
那麼實現以下:
let rec getType (typeInfo: Type): EntityBodyType = let genericType = match typeInfo.Type with | "intrinsic" -> match typeInfo.Name with | Some name -> match name with | "number" -> { Type = "double"; InnerTypes = []; Name = None } | "boolean" -> { Type = "bool"; InnerTypes = []; Name = None } | "string" -> { Type = "string"; InnerTypes = []; Name = None } | "void" -> { Type = "void"; InnerTypes = []; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } | "reference" | "typeParameter" -> match typeInfo.Name with | Some name -> match name with | "Promise" -> { Type = "System.Threading.Tasks.Task"; InnerTypes = []; Name = None } | "Set" -> { Type = "System.Collections.Generic.ISet"; InnerTypes = []; Name = None } | "Map" -> { Type = "System.Collections.Generic.IDictionary"; InnerTypes = []; Name = None } | "Array" -> { Type = "System.Array"; InnerTypes = []; Name = None } | "BigUint64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ulong"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Uint32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "uint"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Uint16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "ushort"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Uint8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "byte"; InnerTypes = [ ]; Name = None };]; Name = None }; | "BigInt64Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "long"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Int32Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "int"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Int16Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "short"; InnerTypes = [ ]; Name = None };]; Name = None }; | "Int8Array" -> { Type = "System.Array"; InnerTypes = [{ Type = "char"; InnerTypes = [ ]; Name = None };]; Name = None }; | "RegExp" -> { Type = "string"; InnerTypes = []; Name = None }; | x -> { Type = x; InnerTypes = []; Name = None }; | _ -> { Type = "object"; InnerTypes = []; Name = None } | "array" -> match typeInfo.ElementType with | Some elementType -> { Type = "System.Array"; InnerTypes = [getType elementType]; Name = None } | _ -> { Type = "System.Array"; InnerTypes = [{ Type = "object"; InnerTypes = []; Name = None }]; Name = None } | "stringLiteral" -> { Type = "string"; InnerTypes = []; Name = None } | "tuple" -> match typeInfo.Types with | Some innerTypes -> match innerTypes with | [] -> { Type = "object"; InnerTypes = []; Name = None } | _ -> { Type = "System.ValueTuple"; InnerTypes = innerTypes |> List.map getType; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } | "union" -> match typeInfo.Types with | Some innerTypes -> match innerTypes with | [] -> { Type = "object"; InnerTypes = []; Name = None } | _ -> printWarning ("Taking only the first type " + innerTypes.[0].Type + " for the entire union type.") getType innerTypes.[0] // TODO: generate unions | _ ->{ Type = "object"; InnerTypes = []; Name = None } | "intersection" -> { Type = "object"; InnerTypes = []; Name = None } // TODO: generate intersections | "reflection" -> match typeInfo.Declaration with | Some dec -> match dec.Signatures with | Some [signature] -> let paras = match signature.Parameters with | Some p -> p |> List.map (fun pi -> match pi.Type with | Some pt -> Some (getType pt) | _ -> None ) |> List.collect (fun x -> match x with | Some s -> [s] | _ -> [] ) | _ -> [] let rec getDelegateParas (paras: EntityBodyType list): EntityBodyType list = match paras with | [x] -> [{ Type = x.Type; InnerTypes = x.InnerTypes; Name = None }] | (front::tails) -> [front] @ getDelegateParas tails | _ -> [] let returnsType = match signature.Type with | Some t -> getType t | _ -> { Type = "void"; InnerTypes = []; Name = None } let typeParas = getDelegateParas paras match typeParas with | [] -> { Type = "System.Action"; InnerTypes = []; Name = None } | _ -> if returnsType.Type = "void" then { Type = "System.Action"; InnerTypes = typeParas; Name = None } else { Type = "System.Func"; InnerTypes = typeParas @ [returnsType]; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } | _ -> { Type = "object"; InnerTypes = []; Name = None } let mutable innerTypes = match typeInfo.TypeArguments with | Some args -> getGenericTypeArguments args | _ -> [] if genericType.Type = "System.Threading.Tasks.Task" then match innerTypes with | (front::_) -> if front.Type = "void" then innerTypes <- [] else () | _ -> () else () { Type = genericType.Type; Name = None; InnerTypes = if innerTypes = [] then genericType.InnerTypes else innerTypes; } and getGenericTypeArguments (typeInfos: Type list): EntityBodyType list = typeInfos |> List.map getType and getGenericTypeParameters (nodes: Reflection list) = // TODO: generate constaints let types = nodes |> List.where(fun x -> x.Kind = ReflectionKind.TypeParameter) |> List.map (fun x -> x.Name) types |> List.map (fun x -> {| Type = x; Constraint = "" |})
固然,目前尚不支持生成泛型約束,若是之後有時間的話會考慮添加。
例如 public
、private
、protected
、static
等等。這一步很簡單,直接將 ReflectionFlags 轉換一下便可,我的以爲使用 mutable 代碼會讓代碼變得很是不優雅,可是有的時候仍是須要用一下的,否則會極大地提升代碼的複雜度。
let getModifier (flags: ReflectionFlags) = let mutable modifier = [] match flags.IsPublic with | Some flag -> if flag then modifier <- modifier |> List.append [ "public" ] else () | _ -> () match flags.IsAbstract with | Some flag -> if flag then modifier <- modifier |> List.append [ "abstract" ] else () | _ -> () match flags.IsPrivate with | Some flag -> if flag then modifier <- modifier |> List.append [ "private" ] else () | _ -> () match flags.IsProtected with | Some flag -> if flag then modifier <- modifier |> List.append [ "protected" ] else () | _ -> () match flags.IsStatic with | Some flag -> if flag then modifier <- modifier |> List.append [ "static" ] else () | _ -> () modifier
終於到 parse 實體的部分了,咱們先從最簡單的作起:枚舉。 代碼很簡單,直接將原 AST 中的枚舉部分轉換一下便可。
let parseEnum (section: string) (node: Reflection): Entity = let values = match node.Children with | Some children -> children |> List.where (fun x -> x.Kind = ReflectionKind.EnumMember) | None -> [] { Type = EntityType.Enum; Namespace = if section = "" then "TypeDocGenerator" else section; Modifier = getModifier node.Flags; Name = node.Name Comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> "" Methods = []; Properties = []; Events = []; InheritedFrom = []; Enums = values |> List.map (fun x -> let comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> "" let mutable intValue = 0L match x.DefaultValue with // ????? | Some value -> if Int64.TryParse(value, &intValue) then { Comment = comment; Name = toPascalCase x.Name; Value = Some intValue; } else match getEnumReferencedValue values value x.Name with | Some t -> { Comment = comment; Name = x.Name; Value = Some (int64 t); } | _ -> { Comment = comment; Name = x.Name; Value = None; } | _ -> { Comment = comment; Name = x.Name; Value = None; } ); TypeParameter = [] }
你會注意到一個上面我有一處標了個 ?????
,這是在幹什麼呢?
其實,TypeScript 的 enum 是 recursive 的,也就意味着定義的時候,一個元素能夠引用另外一個元素,好比這樣:
enum MyEnum {
A = 1,
B = 2,
C = A
}
這個時候,咱們須要查找它引用的枚舉值,好比在上面的例子裏面,處理 C 的時候,須要將它的值 A 用真實值 1 代替。因此咱們還須要一個查找函數:
let rec getEnumReferencedValue (nodes: Reflection list) value name = match nodes |> List.where(fun x -> match x.DefaultValue with | Some v -> v <> value && not (name = x.Name) | _ -> true ) |> List.where(fun x -> x.Name = value) |> List.tryFind(fun x -> let mutable intValue = 0 match x.DefaultValue with | Some y -> Int32.TryParse(y, &intValue) | _ -> true ) with | Some t -> t.DefaultValue | _ -> None
這樣咱們的 Enum parser 就完成了。
下面到了重頭戲,interface 和 class 纔是類型綁定的關鍵。
咱們的函數簽名是這樣的:
let parseInterfaceAndClass (section: string) (node: Reflection) (isInterface: bool): Entity = ...
首先咱們從 Reflection 節點中查找並生成註釋、修飾、名稱、泛型參數、繼承關係、方法、屬性和事件:
let comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> "" let exts = (match node.ExtendedTypes with | Some types -> types |> List.map(fun x -> getType x) | _ -> []) @ (match node.ImplementedTypes with | Some types -> types |> List.map(fun x -> getType x) | _ -> []) let genericType = let types = match node.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None match types with | Some result -> result | _ -> [] let properties = match node.Children with | Some children -> if isInterface then children |> List.where(fun x -> x.Kind = ReflectionKind.Property) |> List.where(fun x -> x.InheritedFrom = None) // exclude inhreited properties |> List.where(fun x -> x.Overwrites = None) // exclude overrites properties else children |> List.where(fun x -> x.Kind = ReflectionKind.Property) | _ -> [] let events = match node.Children with | Some children -> if isInterface then children |> List.where(fun x -> x.Kind = ReflectionKind.Event) |> List.where(fun x -> x.InheritedFrom = None) // exclude inhreited events |> List.where(fun x -> x.Overwrites = None) // exclude overrites events else children |> List.where(fun x -> x.Kind = ReflectionKind.Event) | _ -> [] let methods = match node.Children with | Some children -> if isInterface then children |> List.where(fun x -> x.Kind = ReflectionKind.Method) |> List.where(fun x -> x.InheritedFrom = None) // exclude inhreited methods |> List.where(fun x -> x.Overwrites = None) // exclude overrites methods else children |> List.where(fun x -> x.Kind = ReflectionKind.Method) | _ -> []
有一點要注意,就是對於 interface 來講,子 interface 無需重複父 interface 的成員,所以須要排除。
而後咱們直接返回一個 record,表明該節點的實體便可。
{ Type = if isInterface then EntityType.Interface else EntityType.Class; Namespace = if section = "" then "TypedocConverter" else section; Name = node.Name; Comment = comment; Modifier = getModifier node.Flags; InheritedFrom = exts; Methods = methods |> List.map ( fun x -> let retType = match ( match x.Signatures with | Some signatures -> signatures |> List.where(fun x -> x.Kind = ReflectionKind.CallSignature) | _ -> []) with | [] -> { Type = "object"; InnerTypes = []; Name = None } | (front::_) -> match front.Type with | Some typeInfo -> getType typeInfo | _ -> { Type = "object"; InnerTypes = []; Name = None } let typeParameter = match x.Signatures with | Some (sigs::_) -> let types = match sigs.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None match types with | Some result -> result | _ -> [] | _ -> [] |> List.map (fun x -> x.Type) let parameters = getMethodParameters (match x.Signatures with | Some signatures -> signatures |> List.where(fun x -> x.Kind = ReflectionKind.CallSignature) |> List.map( fun x -> match x.Parameters with | Some parameters -> parameters |> List.where(fun p -> p.Kind = ReflectionKind.Parameter) | _ -> [] ) |> List.reduce(fun accu next -> accu @ next) | _ -> []) { Comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> "" Modifier = if isInterface then [] else getModifier x.Flags; Type = retType Name = x.Name TypeParameter = typeParameter Parameter = parameters } ); Events = events |> List.map ( fun x -> let paras = match x.Signatures with | Some sigs -> sigs |> List.where (fun x -> x.Kind = ReflectionKind.Event) |> List.map(fun x -> x.Parameters) |> List.collect (fun x -> match x with | Some paras -> paras | _ -> []) | _ -> [] { Name = x.Name; IsOptional = match x.Flags.IsOptional with | Some optional -> optional | _ -> false ; DelegateType = match paras with | (front::_) -> match front.Type with | Some typeInfo -> getType typeInfo | _ -> { Type = "System.Delegate"; Name = None; InnerTypes = [] } | _ -> match x.Type with | Some typeInfo -> getType typeInfo | _ -> { Type = "System.Delegate"; Name = None; InnerTypes = [] } ; Comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> "" ; Modifier = if isInterface then [] else getModifier x.Flags; } ); Properties = properties |> List.map ( fun x -> { Comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> "" Modifier = if isInterface then [] else getModifier x.Flags; Name = x.Name Type = match x.Type with | Some typeInfo -> getType typeInfo | _ -> { Type = "object"; Name = None; InnerTypes = [] } WithGet = true; WithSet = true; IsOptional = match x.Flags.IsOptional with | Some optional -> optional | _ -> false ; InitialValue = match x.DefaultValue with | Some value -> Some value | _ -> None } ); Enums = []; TypeParameter = genericType |> List.map(fun x -> x.Type); }
注意處理 event 的時候,委託的類型須要特殊處理一下。
還記得咱們最上面說的一種特殊的 union types 嗎?這裏就是處理純 string 的 type alias 的。
let parseUnionTypeAlias (section: string) (node: Reflection) (nodes: Type list): Entity list = let notStringLiteral = nodes |> List.tryFind(fun x -> x.Type <> "stringLiteral") let enums = match notStringLiteral with | Some _ -> printWarning ("Type alias " + node.Name + " is not supported.") [] | None -> nodes |> List.collect (fun x -> match x.Value with | Some value -> [{ Name = toPascalCase value Comment = "///<summary>\n" + toCommentText value + "\n///</summary>" Value = None }] | _ -> [] ) if enums = [] then [] else [ { Namespace = section Name = node.Name Comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> "" Methods = [] Events = [] Properties = [] Enums = enums InheritedFrom = [] Type = EntityType.StringEnum TypeParameter = [] Modifier = getModifier node.Flags } ] let parseTypeAlias (section: string) (node: Reflection): Entity list = let typeInfo = node.Type match typeInfo with | Some aliasType -> match aliasType.Type with | "union" -> match aliasType.Types with | Some types -> parseUnionTypeAlias section node types | _ -> printWarning ("Type alias " + node.Name + " is not supported.") [] | _ -> printWarning ("Type alias " + node.Name + " is not supported.") [] | _ -> []
咱們最後將以上 parsers 組合起來就 ojbk 了:
let rec parseNode (section: string) (node: Reflection): Entity list = match node.Kind with | ReflectionKind.Global -> match node.Children with | Some children -> parseNodes section children | _ -> [] | ReflectionKind.Module -> match node.Children with | Some children -> parseNodes (if section = "" then node.Name else section + "." + node.Name) children | _ -> [] | ReflectionKind.ExternalModule -> match node.Children with | Some children -> parseNodes section children | _ -> [] | ReflectionKind.Enum -> [parseEnum section node] | ReflectionKind.Interface -> [parseInterfaceAndClass section node true] | ReflectionKind.Class -> [parseInterfaceAndClass section node false] | ReflectionKind.TypeAlias -> match node.Type with | Some _ -> parseTypeAlias section node | _ -> [] | _ -> [] and parseNodes section (nodes: Reflection list): Entity list = match nodes with | ([ front ]) -> parseNode section front | (front :: tails) -> parseNode section front @ parseNodes section tails | _ -> []
至此,咱們的 parse 工做所有搞定,完結撒花~~~
有了 C# 的實體類型,代碼生成還困難嗎?
不過有一點要注意的是,咱們須要將名稱轉換爲 Pascal Case,還須要生成 string literals union types 的 JsonConverter。不過這些都是樣板代碼,很是簡單。
這裏就不放代碼了,感興趣的同窗能夠自行去個人 GitHub 倉庫查看。
原 typescipt 代碼:
declare namespace test { /** * The declaration of an enum */ export enum MyEnum { A = 0, B = 1, C = 2, D = C } /** * The declaration of an interface */ export interface MyInterface1 { /** * A method */ testMethod(arg: string, callback: () => void): string; /** * An event * @event */ onTest(listener: (e: MyInterface1) => void): void; /** * An property */ readonly testProp: string; } /** * Another declaration of an interface */ export interface MyInterface2<T> { /** * A method */ testMethod(arg: T, callback: () => void): T; /** * An event * @event */ onTest(listener: (e: MyInterface2<T>) => void): void; /** * An property */ readonly testProp: T; } /** * The declaration of a class */ export class MyClass1<T> implements MyInterface1 { /** * A method */ testMethod(arg: string, callback: () => void): string; /** * An event * @event */ onTest(listener: (e: MyInterface1) => void): void; /** * An property */ readonly testProp: string; static staticMethod(value: string, isOption?: boolean): UnionStr; } /** * Another declaration of a class */ export class MyClass2<T> implements MyInterface2<T> { /** * A method */ testMethod(arg: T, callback: () => void): T; /** * An event * @event */ onTest(listener: (e: MyInterface2<T>) => void): void; /** * An property */ readonly testProp: T; static staticMethod(value: string, isOption?: boolean): UnionStr; } /** * The declaration of a type alias */ export type UnionStr = "A" | "B" | "C" | "other"; }
Typedoc 生成的 JSON 後,將其做爲輸入,生成 C# 代碼:
namespace TypedocConverter.Test { /// <summary> /// The declaration of an enum /// </summary> enum MyEnum { [Newtonsoft.Json.JsonProperty("A", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] A = 0, [Newtonsoft.Json.JsonProperty("B", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] B = 1, [Newtonsoft.Json.JsonProperty("C", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] C = 2, [Newtonsoft.Json.JsonProperty("D", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] D = 2 } } namespace TypedocConverter.Test { /// <summary> /// The declaration of a class /// </summary> class MyClass1<T> : MyInterface1 { /// <summary> /// An property /// </summary> [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] string TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } event System.Action<MyInterface1> OnTest; string TestMethod(string arg, System.Action callback) => throw new System.NotImplementedException(); static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException(); } } namespace TypedocConverter.Test { /// <summary> /// Another declaration of a class /// </summary> class MyClass2<T> : MyInterface2<T> { /// <summary> /// An property /// </summary> [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] T TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } event System.Action<MyInterface2<T>> OnTest; T TestMethod(T arg, System.Action callback) => throw new System.NotImplementedException(); static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException(); } } namespace TypedocConverter.Test { /// <summary> /// The declaration of an interface /// </summary> interface MyInterface1 { /// <summary> /// An property /// </summary> [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] string TestProp { get; set; } event System.Action<MyInterface1> OnTest; string TestMethod(string arg, System.Action callback); } } namespace TypedocConverter.Test { /// <summary> /// Another declaration of an interface /// </summary> interface MyInterface2<T> { /// <summary> /// An property /// </summary> [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] T TestProp { get; set; } event System.Action<MyInterface2<T>> OnTest; T TestMethod(T arg, System.Action callback); } } namespace TypedocConverter.Test { /// <summary> /// The declaration of a type alias /// </summary> [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))] enum UnionStr { ///<summary> /// A ///</summary> [Newtonsoft.Json.JsonProperty("A", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] A, ///<summary> /// B ///</summary> [Newtonsoft.Json.JsonProperty("B", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] B, ///<summary> /// C ///</summary> [Newtonsoft.Json.JsonProperty("C", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] C, ///<summary> /// other ///</summary> [Newtonsoft.Json.JsonProperty("Other", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] Other } class UnionStrConverter : Newtonsoft.Json.JsonConverter { public override bool CanConvert(System.Type t) => t == typeof(UnionStr) || t == typeof(UnionStr?); public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type t, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) => reader.TokenType switch { Newtonsoft.Json.JsonToken.String => serializer.Deserialize<string>(reader) switch { "A" => UnionStr.A, "B" => UnionStr.B, "C" => UnionStr.C, "Other" => UnionStr.Other, _ => throw new System.Exception("Cannot unmarshal type UnionStr") }, _ => throw new System.Exception("Cannot unmarshal type UnionStr") }; public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? untypedValue, Newtonsoft.Json.JsonSerializer serializer) { if (untypedValue is null) { serializer.Serialize(writer, null); return; } var value = (UnionStr)untypedValue; switch (value) { case UnionStr.A: serializer.Serialize(writer, "A"); return; case UnionStr.B: serializer.Serialize(writer, "B"); return; case UnionStr.C: serializer.Serialize(writer, "C"); return; case UnionStr.Other: serializer.Serialize(writer, "Other"); return; default: break; } throw new System.Exception("Cannot marshal type UnionStr"); } } }
有了這個工具後,媽媽不再用擔憂我封裝 TypeScript 的庫了。有了 TypedocConverter,任何 TypeScript 的庫都能垂手可得地轉換成 C# 的類型綁定,而後進行封裝,很是方便。
感謝你們看到這裏,最後,歡迎你們使用 TypedocConverter。固然,若是能 star 一波甚至貢獻代碼,我會很是感謝的!