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?

プリザンターのCodeDefinerコード自動生成を解剖する[第6回:プレースホルダー置換と型変換]

0
Posted at

はじめに

前回RepeatType 別のハンドラを解説しました。最終回となる今回は、テンプレート内の値プレースホルダーがどのように置換されるか、特に型変換Converts.cs)と予約語エスケープReservedWords.cs)のロジックを詳しく見ていきます。

本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。

テーマ
第1回 全体像:エントリポイント・コマンド体系・初期化フロー
第2回 定義ファイルとテンプレートシステム
第3回 テンプレート展開エンジン
第4回 コードマージシステム
第5回 RepeatType別のコード生成ハンドラ
第6回(本記事) プレースホルダー置換と型変換

Column.ReplaceCode:カラムの値置換

Column.ReplaceCode() は、カラム定義の各属性をテンプレート内のプレースホルダーに置換します。すべてのカラム関連プレースホルダーの処理がここに集約されています。

基本プレースホルダー

プレースホルダー 説明 値の例
#ColumnName# カラム名(先頭大文字) UserId, LoginId
#columnName# カラム名(先頭小文字) userId, loginId
#Type# C#型(TypeCsTypeName の優先順) string, int, Title
#RecordingType# DB記録時の型 int, nvarchar
#RecordingData# 記録時のデータ変換式 .ToJson()
#CastType# キャスト式 .ToInt(), .ToString()
#DefaultData# デフォルト値 0, string.Empty
#InitialValue# 初期値 0, false, "[]"
#Hash# ハッシュ変換式(Hash列の場合) .Sha512Cng()
#MaxLength# 最大長バリデーション .MaxLength(100)
#Calc# 計算式 テーブル名・モデル名を置換済みの式
#ColumnCount# 対象カラムの総数 15
#GridEnable# 一覧表示の有効/無効 1, 0

動的プレースホルダー

上記の固定プレースホルダー以外にも、カラム定義のプロパティ名がそのままプレースホルダーとして使えます。

private static string ReplaceCode(
    string code, ColumnDefinition columnDefinition, string placeholder)
{
    if (Def.ColumnXls.XlsSheet.Columns.Contains(placeholder))
    {
        code = code.Replace(
            "#" + placeholder + "#", columnDefinition[placeholder].ToString());
    }
    return code;
}

定義ファイルのカラム名がそのままプレースホルダーとして展開されるため、新しいプロパティを追加するだけで自動的に使えるようになります。

型変換プレースホルダー

型変換プレースホルダーは、フォーム入力やAPI入力、DataRowからの読み込みなど、データの入口に応じた型変換コードを自動生成します。

プレースホルダー 用途 ソースメソッド
#ByForm#(value) フォーム入力からの変換 Converts.ByForm()
#ByApi#(data.Property) API入力からの変換 Converts.ByApi()
#ByDataRow#(dataRow) DataRowからの変換 Converts.ByDataRow()
#BySession#(session) セッションからの変換 Converts.BySession()

テンプレートでの使い方

// テンプレートの例
#ColumnName# = #ByForm#(value);

Users テーブルの LoginIdTypeName: nvarchar)の場合:

// 展開結果
LoginId = value.ToString();

Users テーブルの UserIdTypeName: int)の場合:

// 展開結果
UserId = value.ToInt();

Converts.cs:型変換ロジックの詳細

変換の優先順位

型変換は以下の優先順位で決定されます。

CastType():基本型のキャスト変換

internal static string CastType(this string type)
{
    switch (type.CsType())
    {
        case Types.CsString:   return ".ToString()";
        case Types.CsInt:      return ".ToInt()";
        case Types.CsLong:     return ".ToLong()";
        case Types.CsDecimal:  return ".ToDecimal()";
        case Types.CsSingle:   return ".ToSingle()";
        case Types.CsDouble:   return ".ToDouble()";
        case Types.CsDateTime: return ".ToDateTime()";
        case Types.CsBool:     return ".ToBool()";
        default:               return string.Empty;
    }
}

