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<") => "{}",
_ => "\"\""
};
}
}