はじめに
前回は定義ファイルの構造と CodeDefinition クラスのプロパティ体系を見ました。今回は、コード自動生成の中核であるテンプレート展開エンジン(Creators.cs)がどのようにテンプレートを処理し、入れ子構造を再帰的に展開しているかを解説します。
本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。
| 回 | テーマ |
|---|---|
| 第1回 | 全体像:エントリポイント・コマンド体系・初期化フロー |
| 第2回 | 定義ファイルとテンプレートシステム |
| 第3回(本記事) | テンプレート展開エンジン |
| 第4回 | コードマージシステム |
| 第5回 | RepeatType別のコード生成ハンドラ |
| 第6回 | プレースホルダー置換と型変換 |
プレースホルダーの正規表現パターン
テンプレート展開エンジンは、CodePatterns クラスに定義された3つの正規表現パターンを使います。
internal static class CodePatterns
{
internal const string IdPlaceholder = "<!--.*?-->";
internal const string Id = "[A-Za-z0-9_]+";
internal const string ReplacementPlaceholder = "(?<=#)[^#]+?(?=#)";
}
各パターンの役割
| パターン | 正規表現 | マッチ例 | 用途 |
|---|---|---|---|
IdPlaceholder |
<!--.*?--> |
<!--Model_Property--> |
子定義の埋め込み位置を検出 |
Id |
[A-Za-z0-9_]+ |
Model_Property |
IdPlaceholder 内から定義IDを抽出 |
ReplacementPlaceholder |
(?<=#)[^#]+?(?=#) |
#ModelName# 中の ModelName
|
値置換プレースホルダーを検出 |
2種類のプレースホルダーは明確に役割が異なります。
-
<!--ID-->:他のCodeDefinitionを参照して展開する(構造的プレースホルダー) -
#Name#:テーブル名やカラム名などの値で直接置換する(値プレースホルダー)
Creators.Create():展開のメインロジック
テンプレート展開エンジンの中核は Creators.Create() メソッドです。
処理のステップ
ソースコードの詳細解説
internal static string Create(CodeDefinition codeDefinition, DataContainer dataContainer)
{
// ステップ1: Bodyをフォーマット(インデント付与)
var code = codeDefinition.FormattedCode();
// ステップ2: 構造的プレースホルダーを走査
foreach (var placeholder in code.RegexValues(CodePatterns.IdPlaceholder))
{
// ステップ3: プレースホルダーから定義IDを抽出
var id = placeholder.RegexFirst(CodePatterns.Id);
// ステップ4: 対応する子定義を取得
var codeChildDefinition = Def.CodeDefinitionCollection
.FirstOrDefault(o => o.Id == id);
if (codeChildDefinition != null)
{
// ステップ5: オプション設定(プロパティの一時書き換え)
Def.SetCodeDefinitionOption(placeholder, codeChildDefinition);
// ステップ6: RepeatTypeに応じたハンドラを呼び出し
var codeChildCollection = new List<string>();
switch (codeChildDefinition.RepeatType)
{
case "Table":
Table.SetCodeCollection(
codeChildDefinition, codeChildCollection, dataContainer);
break;
case "Column":
Column.SetCodeCollection(
codeChildDefinition, codeChildCollection, dataContainer);
break;
// ... 他のRepeatType ...
}
// ステップ7: プレースホルダーを展開結果で置換
code = CodeChildCollection(
code, placeholder, codeChildDefinition, codeChildCollection);
// ステップ8: 一時変更したプロパティを復元
codeChildDefinition.RestoreBySavedMemory();
}
}
// ステップ9: 値プレースホルダーの置換
ReplaceCode(ref code, dataContainer.TableName);
return code;
}
FormattedCode():テンプレートのフォーマット
テンプレート展開の最初のステップで、Body にインデントを付与します。
internal static string FormattedCode(this CodeDefinition codeRow)
{
string code = string.Empty;
if (codeRow.Body.IndexOf("\r\n") == -1)
{
// 単一行の場合:インデント + Body
code += Strings.Tab(codeRow.Indent) + codeRow.Body;
}
else
{
// 複数行の場合:各行にインデントを付与
codeRow.Body.SplitReturn().ForEach(line =>
{
if (line.Trim().IsNullOrEmpty())
code += "\r\n"; // 空行はそのまま
else if (line.StartsWith("<!--"))
code += line + "\r\n"; // 子定義プレースホルダーはインデントなし
else
code += Strings.Tab(codeRow.Indent) + line + "\r\n";
});
}
code = code.NoSpace(codeRow.NoSpace);
return code;
}
重要なポイントは、<!--子定義ID--> で始まる行にはインデントを付与しないことです。子定義は展開時に自身のインデントを適用するため、二重にインデントが付くのを防いでいます。
再帰的な展開の仕組み
テンプレート展開エンジンの最も強力な特徴は、再帰的な入れ子展開です。
入れ子展開の例
親テンプレートに子定義のプレースホルダーが含まれ、その子定義がさらに別の子定義を含む場合を考えてみましょう。
// 親定義 "Base" の Body
namespace Implem.Pleasanter.Models
{
public class _BaseModel
{
<!--Base_Property-->
}
}
// 子定義 "Base_Property" の Body(RepeatType: カラム)
public #Type# #ColumnName# { get; set; }
この場合の展開フローは次のとおりです。
1. Base の Body をフォーマット
2. <!--Base_Property--> を検出
3. Base_Property の定義を取得(RepeatType = カラム単位)
4. ベースモデルの各カラムに対して展開:
- CreatedTime: "public DateTime CreatedTime { get; set; }"
- UpdatedTime: "public DateTime UpdatedTime { get; set; }"
- ...
5. 展開結果をセパレータで結合
6. <!--Base_Property--> を結合結果で置換
最終的に生成されるコードは次のようになります。
namespace Implem.Pleasanter.Models
{
public class _BaseModel
{
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
// ...
}
}
再帰の深さ
子定義のテンプレート内にさらに <!--孫定義--> が含まれている場合、Creators.Create() が再帰的に呼び出されます。
Creators.SetCodeCollection() を中継して再帰が行われます。
internal static void SetCodeCollection(
ref string code,
List<string> codeCollection,
CodeDefinition codeDefinition,
DataContainer dataContainer,
Action replaceCode)
{
code = Create(codeDefinition, dataContainer); // ← 再帰呼び出し
replaceCode(); // 値プレースホルダーの置換
codeCollection.Add(code); // 結果をコレクションに追加
}
SetCodeDefinitionOption:インラインオプション
<!--子定義ID--> 形式のプレースホルダーには、オプションパラメータを埋め込むことができます。
<!--Model_Property(Indent=3, NotPk=1)-->
SetCodeDefinitionOption() は、この括弧内のパラメータをパースして子定義のプロパティを一時的に上書きします。
public static void SetCodeDefinitionOption(
string placeholder, CodeDefinition codeDefinition)
{
// 正規表現で括弧内のオプション文字列を抽出
var options = placeholder.RegexFirst("(?<=\\().+(?=\\))");
if (!options.IsNullOrEmpty())
{
// カンマ区切りの各オプションを処理
foreach (var option in options.Split(','))
{
var keyValue = option.Split('=');
var key = keyValue[0].Trim();
var value = keyValue[1].Trim();
// プロパティに値をセット
switch (key)
{
case "Indent": codeDefinition.Indent = value.ToInt(); break;
case "NotPk": codeDefinition.NotPk = value.ToBool(); break;
case "Include": codeDefinition.Include = value; break;
// ... その他のプロパティ ...
}
}
}
}
これにより、同じ子定義を異なるパラメータで再利用できます。
<!-- 主キーカラムだけを対象にして展開 -->
<!--Model_Property(Pk=1)-->
<!-- 主キーでないカラムだけを対象にして展開 -->
<!--Model_Property(NotPk=1)-->
CodeChildCollection:展開結果の結合
各 RepeatType ハンドラで生成されたコード片は codeChildCollection リストに格納され、最終的に CodeChildCollection() メソッドで結合されます。
private static string CodeChildCollection(
string code,
string placeholder,
CodeDefinition codeChildDefinition,
List<string> codeChildCollection)
{
return code.Replace(
placeholder,
codeChildCollection.Join(
codeChildDefinition.Separator.Replace("\\r\\n", "\r\n")));
}
Separator プロパティで指定された区切り文字で結合されます。一般的な区切り文字は以下のとおりです。
| Separator の値 | 実際の区切り | 用途の例 |
|---|---|---|
\\r\\n |
改行 | プロパティ宣言の羅列 |
,\\r\\n |
カンマ+改行 | enum の値やメソッド引数 |
| (空) | 空文字列 | コード片を直接結合 |
ReplaceCode:最終的な値置換
構造的プレースホルダーの展開が終わった後、最後に値プレースホルダーの置換が行われます。
private static void ReplaceCode(ref string code, string tableName)
{
foreach (var placeholder in code.RegexValues(CodePatterns.ReplacementPlaceholder))
{
switch (placeholder)
{
case "IdType":
code = code.Replace("#IdType#", tableName.CsTypeIdColumn());
break;
case "IdTypeDefault":
code = code.Replace("#IdTypeDefault#", tableName.DefaultIdColumn());
break;
case "CastIdType":
code = code.Replace("#CastIdType#", tableName.CastTypeIdColumn());
break;
}
}
}
ここで置換されるのは、テーブルのID列に関する型情報です。
| プレースホルダー | 説明 | 例 |
|---|---|---|
#IdType# |
ID列のC#型 |
int, long
|
#IdTypeDefault# |
ID列のデフォルト値 |
0, ""
|
#CastIdType# |
ID列のキャスト式 |
.ToInt(), .ToLong()
|
ここで処理される #IdType# 等は Creators レベルの値置換です。#TableName#、#ModelName# 等のより一般的な値置換は、各 RepeatType ハンドラ(Table.ReplaceCode()、Column.ReplaceCode() 等)で行われます。詳細は第5回と第6回で解説します。
展開フローの完全な例
最後に、一連の展開フローを具体的なコードで追跡してみましょう。
入力
親定義 Model(Source: "Mvc", RepeatType: "Table")のテンプレート:
namespace #ServiceName#.Models
{
public class #ModelName#Model : _BaseModel
{
<!--Model_Property-->
}
}
子定義 Model_Property(RepeatType なし、Indent: 2、Separator: "\\r\\n"):
public #Type# #ColumnName# { get; set; }
展開の過程(Users テーブルの場合)
ステップ1: FormattedCode() でインデント付与
→ namespace #ServiceName#.Models
{
public class #ModelName#Model : _BaseModel
{
<!--Model_Property-->
}
}
ステップ2: <!--Model_Property--> を検出し、子定義を取得
ステップ3: Column.SetCodeCollection() で Users テーブルの各カラムに展開
→ " public int UserId { get; set; }"
→ " public string LoginId { get; set; }"
→ " public string Name { get; set; }"
→ ...
ステップ4: Separator("\r\n") で結合
ステップ5: <!--Model_Property--> を展開結果で置換
ステップ6: ReplaceCode() で残りの値プレースホルダーを置換
ステップ7: MvcCreator.ReplacePlaceholder() で最上位の置換
→ #ServiceName# → Implem.Pleasanter
→ #ModelName# → User
出力
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
public string Name { get; set; }
// ...
}
}
まとめ
第3回では、テンプレート展開エンジンの中核ロジックを解説しました。
-
2種類のプレースホルダー:構造的(
<!--ID-->)と値(#Name#)が明確に分離されている -
Creators.Create()は 再帰的 にテンプレートを展開する -
SetCodeDefinitionOption()で インラインオプション を渡し、同じ子定義を異なる条件で再利用できる - SavedMemory / RestoreBySavedMemory パターンで一時的なプロパティ変更を安全に管理する
- 展開結果は
Separatorで結合され、プレースホルダーを置換する
次回は、生成されたコードを既存ファイルに安全にマージするコードマージシステム(Merger.cs、Parser.cs)を解説します。