データベース型名からC#のキャスト式への変換マッピングです。

DB型名 C#型分類 キャスト式
nvarchar, varchar, char CsString .ToString()
int CsInt .ToInt()
bigint CsLong .ToLong()
decimal, money CsDecimal .ToDecimal()
float CsSingle .ToSingle()
real CsDouble .ToDouble()
datetime CsDateTime .ToDateTime()
bit CsBool .ToBool()

TypeCs による特殊変換

TypeCs(C#独自型)が設定されているカラムは、単純なキャストではなくオブジェクト生成が行われます。

private static string TypeCs(
    string code, string convertFrom,
    ColumnDefinition columnDefinition,
    string placeholder, string codeVariable)
{
    switch (columnDefinition.TypeCs + convertFrom)
    {
        case "Title#ByForm#":
        case "Body#ByForm#":
        case "Status#ByForm#":
            return code.Replace(placeholder,
                CreateObjectByForm(columnDefinition));

        case "Time#ByForm#":
        case "CompletionTime#ByForm#":
            return code.Replace(placeholder,
                CreateObjectByForm(columnDefinition, ", byForm: true"));

        case "Title#ByApi#":
        case "Body#ByApi#":
        case "Status#ByApi#":
            return code.Replace(placeholder,
                CreateObjectByApi(columnDefinition));

        // ... DataRow 版も同様 ...

        default:
            return code.Replace(placeholder,
                AsPrefix(columnDefinition, codeVariable));
    }
}

TypeCs + 変換元の組み合わせで出力が異なります。

TypeCs 変換元 生成されるコード
Title ByForm new Title(value.ToString())
Time ByForm new Time(context, value.ToDateTime(), byForm: true)
Status ByApi new Status(data.Status.ToInt())
CompletionTime ByDataRow new CompletionTime(context, dataRow, column.ColumnName)
その他 - value as TypeCs ?? defaultValue

ByForm の Body カラム特殊処理

IssuesResults テーブルの Body カラムには、バイナリパスの正規化という特殊処理が入ります。

internal static string ByForm(this string code, ColumnDefinition columnDefinition)
{
    var placeholder = Placeholder(code, "#ByForm#");
    if ((columnDefinition.TableName == "Issues" || columnDefinition.TableName == "Results")
        && columnDefinition.ColumnName == "Body")
    {
        return code.Replace(placeholder,
            "Implem.Pleasanter.Models.BinaryUtilities" +
            ".NormalizeFormBinaryPath(context, value.ToString())");
    }
    // ... 通常の変換処理 ...
}

DefaultData / InitialValue の生成

カラムのデフォルト値と初期値の生成も型に応じて行われます。

DefaultData

private static string DefaultData(this ColumnDefinition columnDefinition, ...)
{
    switch (/* 型の分類 */)
    {
        case Types.CsString:
            switch (columnDefinition.RecordingData)
            {
                case ".ToJson()":
                case ".RecordingJson()":
                    // JSON配列/オブジェクトの初期値
                    return columnDefinition.TypeCs switch
                    {
                        "List<long>" or "Attachments" => "\"[]\"",
                        _ => "\"{}\""
                    };
                default:
                    return DefaultData(columnDefinition, "string.Empty", bracket: true);
            }
        case Types.CsNumeric:  return DefaultData(columnDefinition, "0");
        case Types.CsDateTime: return "0.ToDateTime()";
        case Types.CsBool:     return columnDefinition.Default == "1" ? "true" : "false";
        case Types.CsBytes:    return "null";
    }
}
型分類 デフォルト値
文字列 string.Empty
文字列(JSON記録) "[]" or "{}"
数値 0
日時 0.ToDateTime()
ブール falseDefault="1" のとき true
バイナリ null

カスタムデフォルト値

DefaultCs プロパティが設定されている場合はそちらが優先されます。

private static string DefaultData(
    ColumnDefinition columnDefinition, string _default, bool bracket = false)
{
    if (!columnDefinition.DefaultCs.IsNullOrEmpty())
        return bracket ? "\"" + columnDefinition.DefaultCs + "\"" : columnDefinition.DefaultCs;
    else if (!columnDefinition.Default.IsNullOrEmpty())
        return bracket ? "\"" + columnDefinition.Default + "\"" : columnDefinition.Default;
    else
        return _default;
}

ReservedWords:C#予約語のエスケープ

コード自動生成では、定義ファイルのIDやカラム名がそのままC#の識別子として使われることがあります。C#の予約語と衝突する場合は、エスケープが必要です。

EscapeReservedWord

public static string EscapeReservedWord(
    this string word, string additionalPrefix = "_", string additionalSuffix = "")
{
    return CsReservedWordArray.Contains(word)
        ? additionalPrefix + word + additionalSuffix
        : word;
}

予約語に一致する場合、デフォルトではアンダースコアのプレフィックスが追加されます。

例:定義カラム名が class の場合 → _class に変換

エスケープ対象の予約語一覧

private static readonly string[] CsReservedWordArray =
{
    "abstract", "as", "async", "await", "base", "bool", "break", "byte",
    "case", "catch", "char", "checked", "class", "const", "continue",
    "decimal", "default", "delegate", "do", "double", "else", "enum",
    "event", "explicit", "extern", "false", "finally", "fixed", "float",
    "for", "foreach", "goto", "if", "implicit", "in", "int", "interface",
    "internal", "is", "lock", "long", "namespace", "new", "null", "object",
    "operator", "out", "override", "params", "private", "protected",
    "public", "readonly", "ref", "return", "sbyte", "sealed", "short",
    "sizeof", "stackalloc", "static", "string", "struct", "switch", "this",
    "throw", "true", "try", "typeof", "uint", "ulong", "unchecked",
    "unsafe", "ushort", "using", "virtual", "volatile", "void", "while",
    "add", "dynamic", "get", "partial", "remove", "set", "value",
    "var", "where", "yield"
};

C#のキーワードに加えて、コンテキストキーワード(adddynamicgetsetvaluevarwhereyield など)もエスケープ対象に含まれています。

ValidName:特殊文字の変換

public static string ValidName(string valiableName)
{
    return valiableName
        .Replace(" ", "_space_")
        .Replace(">", "_")
        .Replace(".", "_dot_")
        .Replace("#", "_sharp_")
        .Replace(",", "_comma_")
        .Replace(":", "_colon_")
        .Replace("[", "_")
        .Replace("]", "_")
        .Replace("(", "_")
        .Replace(")", "_")
        .Replace("-", "_")
        .Replace("+", "_plus_")
        .Replace("=", "_equal_")
        .Replace("^", "_caret_")
        .Replace("\"", "_yen_")
        .Replace("*", "_asterisk_")
        .Replace("@", "_atmark_");
}

定義IDに含まれる可能性のある特殊文字を、C#識別子として有効な文字列に変換します。DefinitionRow ハンドラで使われ、定義行のIDをC#のプロパティ名に変換する際に適用されます。

元の文字 変換後 用途の例
空白 _space_ 表示文字列ID
. _dot_ バージョン番号を含むID
# _sharp_ C#関連の定義名
- _ ハイフン区切りの名前
@ _atmark_ メールアドレス関連

プレースホルダー置換の全体像

連載を通じて解説してきたプレースホルダー置換の全体像をまとめます。

置換の実行順序

  1. 構造的プレースホルダー<!--ID-->)が再帰的に展開される
  2. 各ハンドラの ReplaceCode()カラム名・型名などの値置換が行われる
  3. Creators.ReplaceCode()ID列の型情報が置換される
  4. 最後に MvcCreator.ReplacePlaceholder()テーブル名・モデル名が置換される

