0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

前回は定義ファイルの構造と 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回で解説します。

展開フローの完全な例

最後に、一連の展開フローを具体的なコードで追跡してみましょう。

入力

親定義 ModelSource: "Mvc", RepeatType: "Table")のテンプレート:

namespace #ServiceName#.Models
{
    public class #ModelName#Model : _BaseModel
    {
<!--Model_Property-->
    }
}

子定義 Model_PropertyRepeatType なし、Indent: 2Separator: "\\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.csParser.cs)を解説します。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?