LoginSignup
14
10

More than 5 years have passed since last update.

複数の入力ファイルをT4テンプレートに適用して複数のファイルを生成する

Last updated at Posted at 2015-04-06

QiitaでT4テンプレートのシンタックスハイライトできないのさみしい。

やりたいこと

例えばこういうjsonをインプットとして、

person.json
{
    "name": "Person",
    "ns":  "Test.Types",
    "properties": [
        { "name": "FamilyName", "type": "string" },
        { "name": "FirstName", "type": "string" },
        { "name": "BirthDay", "type": "DateTime" }
    ]
}

こういうコードを生成したい場合、

person.cs
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使うの楽でいいな。

person.tt
<#@ 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プロパティにテンプレートのレンダリング結果が追加されていくので、それを適切なタイミングでファイルに書き出してはリセットする、という処理を自分で書く必要がある。

fruit.tt
<# 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 と出力される
#>

出力用のメソッドを用意する

ということは、あらかじめこんな感じのメソッドを用意しておけば、

T4Generator.cs
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

14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10