全プレースホルダー一覧

連載を通じて登場したプレースホルダーをカテゴリ別にまとめます。

テーブル/モデル関連

プレースホルダー 置換元 説明
#ServiceName# Environments.ServiceName サービス名
#TableName# dataContainer.TableName テーブル名
#tableName# 先頭小文字化 テーブル名(小文字始まり)
#tablename# 全小文字化 テーブル名(全小文字)
#ModelName# dataContainer.ModelName モデル名
#modelName# 先頭小文字化 モデル名(小文字始まり)
#modelname# 全小文字化 モデル名(全小文字)

カラム関連

プレースホルダー 置換元 説明
#ColumnName# columnDefinition.ColumnName カラム名
#columnName# 先頭小文字化 カラム名(小文字始まり)
#Type# TypeCs or TypeName C#型
#RecordingType# TypeName DB記録型
#RecordingData# columnDefinition.RecordingData 記録時変換式
#CastType# TypeName.CastType() キャスト式
#DefaultData# 型に基づく生成 デフォルト値
#InitialValue# 型に基づく生成 初期値
#Hash# Hash フラグ ハッシュ式
#MaxLength# MaxLength 最大長バリデーション
#Calc# columnDefinition.Calc 計算式

型変換関連

プレースホルダー 説明
#ByForm#(expr) フォーム入力からの変換
#ByApi#(expr) API入力からの変換
#ByDataRow#(expr) DataRow読込からの変換
#BySession#(expr) セッション読込からの変換
#IdType# ID列のC#型
#IdTypeDefault# ID列のデフォルト値
#CastIdType# ID列のキャスト式

