using System.Text; using System.Text.RegularExpressions; namespace ApiRegistrationTool; /// /// Reads a DTO .cs file and extracts all public properties to produce /// representative request / response JSON samples. /// /// Handles three property patterns found across the GB5 codebase: /// 1. Auto-property: public string Name { get; set; } /// 2. Backing-field style: public string Name { get { return _name; } set { _name = value; } } /// 3. Record constructor: public record Foo(string Name, int Id) /// public static class DtoPropertyExtractor { // ── Pattern 1: Auto-property ────────────────────────────────────────── private static readonly Regex RxAutoProp = new( @"^\s*public\s+([\w\?\[\]<>,\s]+?)\s+(\w+)\s*\{\s*get\s*;\s*(?:set|init)\s*;", RegexOptions.Compiled | RegexOptions.Multiline); // ── Pattern 2: Old backing-field style ──────────────────────────────── // public string LeaveName { get { return ... } set { ... } } private static readonly Regex RxOldProp = new( @"^\s*public\s+([\w\?\[\]<>,\s]+?)\s+(\w+)\s*\n?\s*\{\s*\n?\s*get\s*\{", RegexOptions.Compiled | RegexOptions.Multiline); // ── Pattern 2b: single-line old style ───────────────────────────────── // public int Id { get { return id; } set { id = value; } } private static readonly Regex RxOldPropInline = new( @"^\s*public\s+([\w\?\[\]<>,\s]+?)\s+(\w+)\s*\{\s*get\s*\{[^}]+\}\s*set\s*\{", RegexOptions.Compiled | RegexOptions.Multiline); // ── Pattern 3: Record primary constructor ───────────────────────────── private static readonly Regex RxRecord = new( @"public\s+(?:sealed\s+)?record\s+\w+\s*\(([^)]+)\)", RegexOptions.Compiled | RegexOptions.Singleline); // ── System properties to exclude (not real API params) ──────────────── private static readonly HashSet _systemProps = new(StringComparer.OrdinalIgnoreCase) { "CreatedOn", "ModifiedOn", "CreatedById", "ModifiedById", "Status", "SortOrder", "Version", "SourceType", "TenantId", "VersionNo", "IsDeleted", "DeletedOn", "DeletedById", "CreatedByName", "ModifiedByName" }; // ═════════════════════════════════════════════════════════════════════ // PUBLIC API // ═════════════════════════════════════════════════════════════════════ /// /// Builds a JSON sample object from ALL public properties in the DTO file. /// The result is a real, parseable JSON string that mirrors the DTO shape. /// Returns "{}" if the file cannot be read. /// public static string BuildRequestJson(string? dtoFilePath) { if (string.IsNullOrWhiteSpace(dtoFilePath) || !File.Exists(dtoFilePath)) return "{}"; try { string text = File.ReadAllText(dtoFilePath); var props = ExtractProperties(text); return props.Count == 0 ? "{}" : BuildJsonObject(props); } catch { return "{}"; } } /// /// Builds the standard GB5 success response wrapper JSON for this entity. /// Uses actual DTO properties if the DTO file is available. /// public static string BuildResponseJson(string entityName, string? responseDtoFile = null) { string innerJson = "null"; if (responseDtoFile != null) { string inner = BuildRequestJson(responseDtoFile); if (inner != "{}") innerJson = inner; } // GB5 standard wrapper: { Status, Message, Body: { Data, List, TotalCount } } return "{" + "\"Status\":\"Success\"," + "\"Message\":\"\"," + "\"Body\":{" + $"\"{entityName}\":{innerJson}," + "\"List\":[]," + "\"TotalCount\":0" + "}" + "}"; } /// /// Builds a compact JSON object representing only the given parameter list /// (used when no DTO file is found — falls back to query-param stubs). /// public static string BuildParamJson(List queryParams) { if (queryParams.Count == 0) return "{}"; var sb = new StringBuilder("{"); for (int i = 0; i < queryParams.Count; i++) { if (i > 0) sb.Append(','); sb.Append($"\"{queryParams[i].Name}\":{SampleValue(queryParams[i].CsType)}"); } sb.Append('}'); return sb.ToString(); } // ═════════════════════════════════════════════════════════════════════ // INTERNALS // ═════════════════════════════════════════════════════════════════════ private static List<(string Name, string Type)> ExtractProperties(string text) { var props = new List<(string Name, string Type)>(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); void Add(string rawType, string name) { string n = name.Trim(); string t = rawType.Trim().TrimEnd('?'); if (string.IsNullOrWhiteSpace(n) || string.IsNullOrWhiteSpace(t)) return; if (n.Length < 2) return; // skip single-letter names if (_systemProps.Contains(n)) return; // Skip name suffixes like "...Name", "...Code" for lookup/display-only props // (keep them — they ARE part of the DTO shape and help describe the API) if (seen.Add(n)) props.Add((n, t)); } // 1. Record primary constructor var recMatch = RxRecord.Match(text); if (recMatch.Success) { foreach (var param in recMatch.Groups[1].Value.Split(',')) { var parts = param.Trim().Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) Add(parts[^2], parts[^1]); } } // 2. Auto-properties foreach (Match m in RxAutoProp.Matches(text)) Add(m.Groups[1].Value, m.Groups[2].Value); // 3. Old backing-field style (multi-line) foreach (Match m in RxOldProp.Matches(text)) Add(m.Groups[1].Value, m.Groups[2].Value); // 4. Old backing-field style (inline) foreach (Match m in RxOldPropInline.Matches(text)) Add(m.Groups[1].Value, m.Groups[2].Value); return props; } private static string BuildJsonObject(List<(string Name, string Type)> props) { var sb = new StringBuilder("{"); bool first = true; foreach (var (name, type) in props) { if (!first) sb.Append(','); first = false; sb.Append($"\"{name}\":{SampleValue(type)}"); } sb.Append('}'); return sb.ToString(); } internal static string SampleValue(string csType) { string t = csType.TrimEnd('?').ToLowerInvariant().Trim(); return t switch { "int" or "int32" or "long" or "int64" or "short" or "int16" or "byte" or "uint" or "uint32" or "smallint" => "0", "bool" or "boolean" => "false", "decimal" or "double" or "float" or "single" => "0.0", "datetime" or "datetimeoffset" => "\"2025-01-01T00:00:00\"", "dateonly" => "\"2025-01-01\"", "guid" => "\"00000000-0000-0000-0000-000000000000\"", _ when t.StartsWith("list<") || t.EndsWith("[]") => "[]", _ when t.StartsWith("dictionary<") => "{}", _ => "\"\"" }; } }