Files
MBN_STOCK_WEBVIEW/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyFixedActionCatalog.cs

816 lines
28 KiB
C#

using System.Collections.ObjectModel;
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
public enum LegacyFixedMarket
{
Overseas,
Exchange,
Index
}
public enum LegacyFixedCategory
{
Plate,
Market,
Chart
}
public sealed record LegacyFixedSelection(
string GroupCode,
string Subject,
string GraphicType,
string Subtype,
string DataCode);
public readonly record struct LegacyMovingAverageSelection(bool Ma5, bool Ma20);
public sealed record LegacyFixedRuntimeAsset(
LegacyFixedMarket Market,
string RelativePath,
string Sha256);
public sealed class LegacyFixedAction
{
internal LegacyFixedAction(
string id,
LegacyFixedMarket market,
string section,
string label,
string detail,
LegacyFixedCategory category,
string builderKey,
string code,
IReadOnlyList<string> aliases,
bool available,
string prerequisite,
LegacyFixedSelection selection,
bool dynamicDate,
bool dynamicNxtSession)
{
Id = id;
Market = market;
Section = section;
Label = label;
Detail = detail;
Category = category;
BuilderKey = builderKey;
Code = code;
Aliases = aliases;
Available = available;
Prerequisite = prerequisite;
Selection = selection;
DynamicDate = dynamicDate;
DynamicNxtSession = dynamicNxtSession;
}
public string Id { get; }
public LegacyFixedMarket Market { get; }
public string Section { get; }
public string Label { get; }
public string Detail { get; }
public LegacyFixedCategory Category { get; }
public string BuilderKey { get; }
public string Code { get; }
public IReadOnlyList<string> Aliases { get; }
public bool Available { get; }
public string Prerequisite { get; }
public LegacyFixedSelection Selection { get; }
public bool DynamicDate { get; }
public bool DynamicNxtSession { get; }
public bool SupportsMovingAverages =>
string.Equals(BuilderKey, "s8010", StringComparison.Ordinal);
}
public sealed record LegacyMaterializedFixedAction(
LegacyFixedAction Action,
LegacyFixedSelection Selection,
LegacyFixedRuntimeAsset RuntimeAsset);
public sealed class LegacyFixedSection
{
internal LegacyFixedSection(
LegacyFixedMarket market,
string name,
IReadOnlyList<LegacyFixedAction> actions)
{
Market = market;
Name = name;
Actions = actions;
}
public LegacyFixedMarket Market { get; }
public string Name { get; }
public IReadOnlyList<LegacyFixedAction> Actions { get; }
}
/// <summary>
/// C#-authoritative catalog for the three fixed legacy UC1 tabs. The embedded JSON is a
/// generated build asset; callers can select only by the opaque action id and cannot provide
/// builder, cut alias, or scene-selection values.
/// </summary>
public sealed class LegacyFixedActionCatalog
{
public const int ExpectedActionCount = 328;
public const int ExpectedOverseasCount = 78;
public const int ExpectedExchangeCount = 46;
public const int ExpectedIndexCount = 204;
public const int ExpectedAvailableCount = 322;
public const string ExpectedDocumentSha256 =
"E56FB9DE8247672022EE2F324AF4C37179EC58E399758425AD4C29DAF1BDDC56";
public const string ExpectedGeneratorSourceSha256 =
"31D1B0F81A53FF47700D179E45BEFCA55342D11EAD3A58E67F6A926607A874B5";
private const string ResourceName =
"MBN_STOCK_WEBVIEW.LegacyApplication.Catalog.LegacyFixedActions.json";
private const string ExpectedGeneratedFrom = "Web/legacy-fixed-workflow.js";
private static readonly JsonSerializerOptions StrictJsonOptions = new()
{
AllowTrailingCommas = false,
PropertyNameCaseInsensitive = false,
ReadCommentHandling = JsonCommentHandling.Disallow,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
};
private static readonly Lazy<LegacyFixedActionCatalog> EmbeddedCatalog =
new(LoadEmbedded, LazyThreadSafetyMode.ExecutionAndPublication);
private readonly IReadOnlyDictionary<string, LegacyFixedAction> _byId;
private readonly IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>>
_byMarket;
private readonly IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>
_sectionsByMarket;
private readonly IReadOnlyDictionary<(LegacyFixedMarket Market, string Section),
IReadOnlyList<LegacyFixedAction>> _bySection;
private LegacyFixedActionCatalog(
IReadOnlyList<LegacyFixedAction> actions,
IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset> runtimeAssets,
string documentSha256,
string generatorSourceSha256)
{
Actions = actions;
RuntimeAssets = runtimeAssets;
DocumentSha256 = documentSha256;
GeneratorSourceSha256 = generatorSourceSha256;
_byId = new ReadOnlyDictionary<string, LegacyFixedAction>(
actions.ToDictionary(action => action.Id, StringComparer.Ordinal));
_byMarket = new ReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>>(
Enum.GetValues<LegacyFixedMarket>().ToDictionary(
market => market,
market => ReadOnly(actions.Where(action => action.Market == market))));
var sectionsByMarket = new Dictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>();
var bySection = new Dictionary<(LegacyFixedMarket Market, string Section),
IReadOnlyList<LegacyFixedAction>>();
foreach (var market in Enum.GetValues<LegacyFixedMarket>())
{
var sections = _byMarket[market]
.GroupBy(action => action.Section, StringComparer.Ordinal)
.Select(group =>
{
var sectionActions = ReadOnly(group);
bySection.Add((market, group.Key), sectionActions);
return new LegacyFixedSection(market, group.Key, sectionActions);
});
sectionsByMarket.Add(market, ReadOnly(sections));
}
_sectionsByMarket =
new ReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>(
sectionsByMarket);
_bySection = new ReadOnlyDictionary<(LegacyFixedMarket Market, string Section),
IReadOnlyList<LegacyFixedAction>>(bySection);
}
public static LegacyFixedActionCatalog Default => EmbeddedCatalog.Value;
public IReadOnlyList<LegacyFixedAction> Actions { get; }
public IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset> RuntimeAssets { get; }
public string DocumentSha256 { get; }
public string GeneratorSourceSha256 { get; }
public IReadOnlyList<LegacyFixedAction> GetActions(LegacyFixedMarket market)
{
RequireMarket(market);
return _byMarket[market];
}
public IReadOnlyList<LegacyFixedSection> GetSections(LegacyFixedMarket market)
{
RequireMarket(market);
return _sectionsByMarket[market];
}
public IReadOnlyList<LegacyFixedAction> GetActions(
LegacyFixedMarket market,
string section)
{
RequireMarket(market);
ArgumentException.ThrowIfNullOrWhiteSpace(section);
return _bySection.TryGetValue((market, section), out var actions)
? actions
: Array.Empty<LegacyFixedAction>();
}
public LegacyFixedAction? FindAction(string? actionId) =>
actionId is not null && _byId.TryGetValue(actionId, out var action)
? action
: null;
public LegacyFixedAction GetAction(string actionId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(actionId);
return _byId.TryGetValue(actionId, out var action)
? action
: throw new KeyNotFoundException($"Unknown fixed legacy action id '{actionId}'.");
}
public LegacyFixedRuntimeAsset GetRuntimeAsset(LegacyFixedMarket market)
{
RequireMarket(market);
return RuntimeAssets[market];
}
internal LegacyFixedActionCatalog WithRuntimeOrder(
IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>> actionsByMarket)
{
ArgumentNullException.ThrowIfNull(actionsByMarket);
var ordered = new List<LegacyFixedAction>(Actions.Count);
foreach (var market in Enum.GetValues<LegacyFixedMarket>())
{
var source = actionsByMarket.TryGetValue(market, out var runtimeActions)
? runtimeActions
: _byMarket[market];
if (source is null || source.Count != _byMarket[market].Count ||
source.Any(action => action is null || action.Market != market) ||
!source.Select(action => action.Id).ToHashSet(StringComparer.Ordinal)
.SetEquals(_byMarket[market].Select(action => action.Id)))
{
throw new InvalidDataException(
$"The runtime fixed-action order for '{MarketText(market)}' is invalid.");
}
ordered.AddRange(source);
}
return new LegacyFixedActionCatalog(
ReadOnly(ordered),
RuntimeAssets,
DocumentSha256,
GeneratorSourceSha256);
}
public LegacyMaterializedFixedAction Materialize(
string actionId,
DateTimeOffset localNow)
{
var action = GetAction(actionId);
if (!action.Available)
{
throw new InvalidOperationException(action.Prerequisite);
}
var graphicType = action.DynamicNxtSession
? localNow.Hour <= 12 ? "PRE_MARKET" : "AFTER_MARKET"
: action.Selection.GraphicType;
var dataCode = action.DynamicDate
? localNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
: action.Selection.DataCode;
var selection = action.Selection with
{
GraphicType = graphicType,
DataCode = dataCode
};
return new LegacyMaterializedFixedAction(
action,
selection,
RuntimeAssets[action.Market]);
}
internal static LegacyFixedActionCatalog LoadForValidation(
ReadOnlyMemory<byte> utf8Json,
string? expectedDocumentSha256 = null) =>
Load(utf8Json.ToArray(), expectedDocumentSha256);
private static LegacyFixedActionCatalog LoadEmbedded()
{
using var stream = typeof(LegacyFixedActionCatalog).Assembly
.GetManifestResourceStream(ResourceName)
?? throw new InvalidDataException(
$"The embedded fixed legacy catalog '{ResourceName}' is missing.");
using var buffer = new MemoryStream();
stream.CopyTo(buffer);
return Load(buffer.ToArray(), ExpectedDocumentSha256);
}
private static LegacyFixedActionCatalog Load(byte[] utf8Json, string? expectedDocumentSha256)
{
ArgumentNullException.ThrowIfNull(utf8Json);
if (utf8Json.Length == 0)
{
throw new InvalidDataException("The fixed legacy catalog is empty.");
}
CatalogDocumentDto document;
try
{
using var parsed = JsonDocument.Parse(utf8Json, new JsonDocumentOptions
{
AllowTrailingCommas = false,
CommentHandling = JsonCommentHandling.Disallow
});
RejectDuplicateProperties(parsed.RootElement, "$root");
document = JsonSerializer.Deserialize<CatalogDocumentDto>(utf8Json, StrictJsonOptions)
?? throw new InvalidDataException("The fixed legacy catalog root is null.");
}
catch (JsonException exception)
{
throw new InvalidDataException("The fixed legacy catalog JSON schema is invalid.", exception);
}
var documentHash = Convert.ToHexString(SHA256.HashData(utf8Json));
if (expectedDocumentSha256 is not null &&
!string.Equals(documentHash, expectedDocumentSha256, StringComparison.Ordinal))
{
throw new InvalidDataException(
$"The fixed legacy catalog hash is invalid. Expected {expectedDocumentSha256}; actual {documentHash}.");
}
ValidateDocumentMetadata(document);
var runtimeAssets = CreateRuntimeAssets(document.RuntimeAssets);
var actions = CreateActions(document.Actions);
ValidateCatalogInvariants(actions);
return new LegacyFixedActionCatalog(
ReadOnly(actions),
runtimeAssets,
documentHash,
document.SourceSha256);
}
private static void ValidateDocumentMetadata(CatalogDocumentDto document)
{
if (document.SchemaVersion != 1)
{
throw Invalid("schemaVersion must be 1");
}
if (!string.Equals(document.GeneratedFrom, ExpectedGeneratedFrom, StringComparison.Ordinal))
{
throw Invalid($"generatedFrom must be '{ExpectedGeneratedFrom}'");
}
if (!string.Equals(
document.SourceSha256,
ExpectedGeneratorSourceSha256,
StringComparison.Ordinal))
{
throw Invalid("the generator source hash is not approved");
}
if (document.RuntimeAssets is null)
{
throw Invalid("runtimeAssets is required");
}
if (document.Actions is null)
{
throw Invalid("actions is required");
}
}
private static IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>
CreateRuntimeAssets(RuntimeAssetsDto runtimeAssets)
{
var assets = new Dictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>
{
[LegacyFixedMarket.Overseas] = ValidateRuntimeAsset(
LegacyFixedMarket.Overseas,
runtimeAssets.Overseas,
"bin/Debug/Res/해외.ini",
"DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A"),
[LegacyFixedMarket.Exchange] = ValidateRuntimeAsset(
LegacyFixedMarket.Exchange,
runtimeAssets.Exchange,
"bin/Debug/Res/환율.ini",
"7A99F8DFB5DD8D8DB514605F232A0A7973545CA6A4A46ABE2D102ADD025B586C"),
[LegacyFixedMarket.Index] = ValidateRuntimeAsset(
LegacyFixedMarket.Index,
runtimeAssets.Index,
"bin/Debug/Res/지수.ini",
"E90ED317FD3ECC1A716AD17DD9BC21FE8B838574E1C20854BD0A886A96BA17E6")
};
return new ReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>(assets);
}
private static LegacyFixedRuntimeAsset ValidateRuntimeAsset(
LegacyFixedMarket market,
RuntimeAssetDto asset,
string expectedPath,
string expectedSha256)
{
if (asset is null ||
!string.Equals(asset.RelativePath, expectedPath, StringComparison.Ordinal) ||
!string.Equals(asset.Sha256, expectedSha256, StringComparison.Ordinal))
{
throw Invalid($"runtime asset metadata for '{MarketText(market)}' is invalid");
}
RequireSha256(asset.Sha256, $"runtimeAssets.{MarketText(market)}.sha256");
return new LegacyFixedRuntimeAsset(market, asset.RelativePath, asset.Sha256);
}
private static LegacyFixedAction[] CreateActions(IReadOnlyList<ActionDto> source)
{
if (source.Count != ExpectedActionCount)
{
throw Invalid($"exactly {ExpectedActionCount} actions are required");
}
var actions = new LegacyFixedAction[source.Count];
for (var index = 0; index < source.Count; index++)
{
var dto = source[index]
?? throw Invalid($"action at index {index} is null");
var expectedId = $"fixed-{index + 1:000}";
if (!string.Equals(dto.Id, expectedId, StringComparison.Ordinal))
{
throw Invalid($"action id at index {index} must be '{expectedId}'");
}
var market = ParseMarket(dto.Market);
var category = ParseCategory(dto.Category);
var section = RequireText(dto.Section, $"{expectedId}.section");
var label = RequireText(dto.Label, $"{expectedId}.label");
var detail = RequireText(dto.Detail, $"{expectedId}.detail");
var builderKey = RequireText(dto.BuilderKey, $"{expectedId}.builderKey");
var code = RequireText(dto.Code, $"{expectedId}.code");
ValidateSceneIdentity(expectedId, builderKey, code, dto.Aliases);
var selection = ValidateSelection(expectedId, dto);
var prerequisite = RequireText(
dto.Prerequisite,
$"{expectedId}.prerequisite",
allowEmpty: true,
maximumLength: 512);
if (dto.Available != string.IsNullOrEmpty(prerequisite))
{
throw Invalid($"{expectedId} availability and prerequisite do not agree");
}
actions[index] = new LegacyFixedAction(
expectedId,
market,
section,
label,
detail,
category,
builderKey,
code,
Array.AsReadOnly(dto.Aliases.ToArray()),
dto.Available,
prerequisite,
selection,
dto.DynamicDate,
dto.DynamicNxtSession);
}
return actions;
}
private static void ValidateSceneIdentity(
string actionId,
string builderKey,
string code,
IReadOnlyList<string> aliases)
{
if (aliases is null)
{
throw Invalid($"{actionId}.aliases is required");
}
var definition = LegacySceneCatalog.Definitions.SingleOrDefault(candidate =>
string.Equals(candidate.BuilderKey, builderKey, StringComparison.Ordinal))
?? throw Invalid($"{actionId} has unknown builder '{builderKey}'");
if (!definition.CutAliases.Contains(code, StringComparer.Ordinal))
{
throw Invalid($"{actionId} code '{code}' is not an alias of '{builderKey}'");
}
if (aliases.Count != 1 || !string.Equals(aliases[0], code, StringComparison.Ordinal))
{
throw Invalid($"{actionId} must declare exactly its code as its sole alias");
}
var aliasMatches = LegacySceneCatalog.FindByCutAlias(code);
if (!aliasMatches.Any(candidate =>
string.Equals(candidate.BuilderKey, builderKey, StringComparison.Ordinal)))
{
throw Invalid($"{actionId} alias '{code}' does not resolve to '{builderKey}'");
}
}
private static LegacyFixedSelection ValidateSelection(string actionId, ActionDto action)
{
var source = action.Selection
?? throw Invalid($"{actionId}.selection is required");
var selection = new LegacyFixedSelection(
RequireText(source.GroupCode, $"{actionId}.selection.groupCode"),
RequireText(source.Subject, $"{actionId}.selection.subject", allowEmpty: true),
RequireText(source.GraphicType, $"{actionId}.selection.graphicType", allowEmpty: true),
RequireText(source.Subtype, $"{actionId}.selection.subtype", allowEmpty: true),
RequireText(source.DataCode, $"{actionId}.selection.dataCode", allowEmpty: true));
if (action.DynamicDate && action.DynamicNxtSession)
{
throw Invalid($"{actionId} cannot have two dynamic selection fields");
}
if (action.DynamicDate !=
string.Equals(selection.DataCode, "$TODAY", StringComparison.Ordinal))
{
throw Invalid($"{actionId} has an invalid dynamic-date declaration");
}
if (action.DynamicNxtSession !=
string.Equals(selection.GraphicType, "$NXT_SESSION", StringComparison.Ordinal))
{
throw Invalid($"{actionId} has an invalid NXT-session declaration");
}
if (string.Equals(action.BuilderKey, "s8010", StringComparison.Ordinal) &&
!string.IsNullOrEmpty(selection.GraphicType))
{
throw Invalid($"{actionId} moving-average flags must be materialized by C#");
}
var values = new[]
{
selection.GroupCode,
selection.Subject,
selection.GraphicType,
selection.Subtype,
selection.DataCode
};
if (values.Any(value => value.Contains('$', StringComparison.Ordinal)) &&
!action.DynamicDate && !action.DynamicNxtSession)
{
throw Invalid($"{actionId} contains an unapproved selection placeholder");
}
return selection;
}
private static void ValidateCatalogInvariants(IReadOnlyList<LegacyFixedAction> actions)
{
RequireCount(actions, LegacyFixedMarket.Overseas, ExpectedOverseasCount);
RequireCount(actions, LegacyFixedMarket.Exchange, ExpectedExchangeCount);
RequireCount(actions, LegacyFixedMarket.Index, ExpectedIndexCount);
if (actions.Count(action => action.Available) != ExpectedAvailableCount)
{
throw Invalid($"exactly {ExpectedAvailableCount} actions must be available");
}
if (actions.Take(ExpectedOverseasCount).Any(action =>
action.Market != LegacyFixedMarket.Overseas) ||
actions.Skip(ExpectedOverseasCount).Take(ExpectedExchangeCount).Any(action =>
action.Market != LegacyFixedMarket.Exchange) ||
actions.Skip(ExpectedOverseasCount + ExpectedExchangeCount).Any(action =>
action.Market != LegacyFixedMarket.Index))
{
throw Invalid("market action ranges are not in the approved legacy order");
}
}
private static void RequireCount(
IEnumerable<LegacyFixedAction> actions,
LegacyFixedMarket market,
int expected)
{
var actual = actions.Count(action => action.Market == market);
if (actual != expected)
{
throw Invalid(
$"market '{MarketText(market)}' must contain {expected} actions, not {actual}");
}
}
private static LegacyFixedMarket ParseMarket(string value) => value switch
{
"overseas" => LegacyFixedMarket.Overseas,
"exchange" => LegacyFixedMarket.Exchange,
"index" => LegacyFixedMarket.Index,
_ => throw Invalid($"unknown market '{value}'")
};
private static LegacyFixedCategory ParseCategory(string value) => value switch
{
"plate" => LegacyFixedCategory.Plate,
"market" => LegacyFixedCategory.Market,
"chart" => LegacyFixedCategory.Chart,
_ => throw Invalid($"unknown category '{value}'")
};
private static string MarketText(LegacyFixedMarket market) => market switch
{
LegacyFixedMarket.Overseas => "overseas",
LegacyFixedMarket.Exchange => "exchange",
LegacyFixedMarket.Index => "index",
_ => throw new ArgumentOutOfRangeException(nameof(market), market, "Unknown market.")
};
private static void RequireMarket(LegacyFixedMarket market)
{
if (!Enum.IsDefined(market))
{
throw new ArgumentOutOfRangeException(nameof(market), market, "Unknown market.");
}
}
private static string RequireText(
string value,
string field,
bool allowEmpty = false,
int maximumLength = 256)
{
if (value is null || (!allowEmpty && value.Length == 0) ||
value.Length > maximumLength || value.Any(char.IsControl) ||
!string.Equals(value, value.Trim(), StringComparison.Ordinal))
{
throw Invalid($"{field} is invalid");
}
return value;
}
private static void RequireSha256(string value, string field)
{
if (value is null || value.Length != 64 || value.Any(character =>
!(char.IsAsciiDigit(character) || character is >= 'A' and <= 'F')))
{
throw Invalid($"{field} is not an uppercase SHA-256 value");
}
}
private static void RejectDuplicateProperties(JsonElement element, string path)
{
if (element.ValueKind == JsonValueKind.Object)
{
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var property in element.EnumerateObject())
{
if (!names.Add(property.Name))
{
throw new JsonException($"Duplicate property '{property.Name}' at {path}.");
}
RejectDuplicateProperties(property.Value, $"{path}.{property.Name}");
}
}
else if (element.ValueKind == JsonValueKind.Array)
{
var index = 0;
foreach (var item in element.EnumerateArray())
{
RejectDuplicateProperties(item, $"{path}[{index++}]");
}
}
}
private static IReadOnlyList<T> ReadOnly<T>(IEnumerable<T> source) =>
Array.AsReadOnly(source.ToArray());
private static InvalidDataException Invalid(string message) =>
new($"The fixed legacy catalog is invalid: {message}.");
private sealed class CatalogDocumentDto
{
[JsonPropertyName("schemaVersion")]
public required int SchemaVersion { get; init; }
[JsonPropertyName("generatedFrom")]
public required string GeneratedFrom { get; init; }
[JsonPropertyName("sourceSha256")]
public required string SourceSha256 { get; init; }
[JsonPropertyName("runtimeAssets")]
public required RuntimeAssetsDto RuntimeAssets { get; init; }
[JsonPropertyName("actions")]
public required List<ActionDto> Actions { get; init; }
}
private sealed class RuntimeAssetsDto
{
[JsonPropertyName("overseas")]
public required RuntimeAssetDto Overseas { get; init; }
[JsonPropertyName("exchange")]
public required RuntimeAssetDto Exchange { get; init; }
[JsonPropertyName("index")]
public required RuntimeAssetDto Index { get; init; }
}
private sealed class RuntimeAssetDto
{
[JsonPropertyName("relativePath")]
public required string RelativePath { get; init; }
[JsonPropertyName("sha256")]
public required string Sha256 { get; init; }
}
private sealed class ActionDto
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("market")]
public required string Market { get; init; }
[JsonPropertyName("section")]
public required string Section { get; init; }
[JsonPropertyName("label")]
public required string Label { get; init; }
[JsonPropertyName("detail")]
public required string Detail { get; init; }
[JsonPropertyName("category")]
public required string Category { get; init; }
[JsonPropertyName("builderKey")]
public required string BuilderKey { get; init; }
[JsonPropertyName("code")]
public required string Code { get; init; }
[JsonPropertyName("aliases")]
public required List<string> Aliases { get; init; }
[JsonPropertyName("available")]
public required bool Available { get; init; }
[JsonPropertyName("prerequisite")]
public required string Prerequisite { get; init; }
[JsonPropertyName("selection")]
public required SelectionDto Selection { get; init; }
[JsonPropertyName("dynamicDate")]
public required bool DynamicDate { get; init; }
[JsonPropertyName("dynamicNxtSession")]
public required bool DynamicNxtSession { get; init; }
}
private sealed class SelectionDto
{
[JsonPropertyName("groupCode")]
public required string GroupCode { get; init; }
[JsonPropertyName("subject")]
public required string Subject { get; init; }
[JsonPropertyName("graphicType")]
public required string GraphicType { get; init; }
[JsonPropertyName("subtype")]
public required string Subtype { get; init; }
[JsonPropertyName("dataCode")]
public required string DataCode { get; init; }
}
}