JOIN 関連

プレースホルダー 説明
#JoinTableName# 結合先テーブル名
#JoinType# 結合タイプ
#JoinExpression# 結合条件式
#TableNameAlias# テーブルエイリアス
#ColumnBracket# カラムブラケット
#ColumnBrackets# 計算列含むブラケット

定義メタ関連

プレースホルダー 説明
#File# / #file# 定義ファイル名
#ColumnNames# カラム名リスト
#Id# 定義行のID
#DefColumnName# 定義カラム名(エスケープ済み)
#DefColumnNameOriginal# 定義カラム名(原本)
#SetDefault# デフォルト値初期化式

表示関連

プレースホルダー 説明
#DisplayId# 表示ID
#DisplayContent# 表示テキスト
#DisplayCssClass# CSSクラス名
#DisplayContentEncoded# HTMLエンコード済みテキスト
#FormName# フォーム名

連載のまとめ

全6回にわたって、CodeDefinerのコード自動生成エンジンを体系的に解説してきました。

アーキテクチャの全体像

各回で学んだこと

テーマ キーポイント
第1回 全体像 コマンド体系、初期化フロー、3つの生成フェーズ
第2回 定義ファイル JSON + Body.txt のペア構成、フィルタプロパティ体系
第3回 展開エンジン 2種類のプレースホルダー、再帰展開、インラインオプション
第4回 コードマージ /// Fixed: マーカー、Parser による構造解析、差分書き込み
第5回 ハンドラ 10種類の RepeatType、フィルタリングロジック
第6回 型変換 CastType、TypeCs 特殊変換、予約語エスケープ

コントリビュータへの実践的アドバイス

  1. 新しいカラムを追加する場合Definition_Column/ にJSONファイルを追加し、CodeDefinerの def コマンドを実行する
  2. 手動修正を保護する場合/// Fixed: コメントをメソッドの直前に追加する
  3. 特定のテーブルのみにコードを生成する場合Include / Exclude プロパティを定義JSONに追加する
  4. コード生成のデバッグ/t オプションで特定の定義IDだけを再生成して動作確認する
  5. 定義ファイルの新規プロパティ追加:カラム定義のプロパティ名がそのまま動的プレースホルダーとして使えることを活用する

CodeDefinerのコード自動生成は、一見複雑に見えますが、基本的な仕組みはテンプレート + プレースホルダー置換 + 再帰展開というシンプルなパターンの組み合わせです。このリファレンスが、新規コントリビュータの皆さんのソースコード理解の一助になれば幸いです。

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?