QiitaでT4テンプレートのシンタックスハイライトできないのさみしい。
やりたいこと
例えばこういうjsonをインプットとして、
{
"name": "Person",
"ns": "Test.Types",
"properties": [
{ "name": "FamilyName", "type": "string" },
{ "name": "FirstName", "type": "string" },
{ "name": "BirthDay", "type": "DateTime" }
]
}
こういうコードを生成したい場合、
using System;
using System.Runtime.Serialization;
namespace Test.Types
{
[DataContract]
public class Person
{
[DataMember(Name = "FamilyName")] string _FamilyName;
public string FamilyName { get { return _FamilyName; } }
[DataMember(Name = "FirstName")] string _FirstName;
public string FirstName { get { return _FirstName; } }
[DataMember(Name = "BirthDay")] DateTime _BirthDay;
public DateTime BirthDay { get { return _BirthDay; } }
}
}
こんな感じのT4テンプレートでできる。今回の主題と違うけどこういう用途にDynamicJson使うの楽でいいな。
<#@ template hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Microsoft.Csharp" #>
<#@ assembly name="$(SolutionDir)\dynamicjson.dll" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Codeplex.Data" #>
<#@ output extension=".cs" #>
<#
dynamic m = DynamicJson.Parse(File.ReadAllText(Host.ResolvePath("person.json")));
#>
using System;
using System.Runtime.Serialization;
namespace <#= m.ns #>
{
[DataContract]
public class <#= m.name #>
{
<# foreach (dynamic p in m.properties) { #>
[DataMember(Name = "<#= p.name #>")] <#= p.type #> _<#= p.name #>;
public <#= p.type #> <#= p.name #> { get { return _<#= p.name #>; } }
<# } #>
}
}
では、指定のフォルダ内にあるすべての*.jsonに対してこの処理をしたいという場合にどういうテンプレートを書けばいいかという話。
Step by Step
テンプレートからの複数ファイル出力の基本
T4には、一つのテンプレートから複数のファイルを出力する機能はない。
ではどうするかというと、GenerationEnvironmentプロパティにテンプレートのレンダリング結果が追加されていくので、それを適切なタイミングでファイルに書き出してはリセットする、という処理を自分で書く必要がある。
<# foreach (var fruit in new [] {"apple", "orange", "banana"}) { #>
I like <#= fruit #>.
<# } #>
<#
// Fruit1.txtにはI like apple. ~ I like banana.が出力される
File.WriteAllText("Fruit1.txt", this.GenerationEnvironment.ToString());
this.GenerationEnvironment.Clear();
#>
<# foreach (var fruit in new [] {"kiwi", "strawberry", "grape"}) { #>
I like <#= fruit #>.
<# } #>
<#
// Fruit2.txtにはI like kiwi. ~ I like grape.が出力される
File.WriteAllText("Fruit2.txt", this.GenerationEnvironment.ToString());
this.GenerationEnvironment.Clear();
#>
匿名メソッドに出力コードを埋め込む
考えてみれば当たり前の話なんだけど、foreach {}
とか if {}
だけじゃなくて、匿名メソッドにも出力コードを埋め込むことができる。
つまりこんな感じ。
<# Action<string> act = s => { #>
テスト<#= s #>
<# }; #>
<#
act("A"); // テストA と出力される
act("B"); // テストB と出力される
#>
出力用のメソッドを用意する
ということは、あらかじめこんな感じのメソッドを用意しておけば、
using System.IO;
using System.Text;
public class T4Generator
{
public static void ProcessInputFiles(string basedir, string pattern,
StringBuilder generationEnvironment,
Action<string> proc)
{
foreach (var inputFile in Directory.GetFiles(basedir, pattern))
{
var outputFile = Path.ChangeExtension(inputFile, ".cs");
try
{
proc(inputFile);
File.WriteAllText(generationEnvironment.ToString());
}
catch (Exception e)
{
File.WriteAllText("Failed to process template\n" + e.StackTrace);
}
finally
{
generationEnvironment.Clear();
}
}
}
}
ファイル出力やGenerationEnvironmentのリセット、エラー処理なんかはぜんぶそのメソッドにまかせてしまって、
テンプレート側はほとんど変えずに複数ファイル出力に対応することができる。
<#@ ... #>
<#
T4Generator.ProcessInputFile(Path.GetDirectoryName(Host.TemplatePath), "*.json",
this.GenerationEnvironment, inputFile => {
dynamic m = DynamicJson.Parse(File.ReadAllText(inputFile));
#>
using System;
using System.Runtime.Serialization;
namespace <#= m.ns #>
{
[DataContract]
public class <#= m.name #>
{
...
}
}
<# }); #>
このメソッドはdllにして参照してもいいし、別のテンプレートに書いてincludeしてもいいと思う。
T4テンプレートから複数ファイルを出力するためのツールキットとしては、こういうの↓もあるんだけど、EnvDTEを参照してるとCIに乗せづらくなるので今回は使わなかった。
http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited