Files
MBN_STOCK_WEBVIEW/src/MBN_STOCK_WEBVIEW.LegacyBridge/LegacyBridgeProtocol.cs

2738 lines
109 KiB
C#

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyBridge;
public abstract record LegacyUiIntent;
public sealed record LegacyReadyIntent : LegacyUiIntent;
public sealed record LegacySearchStocksIntent(string Text, LegacySearchTrigger Trigger)
: LegacyUiIntent;
public sealed record LegacySelectStockIntent(int ResultIndex) : LegacyUiIntent;
public sealed record LegacyCutPointerDownIntent(
int PhysicalIndex,
bool Control,
bool Shift,
int X,
int Y,
long TimestampMilliseconds,
bool AllowAppend) : LegacyUiIntent;
public sealed record LegacyCutKeyDownIntent(
LegacyCutKeyboardKey Key,
bool Control,
bool Shift) : LegacyUiIntent;
public sealed record LegacyClearCutSelectionIntent : LegacyUiIntent;
public sealed record LegacyDropSelectedCutsIntent : LegacyUiIntent;
public sealed record LegacySetPlaylistEnabledIntent(string RowId, bool Enabled)
: LegacyUiIntent;
public sealed record LegacySetAllPlaylistEnabledIntent(bool Enabled)
: LegacyUiIntent;
public sealed record LegacySelectPlaylistRowIntent(
string RowId,
bool Control,
bool Shift) : LegacyUiIntent;
public sealed record LegacyActivatePlaylistRowIntent(string RowId) : LegacyUiIntent;
public sealed record LegacyMoveSelectedPlaylistRowsIntent(
LegacyPlaylistMoveDirection Direction) : LegacyUiIntent;
public sealed record LegacyReorderPlaylistRowsIntent(
string SourceRowId,
string TargetRowId,
LegacyDragDropPosition Position) : LegacyUiIntent;
public sealed record LegacyDeleteSelectedPlaylistRowsIntent : LegacyUiIntent;
public sealed record LegacyDeleteAllPlaylistRowsIntent : LegacyUiIntent;
public sealed record LegacyToggleActivePlaylistEnabledIntent : LegacyUiIntent;
public sealed record LegacySelectPlaylistBoundaryIntent(bool Last) : LegacyUiIntent;
public sealed record LegacySelectTabIntent(LegacyOperatorTabId TabId) : LegacyUiIntent;
public sealed record LegacySwapTabsIntent(
LegacyOperatorTabId Source,
LegacyOperatorTabId Target) : LegacyUiIntent;
public sealed record LegacyHoverSwapTabsIntent(
LegacyOperatorTabId Source,
LegacyOperatorTabId Target,
int ExpectedSourceIndex,
int ExpectedTargetIndex) : LegacyUiIntent;
public sealed record LegacySetMovingAveragesIntent(bool Ma5, bool Ma20) : LegacyUiIntent;
public sealed record LegacyActivateFixedActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacyActivateFixedSectionIntent(int SectionIndex) : LegacyUiIntent;
public sealed record LegacySelectIndustryIntent(int Index, bool DoubleClick) : LegacyUiIntent;
public sealed record LegacySetIndustryComparisonTextIntent(
LegacyIndustryComparisonSlot Slot,
string Value) : LegacyUiIntent;
public sealed record LegacySwapIndustriesIntent : LegacyUiIntent;
public sealed record LegacyActivateIndustryActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacyActivateIndustrySectionIntent(int SectionIndex) : LegacyUiIntent;
public sealed record LegacySelectOverseasFixedIndexIntent(string TargetId) : LegacyUiIntent;
public sealed record LegacySearchOverseasIndustriesIntent(string Query) : LegacyUiIntent;
public sealed record LegacySearchOverseasStocksIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectOverseasIndustryIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacySelectOverseasStockIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacyActivateOverseasActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacySearchComparisonDomesticIntent(string Query) : LegacyUiIntent;
public sealed record LegacySearchComparisonWorldIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectComparisonDomesticIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacySelectComparisonWorldIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacySelectComparisonFixedIntent(string TargetId) : LegacyUiIntent;
public sealed record LegacyChooseComparisonDomesticIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacyChooseComparisonWorldIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacyChooseComparisonFixedIntent(string TargetId) : LegacyUiIntent;
public sealed record LegacySwapComparisonTargetsIntent : LegacyUiIntent;
public sealed record LegacyClearComparisonTargetsIntent : LegacyUiIntent;
public sealed record LegacyAddComparisonPairIntent : LegacyUiIntent;
public sealed record LegacySelectComparisonPairIntent(string RowId) : LegacyUiIntent;
public sealed record LegacyMoveComparisonPairIntent(LegacyComparisonPairMoveDirection Direction)
: LegacyUiIntent;
public sealed record LegacyDeleteSelectedComparisonPairIntent : LegacyUiIntent;
public sealed record LegacyDeleteAllComparisonPairsIntent : LegacyUiIntent;
public sealed record LegacyActivateComparisonActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacyActivateComparisonSectionIntent(int SectionIndex) : LegacyUiIntent;
// UC4 has no NXT-session control. The native controller chooses it from its
// local clock, so the WebView can supply only the search text.
public sealed record LegacySearchThemesIntent(string Query)
: LegacyUiIntent;
public sealed record LegacySelectThemeIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyActivateThemeResultIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacySetThemeSortIntent(string SortId) : LegacyUiIntent;
public sealed record LegacyActivateThemeActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacySearchExpertsIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectExpertIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacyActivateExpertActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacySearchTradingHaltsIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectTradingHaltIntent(string SelectionId) : LegacyUiIntent;
public sealed record LegacyToggleTradingHaltSortIntent : LegacyUiIntent;
public sealed record LegacyActivateTradingHaltActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacyOpenOperatorCatalogIntent(LegacyOperatorCatalogEntity Entity)
: LegacyUiIntent;
public sealed record LegacyBeginUc4ThemeCatalogIntent : LegacyUiIntent;
public sealed record LegacyEditUc4SelectedThemeIntent : LegacyUiIntent;
public sealed record LegacyDeleteUc4SelectedThemeIntent : LegacyUiIntent;
public sealed record LegacyBeginUc6ExpertCatalogIntent : LegacyUiIntent;
public sealed record LegacyEditUc6SelectedExpertIntent : LegacyUiIntent;
public sealed record LegacyDeleteUc6SelectedExpertIntent : LegacyUiIntent;
public sealed record LegacyCloseOperatorCatalogIntent : LegacyUiIntent;
public sealed record LegacySearchOperatorCatalogIntent(string Query, ThemeSession NxtSession)
: LegacyUiIntent;
public sealed record LegacySelectOperatorCatalogResultIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyBeginCreateThemeCatalogIntent(ThemeMarket Market) : LegacyUiIntent;
public sealed record LegacyBeginCreateExpertCatalogIntent : LegacyUiIntent;
public sealed record LegacySearchOperatorCatalogStocksIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectOperatorCatalogStockIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyAddOperatorCatalogStockIntent(decimal? BuyAmount) : LegacyUiIntent;
public sealed record LegacyActivateOperatorCatalogStockIntent(
string ResultId,
decimal? BuyAmount) : LegacyUiIntent;
public sealed record LegacyMoveOperatorCatalogDraftRowIntent(
string RowId,
LegacyOperatorCatalogMoveDirection Direction) : LegacyUiIntent;
public sealed record LegacyReorderOperatorCatalogDraftRowIntent(
string SourceRowId,
string TargetRowId,
LegacyDragDropPosition Position) : LegacyUiIntent;
public sealed record LegacyRemoveOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent;
public sealed record LegacySelectOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent;
public sealed record LegacyRemoveActiveOperatorCatalogDraftRowIntent : LegacyUiIntent;
public sealed record LegacyClearOperatorCatalogDraftRowsIntent : LegacyUiIntent;
public sealed record LegacySetOperatorCatalogDraftBuyAmountIntent(
string RowId,
decimal? BuyAmount) : LegacyUiIntent;
public sealed record LegacyCheckOperatorCatalogThemeNameIntent(string Name) : LegacyUiIntent;
public sealed record LegacySaveOperatorCatalogIntent(string Name) : LegacyUiIntent;
public sealed record LegacyConfirmNativeDialogIntent : LegacyUiIntent;
public sealed record LegacyCancelNativeDialogIntent : LegacyUiIntent;
public sealed record LegacyOpenManualFinancialIntent(ManualFinancialScreenKind Screen)
: LegacyUiIntent;
public sealed record LegacyCloseManualFinancialIntent : LegacyUiIntent;
public sealed record LegacySearchManualFinancialIntent(string Query) : LegacyUiIntent;
public sealed record LegacyFindManualFinancialIntent(
string Query,
LegacyManualFinancialFindDirection Direction,
long Generation) : LegacyUiIntent;
public sealed record LegacyToggleManualFinancialNameSortIntent(long Generation) : LegacyUiIntent;
public sealed record LegacyLoadManualFinancialIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyActivateManualFinancialResultIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyBeginManualFinancialCreateIntent : LegacyUiIntent;
public sealed record LegacySearchManualFinancialStocksIntent(string StockName) : LegacyUiIntent;
public sealed record LegacySelectManualFinancialStockIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacySaveManualFinancialIntent(ManualFinancialRecord Record)
: LegacyUiIntent;
public sealed record LegacyDeleteManualFinancialIntent : LegacyUiIntent;
public sealed record LegacyDeleteAllManualFinancialIntent : LegacyUiIntent;
public sealed record LegacyActivateManualFinancialActionIntent(string ActionId) : LegacyUiIntent;
public sealed record LegacyOpenManualListIntent(
LegacyManualListScreen Screen,
S5025ManualAudience Audience) : LegacyUiIntent;
public sealed record LegacyCloseManualListIntent : LegacyUiIntent;
public sealed record LegacyRefreshManualListIntent : LegacyUiIntent;
public sealed record LegacySetManualNetSellCellIntent(
string RowId,
LegacyManualNetSellField Field,
string Value) : LegacyUiIntent;
public sealed record LegacySaveManualNetSellIntent : LegacyUiIntent;
public sealed record LegacyAddManualNetSellCutIntent : LegacyUiIntent;
public sealed record LegacySearchManualViIntent(string Query) : LegacyUiIntent;
public sealed record LegacySelectManualViResultIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyAddManualViResultIntent(string ResultId) : LegacyUiIntent;
public sealed record LegacyMoveManualViItemIntent(
string ItemId,
LegacyManualListMoveDirection Direction) : LegacyUiIntent;
public sealed record LegacyDeleteManualViItemIntent(string ItemId) : LegacyUiIntent;
public sealed record LegacyDeleteAllManualViItemsIntent : LegacyUiIntent;
public sealed record LegacySaveManualViIntent : LegacyUiIntent;
public sealed record LegacyAddManualViCutIntent : LegacyUiIntent;
public sealed record LegacyConfirmManualListIntent : LegacyUiIntent;
public sealed record LegacyImportManualListsIntent : LegacyUiIntent;
public sealed record LegacyRefreshNamedPlaylistsIntent(string RequestId) : LegacyUiIntent;
public sealed record LegacySelectNamedPlaylistIntent(
string DefinitionId,
string RequestId) : LegacyUiIntent;
public sealed record LegacyLoadSelectedNamedPlaylistIntent : LegacyUiIntent;
public sealed record LegacyLoadNamedPlaylistByIdIntent(string DefinitionId) : LegacyUiIntent;
public sealed record LegacyCreateNamedPlaylistIntent(string Title, string RequestId) : LegacyUiIntent;
public sealed record LegacySaveCurrentNamedPlaylistIntent : LegacyUiIntent;
public sealed record LegacySaveCurrentNamedPlaylistToIntent(string DefinitionId) : LegacyUiIntent;
public sealed record LegacyDeleteSelectedNamedPlaylistIntent(string RequestId) : LegacyUiIntent;
public sealed record LegacyDismissDialogIntent : LegacyUiIntent;
public sealed record LegacyInteractionMetrics(
int CutDragWidthDips,
int CutDragHeightDips,
uint SourceDpi = 96,
double HostRasterizationScale = 1d)
{
public const uint CssPixelsPerInch = 96;
public const int FallbackCutDragWidth = 4;
public const int FallbackCutDragHeight = 4;
public static LegacyInteractionMetrics Default { get; } = new(
FallbackCutDragWidth,
FallbackCutDragHeight,
CssPixelsPerInch,
1d);
public static LegacyInteractionMetrics FromPixelsAtDpi(
int cutDragWidthPixels,
int cutDragHeightPixels,
uint sourceDpi,
double hostRasterizationScale = 1d) =>
sourceDpi == 0
? Default with
{
HostRasterizationScale = NormalizeHostScale(hostRasterizationScale)
}
: new(
ToDips(
cutDragWidthPixels,
sourceDpi,
FallbackCutDragWidth),
ToDips(
cutDragHeightPixels,
sourceDpi,
FallbackCutDragHeight),
sourceDpi,
NormalizeHostScale(hostRasterizationScale));
internal static LegacyInteractionMetrics Normalize(LegacyInteractionMetrics? metrics) =>
metrics is null
? Default
: new(
metrics.CutDragWidthDips > 0
? metrics.CutDragWidthDips
: FallbackCutDragWidth,
metrics.CutDragHeightDips > 0
? metrics.CutDragHeightDips
: FallbackCutDragHeight,
metrics.SourceDpi > 0 ? metrics.SourceDpi : CssPixelsPerInch,
NormalizeHostScale(metrics.HostRasterizationScale));
private static double NormalizeHostScale(double hostRasterizationScale) =>
double.IsFinite(hostRasterizationScale) && hostRasterizationScale > 0d
? hostRasterizationScale
: 1d;
private static int ToDips(int pixelsAtSourceDpi, uint sourceDpi, int fallback) =>
pixelsAtSourceDpi <= 0
? fallback
: Math.Max(
1,
(int)Math.Round(
pixelsAtSourceDpi * (double)CssPixelsPerInch / sourceDpi,
MidpointRounding.AwayFromZero));
}
public static class LegacyBridgeProtocol
{
public const string TrustedHost = "legacy-parity.mbn.local";
private static readonly JsonSerializerOptions SerializerOptions = CreateSerializerOptions();
public static bool IsTrustedSource(string? source)
{
if (!Uri.TryCreate(source, UriKind.Absolute, out var uri))
{
return false;
}
return string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
string.Equals(uri.Host, TrustedHost, StringComparison.OrdinalIgnoreCase) &&
uri.IsDefaultPort;
}
public static bool TryParseIntent(
string json,
out LegacyUiIntent? intent,
out string? error)
{
intent = null;
error = null;
try
{
using var document = JsonDocument.Parse(json, new JsonDocumentOptions
{
AllowTrailingCommas = false,
CommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8
});
var root = document.RootElement;
if (root.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(root, "type", "payload") ||
!TryString(root, "type", 64, out var type) ||
!root.TryGetProperty("payload", out var payload) ||
payload.ValueKind != JsonValueKind.Object)
{
error = "올바르지 않은 UI 요청입니다.";
return false;
}
intent = type switch
{
"ready" => ParseReady(payload),
"search-stocks" => ParseSearch(payload),
"select-stock" => ParseStockSelection(payload),
"cut-pointer-down" => ParseCutPointer(payload),
"cut-key-down" => ParseCutKeyDown(payload),
"clear-cut-selection" => ParseClearCutSelection(payload),
"drop-selected-cuts" => ParseDrop(payload),
"set-playlist-enabled" => ParseEnabled(payload),
"set-all-playlist-enabled" => ParseAllEnabled(payload),
"select-playlist-row" => ParsePlaylistSelection(payload),
"activate-playlist-row" => ParsePlaylistActivation(payload),
"move-selected-playlist-rows" => ParsePlaylistMove(payload),
"reorder-playlist-rows" => ParsePlaylistReorder(payload),
"delete-selected-playlist-rows" => ParseDeleteSelected(payload),
"delete-all-playlist-rows" => ParseDeleteAll(payload),
"toggle-active-playlist-enabled" => ParseToggleActiveEnabled(payload),
"select-playlist-boundary" => ParsePlaylistBoundary(payload),
"select-tab" => ParseTabSelection(payload),
"swap-tabs" => ParseTabSwap(payload),
"hover-swap-tabs" => ParseTabHoverSwap(payload),
"set-moving-averages" => ParseMovingAverages(payload),
"activate-fixed-action" => ParseFixedAction(payload),
"activate-fixed-section" => ParseFixedSection(payload),
"select-industry" => ParseIndustrySelection(payload, false),
"double-click-industry" => ParseIndustrySelection(payload, true),
"set-industry-comparison-text" => ParseIndustryComparisonText(payload),
"swap-industries" => ParseSwapIndustries(payload),
"activate-industry-action" => ParseIndustryAction(payload),
"activate-industry-section" => ParseSectionIndex(
payload,
static index => new LegacyActivateIndustrySectionIntent(index)),
"select-overseas-fixed-index" => ParseOverseasFixedIndex(payload),
"search-overseas-industries" => ParseOverseasSearch(payload, false),
"search-overseas-stocks" => ParseOverseasSearch(payload, true),
"select-overseas-industry" => ParseOverseasSelection(payload, false),
"select-overseas-stock" => ParseOverseasSelection(payload, true),
"activate-overseas-action" => ParseOverseasAction(payload),
"search-comparison-domestic" => ParseComparisonSearch(payload, false),
"search-comparison-world" => ParseComparisonSearch(payload, true),
"select-comparison-domestic" => ParseComparisonSelection(payload, false),
"select-comparison-world" => ParseComparisonSelection(payload, true),
"select-comparison-fixed" => ParseComparisonFixedSelection(payload),
"choose-comparison-domestic" => ParseComparisonChoice(payload, false),
"choose-comparison-world" => ParseComparisonChoice(payload, true),
"choose-comparison-fixed" => ParseComparisonFixed(payload),
"swap-comparison-targets" => ParseComparisonSwap(payload),
"clear-comparison-targets" => ParseComparisonClear(payload),
"add-comparison-pair" => ParseComparisonAdd(payload),
"select-comparison-pair" => ParseComparisonPairSelection(payload),
"move-comparison-pair" => ParseComparisonPairMove(payload),
"delete-selected-comparison-pair" => ParseComparisonDeleteSelected(payload),
"delete-all-comparison-pairs" => ParseComparisonDeleteAll(payload),
"activate-comparison-action" => ParseComparisonAction(payload),
"activate-comparison-section" => ParseSectionIndex(
payload,
static index => new LegacyActivateComparisonSectionIntent(index)),
"search-themes" => ParseThemeSearch(payload),
"select-theme" => ParseThemeSelection(payload),
"activate-theme-result" => ParseThemeResultActivation(payload),
"set-theme-sort" => ParseThemeSort(payload),
"activate-theme-action" => ParseThemeAction(payload),
"search-experts" => ParseExpertSearch(payload),
"select-expert" => ParseExpertSelection(payload),
"activate-expert-action" => ParseExpertAction(payload),
"search-trading-halts" => ParseTradingHaltSearch(payload),
"select-trading-halt" => ParseTradingHaltSelection(payload),
"toggle-trading-halt-sort" => ParseTradingHaltSort(payload),
"activate-trading-halt-action" => ParseTradingHaltAction(payload),
"open-operator-catalog" => ParseOperatorCatalogOpen(payload),
"begin-uc4-theme-catalog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyBeginUc4ThemeCatalogIntent()),
"edit-uc4-selected-theme" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyEditUc4SelectedThemeIntent()),
"delete-uc4-selected-theme" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyDeleteUc4SelectedThemeIntent()),
"begin-uc6-expert-catalog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyBeginUc6ExpertCatalogIntent()),
"edit-uc6-selected-expert" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyEditUc6SelectedExpertIntent()),
"delete-uc6-selected-expert" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyDeleteUc6SelectedExpertIntent()),
"close-operator-catalog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyCloseOperatorCatalogIntent()),
"search-operator-catalog" => ParseOperatorCatalogSearch(payload),
"select-operator-catalog-result" => ParseOperatorCatalogResult(payload),
"begin-create-theme-catalog" => ParseOperatorCatalogThemeCreate(payload),
"begin-create-expert-catalog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyBeginCreateExpertCatalogIntent()),
"search-operator-catalog-stocks" => ParseOperatorCatalogStockSearch(payload),
"select-operator-catalog-stock" => ParseOperatorCatalogStockSelection(payload),
"add-operator-catalog-stock" => ParseOperatorCatalogStockAdd(payload),
"activate-operator-catalog-stock" =>
ParseOperatorCatalogStockActivation(payload),
"move-operator-catalog-draft-row" => ParseOperatorCatalogDraftMove(payload),
"reorder-operator-catalog-draft-row" =>
ParseOperatorCatalogDraftReorder(payload),
"remove-operator-catalog-draft-row" => ParseOperatorCatalogDraftRemove(payload),
"select-operator-catalog-draft-row" =>
ParseOperatorCatalogDraftSelection(payload),
"remove-active-operator-catalog-draft-row" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyRemoveActiveOperatorCatalogDraftRowIntent()),
"clear-operator-catalog-draft-rows" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyClearOperatorCatalogDraftRowsIntent()),
"set-operator-catalog-draft-buy-amount" =>
ParseOperatorCatalogDraftBuyAmount(payload),
"check-operator-catalog-theme-name" =>
ParseOperatorCatalogThemeNameCheck(payload),
"save-operator-catalog" => ParseOperatorCatalogSave(payload),
"confirm-native-dialog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyConfirmNativeDialogIntent()),
"cancel-native-dialog" => ParseOperatorCatalogEmpty(
payload,
static () => new LegacyCancelNativeDialogIntent()),
"refresh-named-playlists" => ParseNamedPlaylistRefresh(payload),
"select-named-playlist" => ParseNamedPlaylistSelection(payload),
"load-named-playlist-by-id" => ParseNamedPlaylistLoadById(payload),
"load-selected-named-playlist" => ParseNamedPlaylistEmpty(
payload,
static () => new LegacyLoadSelectedNamedPlaylistIntent()),
"create-named-playlist" => ParseNamedPlaylistCreate(payload),
"save-current-named-playlist" => ParseNamedPlaylistEmpty(
payload,
static () => new LegacySaveCurrentNamedPlaylistIntent()),
"save-current-named-playlist-to" => ParseNamedPlaylistSaveTo(payload),
"delete-selected-named-playlist" =>
ParseNamedPlaylistDelete(payload),
"open-manual-financial" => ParseManualFinancialOpen(payload),
"close-manual-financial" => ParseManualFinancialEmpty(
payload,
static () => new LegacyCloseManualFinancialIntent()),
"search-manual-financial" => ParseManualFinancialSearch(payload),
"find-manual-financial" => ParseManualFinancialFind(payload),
"toggle-manual-financial-name-sort" =>
ParseManualFinancialNameSort(payload),
"load-manual-financial" => ParseManualFinancialLoad(payload),
"activate-manual-financial-result" =>
ParseManualFinancialResultActivation(payload),
"begin-manual-financial-create" => ParseManualFinancialEmpty(
payload,
static () => new LegacyBeginManualFinancialCreateIntent()),
"search-manual-financial-stocks" => ParseManualFinancialStockSearch(payload),
"select-manual-financial-stock" => ParseManualFinancialStockSelection(payload),
"save-manual-financial" => ParseManualFinancialSave(payload),
"delete-manual-financial" => ParseManualFinancialEmpty(
payload,
static () => new LegacyDeleteManualFinancialIntent()),
"delete-all-manual-financial" => ParseManualFinancialEmpty(
payload,
static () => new LegacyDeleteAllManualFinancialIntent()),
"activate-manual-financial-action" => ParseManualFinancialAction(payload),
"open-manual-list" => ParseManualListOpen(payload),
"close-manual-list" => ParseManualListEmpty(
payload,
static () => new LegacyCloseManualListIntent()),
"refresh-manual-list" => ParseManualListEmpty(
payload,
static () => new LegacyRefreshManualListIntent()),
"set-manual-net-sell-cell" => ParseManualNetSellCell(payload),
"save-manual-net-sell" => ParseManualListEmpty(
payload,
static () => new LegacySaveManualNetSellIntent()),
"add-manual-net-sell-cut" => ParseManualListEmpty(
payload,
static () => new LegacyAddManualNetSellCutIntent()),
"search-manual-vi" => ParseManualViSearch(payload),
"select-manual-vi-result" => ParseManualViSelection(payload),
"add-manual-vi-result" => ParseManualViResult(payload),
"move-manual-vi-item" => ParseManualViMove(payload),
"delete-manual-vi-item" => ParseManualViDelete(payload),
"delete-all-manual-vi-items" => ParseManualListEmpty(
payload,
static () => new LegacyDeleteAllManualViItemsIntent()),
"save-manual-vi" => ParseManualListEmpty(
payload,
static () => new LegacySaveManualViIntent()),
"add-manual-vi-cut" => ParseManualListEmpty(
payload,
static () => new LegacyAddManualViCutIntent()),
"confirm-manual-list" => ParseManualListEmpty(
payload,
static () => new LegacyConfirmManualListIntent()),
"import-manual-lists" => ParseManualListEmpty(
payload,
static () => new LegacyImportManualListsIntent()),
"dismiss-dialog" => ParseDismiss(payload),
_ => LegacyPlayoutUiIntentParser.Parse(type, payload)
};
if (intent is null)
{
error = "지원하지 않는 UI 요청입니다.";
return false;
}
return true;
}
catch (JsonException)
{
error = "UI 요청 JSON을 읽을 수 없습니다.";
return false;
}
}
public static string SerializeState(
LegacyOperatorSnapshot snapshot,
bool isBusy = false,
LegacyOperatorPlayoutSnapshot? playout = null,
LegacyInteractionMetrics? interactionMetrics = null)
{
ArgumentNullException.ThrowIfNull(snapshot);
var normalizedInteractionMetrics = LegacyInteractionMetrics.Normalize(
interactionMetrics);
var payload = new
{
snapshot.Revision,
snapshot.SearchText,
snapshot.SearchResults,
snapshot.SelectedStockIndex,
snapshot.CutRows,
playlist = snapshot.Playlist.Select(row => new
{
row.RowId,
row.IsEnabled,
row.IsSelected,
row.IsActive,
row.MarketText,
row.StockName,
row.GraphicType,
row.Subtype,
row.PageText
}),
snapshot.Dialog,
snapshot.StatusMessage,
snapshot.StatusKind,
commandResult = snapshot.CommandResult is null ? null : new
{
snapshot.CommandResult.Sequence,
snapshot.CommandResult.Command,
snapshot.CommandResult.TargetId,
snapshot.CommandResult.Succeeded
},
tabs = snapshot.Tabs ?? [],
snapshot.MovingAverages,
snapshot.FixedCatalog,
snapshot.FixedSectionBatch,
industry = snapshot.Industry is null ? null : new
{
snapshot.Industry.Market,
industries = snapshot.Industry.Industries.Select((row, index) => new
{
index,
name = row.IndustryName
}),
snapshot.Industry.SelectedIndex,
selectedName = snapshot.Industry.SelectedIndustry?.IndustryName,
firstName = snapshot.Industry.FirstText,
secondName = snapshot.Industry.SecondText,
actions = snapshot.Industry.Actions.Select(action => new
{
actionId = action.ActionId,
action.Label,
action.Section,
action.Category
})
},
overseas = snapshot.Overseas is null ? null : new
{
snapshot.Overseas.Revision,
fixedIndexTargets = snapshot.Overseas.FixedIndexTargets.Select(target => new
{
target.TargetId,
target.DisplayName
}),
selectedFixedIndexId = snapshot.Overseas.SelectedFixedIndex?.TargetId,
selectedFixedIndexName = snapshot.Overseas.SelectedFixedIndex?.DisplayName,
industry = new
{
snapshot.Overseas.Industry.Status,
snapshot.Overseas.Industry.Query,
snapshot.Overseas.Industry.ResultCount,
snapshot.Overseas.Industry.IsTruncated,
snapshot.Overseas.Industry.SelectedId,
selectedName = snapshot.Overseas.Industry.Selected?.KoreanName,
snapshot.Overseas.Industry.Error,
results = snapshot.Overseas.Industry.Results.Select(row => new
{
row.SelectionId,
displayName = row.KoreanName
})
},
stock = new
{
snapshot.Overseas.Stock.Status,
snapshot.Overseas.Stock.Query,
snapshot.Overseas.Stock.ResultCount,
snapshot.Overseas.Stock.IsTruncated,
snapshot.Overseas.Stock.SelectedId,
selectedName = snapshot.Overseas.Stock.Selected?.InputName,
snapshot.Overseas.Stock.Error,
results = snapshot.Overseas.Stock.Results.Select(row => new
{
row.SelectionId,
displayName = row.InputName
})
},
actions = snapshot.Overseas.Actions.Select(action =>
{
var availability = GetOverseasActionAvailability(snapshot.Overseas, action);
return new
{
action.ActionId,
action.Label,
action.Source,
action.Kind,
action.PeriodDays,
availability.IsAvailable,
availability.Reason
};
})
},
comparison = snapshot.Comparison is null ? null : new
{
snapshot.Comparison.IsBusy,
domesticSearch = new
{
snapshot.Comparison.DomesticSearch.Query,
snapshot.Comparison.DomesticSearch.Status,
results = snapshot.Comparison.DomesticSearch.Results.Select(row => new
{
row.SelectionId,
row.DisplayName,
row.Metadata,
row.Kind
}),
snapshot.Comparison.DomesticSearch.SelectedResultId,
snapshot.Comparison.DomesticSearch.TotalRowCount,
snapshot.Comparison.DomesticSearch.ErrorMessage
},
worldSearch = new
{
snapshot.Comparison.WorldSearch.Query,
snapshot.Comparison.WorldSearch.Status,
results = snapshot.Comparison.WorldSearch.Results.Select(row => new
{
row.SelectionId,
row.DisplayName,
row.Metadata,
row.Kind
}),
snapshot.Comparison.WorldSearch.SelectedResultId,
snapshot.Comparison.WorldSearch.TotalRowCount,
snapshot.Comparison.WorldSearch.ErrorMessage
},
fixedTargets = snapshot.Comparison.FixedTargets.Select(row => new
{
targetId = row.SelectionId,
row.DisplayName,
row.Metadata,
row.Kind
}),
snapshot.Comparison.SelectedFixedTargetId,
firstTarget = snapshot.Comparison.FirstTarget is null ? null : new
{
snapshot.Comparison.FirstTarget.SelectionId,
snapshot.Comparison.FirstTarget.DisplayName,
snapshot.Comparison.FirstTarget.Metadata,
snapshot.Comparison.FirstTarget.Kind
},
secondTarget = snapshot.Comparison.SecondTarget is null ? null : new
{
snapshot.Comparison.SecondTarget.SelectionId,
snapshot.Comparison.SecondTarget.DisplayName,
snapshot.Comparison.SecondTarget.Metadata,
snapshot.Comparison.SecondTarget.Kind
},
savedPairs = snapshot.Comparison.SavedPairs.Select(row => new
{
row.RowId,
row.RowNumber,
row.DisplayLabel,
row.IsSelected
}),
snapshot.Comparison.SelectedPairId,
snapshot.Comparison.Actions,
snapshot.Comparison.StatusMessage,
snapshot.Comparison.StatusKind
},
theme = snapshot.Theme is null ? null : new
{
snapshot.Theme.Query,
snapshot.Theme.NxtSession,
snapshot.Theme.SortId,
searchResults = snapshot.Theme.SearchResults.Select(row => new
{
row.ResultId,
row.DisplayTitle,
row.Market,
row.Session
}),
snapshot.Theme.SearchIsTruncated,
snapshot.Theme.SelectedResultId,
snapshot.Theme.SelectedTheme,
previewItems = snapshot.Theme.PreviewItems.Select(row => new
{
row.InputIndex,
row.ItemName
}),
snapshot.Theme.PreviewIsTruncated,
sorts = LegacyThemeWorkflow.Sorts.Select(sort => new
{
sort.Id,
sort.Label
}),
actions = LegacyThemeWorkflow.Actions.Select(action =>
{
var state = snapshot.Theme.ActionStates.Single(item =>
string.Equals(item.ActionId, action.Id, StringComparison.Ordinal));
return new
{
actionId = action.Id,
action.Label,
state.IsAvailable,
state.UnavailableReason
};
})
},
expert = snapshot.Expert is null ? null : new
{
search = new
{
snapshot.Expert.Search.Query,
snapshot.Expert.Search.Status,
results = snapshot.Expert.Search.Results.Select(row => new
{
row.SelectionId,
row.ExpertName
}),
snapshot.Expert.Search.IsTruncated,
snapshot.Expert.Search.ErrorMessage
},
selectedExpert = snapshot.Expert.SelectedExpert is null ? null : new
{
snapshot.Expert.SelectedExpert.SelectionId,
snapshot.Expert.SelectedExpert.ExpertName
},
preview = new
{
snapshot.Expert.Preview.Status,
items = snapshot.Expert.Preview.Items.Select(row => new
{
row.PlayIndex,
row.StockName,
row.BuyAmount
}),
snapshot.Expert.Preview.IsTruncated,
snapshot.Expert.Preview.PageCount,
snapshot.Expert.Preview.MaximumPageCount,
snapshot.Expert.Preview.ErrorMessage
},
snapshot.Expert.Actions,
snapshot.Expert.StatusMessage,
snapshot.Expert.StatusKind
},
tradingHalt = snapshot.TradingHalt is null ? null : new
{
snapshot.TradingHalt.Query,
snapshot.TradingHalt.Status,
results = snapshot.TradingHalt.Results.Select(row => new
{
row.SelectionId,
row.Market,
row.DisplayName,
row.IsSelected
}),
selected = snapshot.TradingHalt.Selected is null ? null : new
{
snapshot.TradingHalt.Selected.SelectionId,
snapshot.TradingHalt.Selected.Market,
snapshot.TradingHalt.Selected.DisplayName
},
snapshot.TradingHalt.IsTruncated,
snapshot.TradingHalt.NameSort,
snapshot.TradingHalt.Ma5,
snapshot.TradingHalt.Ma20,
snapshot.TradingHalt.Actions,
snapshot.TradingHalt.ErrorMessage,
snapshot.TradingHalt.StatusMessage,
snapshot.TradingHalt.StatusKind
},
operatorCatalog = snapshot.OperatorCatalog is null ? null : new
{
snapshot.OperatorCatalog.Revision,
snapshot.OperatorCatalog.IsOpen,
snapshot.OperatorCatalog.Entity,
snapshot.OperatorCatalog.Mode,
snapshot.OperatorCatalog.Query,
snapshot.OperatorCatalog.NxtSession,
snapshot.OperatorCatalog.NewThemeMarket,
results = snapshot.OperatorCatalog.Results.Select(row => new
{
row.ResultId,
row.CodeLabel,
row.DisplayName,
row.Scope,
row.IsSelected
}),
snapshot.OperatorCatalog.ResultsAreTruncated,
snapshot.OperatorCatalog.SelectedResultId,
snapshot.OperatorCatalog.CodeLabel,
snapshot.OperatorCatalog.EditorName,
snapshot.OperatorCatalog.ThemeNameAvailability,
snapshot.OperatorCatalog.CheckedThemeName,
draftRows = snapshot.OperatorCatalog.DraftRows.Select(row => new
{
row.RowId,
row.DisplayName,
row.Market,
row.BuyAmount,
row.Position
}),
snapshot.OperatorCatalog.ActiveDraftRowId,
snapshot.OperatorCatalog.StockQuery,
stockResults = snapshot.OperatorCatalog.StockResults.Select(row => new
{
row.ResultId,
row.DisplayName,
row.Market,
row.IsCompatible,
row.IsSelected
}),
snapshot.OperatorCatalog.StockResultsAreTruncated,
snapshot.OperatorCatalog.SelectedStockResultId,
snapshot.OperatorCatalog.SchemaValidated,
snapshot.OperatorCatalog.WritesQuarantined,
snapshot.OperatorCatalog.CanBeginCreate,
snapshot.OperatorCatalog.CanCheckThemeName,
snapshot.OperatorCatalog.CanSave,
snapshot.OperatorCatalog.CanDelete,
snapshot.OperatorCatalog.CanAddStock,
snapshot.OperatorCatalog.StatusMessage,
snapshot.OperatorCatalog.StatusKind
},
manualFinancial = snapshot.ManualFinancial is null ? null : new
{
screen = ManualFinancialScreenId(snapshot.ManualFinancial.Screen),
snapshot.ManualFinancial.Definition.Label,
generation = snapshot.ManualFinancial.Revision,
snapshot.ManualFinancial.Query,
snapshot.ManualFinancial.FindQuery,
nameSortDirection = snapshot.ManualFinancial.NameSortDirection ==
LegacyManualFinancialNameSortDirection.Ascending
? "ascending"
: "descending",
rows = snapshot.ManualFinancial.Rows.Select(row => new
{
row.ResultId,
row.StockName
}),
snapshot.ManualFinancial.ListIsTruncated,
snapshot.ManualFinancial.SelectedRowId,
selectedRecord = ProjectManualFinancialRecord(
snapshot.ManualFinancial.SelectedRecord),
snapshot.ManualFinancial.IsDetailFresh,
snapshot.ManualFinancial.StockQuery,
stockCandidates = snapshot.ManualFinancial.StockCandidates.Select(row => new
{
row.ResultId,
row.Market,
row.Name
}),
snapshot.ManualFinancial.StockSearchIsTruncated,
snapshot.ManualFinancial.SelectedStockResultId,
verifiedStock = snapshot.ManualFinancial.VerifiedStock is null ? null : new
{
snapshot.ManualFinancial.VerifiedStock.Market,
snapshot.ManualFinancial.VerifiedStock.Name
},
snapshot.ManualFinancial.Status,
snapshot.ManualFinancial.WritesQuarantined,
snapshot.ManualFinancial.LastMessage,
snapshot.ManualFinancial.CanSave,
snapshot.ManualFinancial.CanDelete,
snapshot.ManualFinancial.CanMaterializePlaylist,
action = new
{
snapshot.ManualFinancial.Definition.ActionId,
snapshot.ManualFinancial.Definition.Label
}
},
manualLists = snapshot.ManualLists is null ? null : new
{
snapshot.ManualLists.IsOpen,
screen = snapshot.ManualLists.Screen switch
{
LegacyManualListScreen.NetSell => "net-sell",
LegacyManualListScreen.Vi => "vi",
_ => null
},
audience = snapshot.ManualLists.Audience switch
{
S5025ManualAudience.Individual => "individual",
S5025ManualAudience.Foreign => "foreign",
S5025ManualAudience.Institution => "institution",
_ => throw new ArgumentOutOfRangeException()
},
snapshot.ManualLists.IsAvailable,
snapshot.ManualLists.AvailabilityCode,
snapshot.ManualLists.AvailabilityMessage,
snapshot.ManualLists.WritesQuarantined,
netRows = snapshot.ManualLists.NetRows.Select(row => new
{
row.RowId,
row.LeftName,
row.LeftAmount,
row.RightName,
row.RightAmount
}),
snapshot.ManualLists.NetRowsAreFresh,
viItems = snapshot.ManualLists.ViItems.Select(row => new
{
row.ItemId,
row.Index,
row.Name
}),
snapshot.ManualLists.ViPageCount,
snapshot.ManualLists.ViItemsAreFresh,
snapshot.ManualLists.SearchQuery,
searchResults = snapshot.ManualLists.SearchResults.Select(row => new
{
row.ResultId,
row.DisplayName,
row.MarketText
}),
snapshot.ManualLists.SelectedSearchResultId,
snapshot.ManualLists.StatusMessage,
snapshot.ManualLists.StatusKind,
snapshot.ManualLists.CanSave,
snapshot.ManualLists.CanMaterializePlaylist
},
namedPlaylist = snapshot.NamedPlaylist is null ? null : new
{
snapshot.NamedPlaylist.Revision,
snapshot.NamedPlaylist.ReadStatus,
definitions = snapshot.NamedPlaylist.Definitions.Select(definition => new
{
definition.DefinitionId,
definition.Title,
definition.IsSelected
}),
snapshot.NamedPlaylist.IsListTruncated,
snapshot.NamedPlaylist.SelectedDefinitionId,
snapshot.NamedPlaylist.SelectedTitle,
rowCount = snapshot.NamedPlaylist.Rows.Count,
snapshot.NamedPlaylist.ListFreshness,
snapshot.NamedPlaylist.DocumentFreshness,
snapshot.NamedPlaylist.IsWriteInProgress,
snapshot.NamedPlaylist.IsWriteQuarantined,
snapshot.NamedPlaylist.CanMutate,
snapshot.NamedPlaylist.CanCreate,
snapshot.NamedPlaylist.CanLoad,
snapshot.NamedPlaylist.CanSave,
snapshot.NamedPlaylist.CanDelete,
snapshot.NamedPlaylist.LastMutationOutcome,
snapshot.NamedPlaylist.StatusMessage,
snapshot.NamedPlaylist.StatusKind
},
playout = playout is null ? null : new
{
playout.Mode,
playout.ConnectionState,
playout.Phase,
playout.IsProcessRunning,
playout.IsConnected,
playout.IsCommandAvailable,
playout.LiveTakeInAllowed,
playout.IsPlayCompletionPending,
playout.IsTakeOutCompletionPending,
playout.OutcomeUnknown,
playout.PreparedCode,
playout.OnAirCode,
playout.CurrentCueIndexZeroBased,
playout.CurrentEntryId,
playout.PageIndexZeroBased,
playout.PageCount,
playout.PageSize,
playout.ItemCount,
playout.CurrentPageItemCount,
playout.IsLastPage,
playout.NextKind,
playout.IsBusy,
playout.RefreshActive,
playout.RefreshCompletedCount,
playout.RefreshMaximumCount,
playout.RefreshLimitReached,
playout.RefreshNextAtUtc,
playout.RefreshLastSuccessAtUtc,
playout.RefreshFaultCode,
playout.FadeDuration,
playout.BackgroundEnabled,
playout.BackgroundFileName,
playout.CanChangeBackground,
playout.Message
},
interactionMetrics = new
{
cutDragWidthDips = normalizedInteractionMetrics.CutDragWidthDips,
cutDragHeightDips = normalizedInteractionMetrics.CutDragHeightDips,
coordinateSpace = "host-dip",
sourceDpi = normalizedInteractionMetrics.SourceDpi,
hostRasterizationScale =
normalizedInteractionMetrics.HostRasterizationScale
},
isBusy
};
return JsonSerializer.Serialize(new { type = "state", payload }, SerializerOptions);
}
private static LegacyOverseasActionAvailability GetOverseasActionAvailability(
LegacyOverseasWorkflowSnapshot snapshot,
LegacyOverseasActionDefinition action) =>
action.Source switch
{
LegacyOverseasActionSource.FixedIndex when snapshot.SelectedFixedIndex is null =>
new(false, "해외지수를 선택하십시오."),
LegacyOverseasActionSource.Industry when snapshot.Industry.Selected is null =>
new(false, "해외업종을 선택하십시오."),
LegacyOverseasActionSource.Stock when snapshot.Stock.Selected is null =>
new(false, "해외종목을 선택하십시오."),
LegacyOverseasActionSource.Stock when snapshot.Stock.Selected!.NationCode.Length != 2 =>
new(false, "선택한 해외종목의 국가 코드는 두 글자여야 합니다."),
_ => new(true, null)
};
private static LegacyUiIntent? ParseReady(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyReadyIntent() : null;
private static LegacyUiIntent? ParseSearch(JsonElement payload)
{
if (!HasOnlyProperties(payload, "text", "trigger") ||
!TryString(
payload,
"text",
LegacyParityStockSearchService.LegacyTextBoxMaximumLength,
out var text) ||
!TryString(payload, "trigger", 16, out var triggerText))
{
return null;
}
var trigger = triggerText switch
{
"button" => LegacySearchTrigger.Button,
"enter" => LegacySearchTrigger.Enter,
_ => (LegacySearchTrigger?)null
};
return trigger.HasValue ? new LegacySearchStocksIntent(text, trigger.Value) : null;
}
private static LegacyUiIntent? ParseStockSelection(JsonElement payload) =>
HasOnlyProperties(payload, "resultIndex") &&
TryInt32(payload, "resultIndex", 0, int.MaxValue, out var resultIndex)
? new LegacySelectStockIntent(resultIndex)
: null;
private static LegacyUiIntent? ParseCutPointer(JsonElement payload)
{
if (!HasOnlyProperties(
payload,
"physicalIndex",
"control",
"shift",
"x",
"y",
"timestampMilliseconds",
"allowAppend") ||
!TryInt32(
payload,
"physicalIndex",
0,
LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
out var physicalIndex) ||
!TryBoolean(payload, "control", out var control) ||
!TryBoolean(payload, "shift", out var shift) ||
!TryInt32(payload, "x", 0, 16_384, out var x) ||
!TryInt32(payload, "y", 0, 16_384, out var y) ||
!TryInt64(payload, "timestampMilliseconds", 0, long.MaxValue, out var timestamp) ||
!TryBoolean(payload, "allowAppend", out var allowAppend))
{
return null;
}
return new LegacyCutPointerDownIntent(
physicalIndex,
control,
shift,
x,
y,
timestamp,
allowAppend);
}
private static LegacyUiIntent? ParseDrop(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDropSelectedCutsIntent() : null;
private static LegacyUiIntent? ParseCutKeyDown(JsonElement payload)
{
if (!HasOnlyProperties(payload, "key", "control", "shift") ||
!TryString(payload, "key", 16, out var keyText) ||
!TryBoolean(payload, "control", out var control) ||
!TryBoolean(payload, "shift", out var shift))
{
return null;
}
var key = keyText switch
{
"arrow-up" => LegacyCutKeyboardKey.ArrowUp,
"arrow-down" => LegacyCutKeyboardKey.ArrowDown,
"home" => LegacyCutKeyboardKey.Home,
"end" => LegacyCutKeyboardKey.End,
"space" => LegacyCutKeyboardKey.Space,
_ => (LegacyCutKeyboardKey?)null
};
return key.HasValue
? new LegacyCutKeyDownIntent(key.Value, control, shift)
: null;
}
private static LegacyUiIntent? ParseClearCutSelection(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyClearCutSelectionIntent() : null;
private static LegacyUiIntent? ParseEnabled(JsonElement payload)
{
if (!HasOnlyProperties(payload, "rowId", "enabled") ||
!TryString(payload, "rowId", 128, out var rowId) ||
!IsSafeIdentifier(rowId) ||
!TryBoolean(payload, "enabled", out var enabled))
{
return null;
}
return new LegacySetPlaylistEnabledIntent(rowId, enabled);
}
private static LegacyUiIntent? ParseAllEnabled(JsonElement payload) =>
HasOnlyProperties(payload, "enabled") &&
TryBoolean(payload, "enabled", out var enabled)
? new LegacySetAllPlaylistEnabledIntent(enabled)
: null;
private static LegacyUiIntent? ParsePlaylistSelection(JsonElement payload)
{
if (!HasOnlyProperties(payload, "rowId", "control", "shift") ||
!TryString(payload, "rowId", 128, out var rowId) ||
!IsSafeIdentifier(rowId) ||
!TryBoolean(payload, "control", out var control) ||
!TryBoolean(payload, "shift", out var shift))
{
return null;
}
return new LegacySelectPlaylistRowIntent(rowId, control, shift);
}
private static LegacyUiIntent? ParsePlaylistActivation(JsonElement payload) =>
HasOnlyProperties(payload, "rowId") &&
TryString(payload, "rowId", 128, out var rowId) &&
IsSafeIdentifier(rowId)
? new LegacyActivatePlaylistRowIntent(rowId)
: null;
private static LegacyUiIntent? ParsePlaylistMove(JsonElement payload)
{
if (!HasOnlyProperties(payload, "direction") ||
!TryString(payload, "direction", 8, out var directionText))
{
return null;
}
var direction = directionText switch
{
"up" => LegacyPlaylistMoveDirection.Up,
"down" => LegacyPlaylistMoveDirection.Down,
_ => (LegacyPlaylistMoveDirection?)null
};
return direction.HasValue
? new LegacyMoveSelectedPlaylistRowsIntent(direction.Value)
: null;
}
private static LegacyUiIntent? ParsePlaylistReorder(JsonElement payload)
{
if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") ||
!TryString(payload, "sourceRowId", 128, out var sourceRowId) ||
!TryString(payload, "targetRowId", 128, out var targetRowId) ||
!IsSafeIdentifier(sourceRowId) ||
!IsSafeIdentifier(targetRowId) ||
string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) ||
!TryString(payload, "position", 8, out var positionText))
{
return null;
}
var position = ParseDragDropPosition(positionText);
return position.HasValue
? new LegacyReorderPlaylistRowsIntent(sourceRowId, targetRowId, position.Value)
: null;
}
private static LegacyUiIntent? ParseDeleteSelected(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDeleteSelectedPlaylistRowsIntent() : null;
private static LegacyUiIntent? ParseDeleteAll(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDeleteAllPlaylistRowsIntent() : null;
private static LegacyUiIntent? ParseToggleActiveEnabled(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyToggleActivePlaylistEnabledIntent() : null;
private static LegacyUiIntent? ParsePlaylistBoundary(JsonElement payload)
{
if (!HasOnlyProperties(payload, "boundary") ||
!TryString(payload, "boundary", 8, out var boundary))
{
return null;
}
return boundary switch
{
"first" => new LegacySelectPlaylistBoundaryIntent(false),
"last" => new LegacySelectPlaylistBoundaryIntent(true),
_ => null
};
}
private static LegacyUiIntent? ParseTabSelection(JsonElement payload) =>
HasOnlyProperties(payload, "tabId") &&
TryTabId(payload, "tabId", out var tabId)
? new LegacySelectTabIntent(tabId)
: null;
private static LegacyUiIntent? ParseTabSwap(JsonElement payload) =>
HasOnlyProperties(payload, "source", "target") &&
TryTabId(payload, "source", out var source) &&
TryTabId(payload, "target", out var target)
? new LegacySwapTabsIntent(source, target)
: null;
private static LegacyUiIntent? ParseTabHoverSwap(JsonElement payload)
{
if (!HasOnlyProperties(
payload,
"source",
"target",
"expectedSourceIndex",
"expectedTargetIndex") ||
!TryTabId(payload, "source", out var source) ||
!TryTabId(payload, "target", out var target) ||
source == target ||
!TryInt32(payload, "expectedSourceIndex", 0, 9, out var sourceIndex) ||
!TryInt32(payload, "expectedTargetIndex", 0, 9, out var targetIndex) ||
sourceIndex == targetIndex)
{
return null;
}
return new LegacyHoverSwapTabsIntent(source, target, sourceIndex, targetIndex);
}
private static LegacyUiIntent? ParseMovingAverages(JsonElement payload) =>
HasOnlyProperties(payload, "ma5", "ma20") &&
TryBoolean(payload, "ma5", out var ma5) &&
TryBoolean(payload, "ma20", out var ma20)
? new LegacySetMovingAveragesIntent(ma5, ma20)
: null;
private static LegacyUiIntent? ParseFixedAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
IsSafeIdentifier(actionId)
? new LegacyActivateFixedActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseFixedSection(JsonElement payload) =>
HasOnlyProperties(payload, "sectionIndex") &&
TryInt32(payload, "sectionIndex", 0, 255, out var sectionIndex)
? new LegacyActivateFixedSectionIntent(sectionIndex)
: null;
private static LegacyUiIntent? ParseIndustrySelection(
JsonElement payload,
bool doubleClick) =>
HasOnlyProperties(payload, "index") &&
TryInt32(payload, "index", 0, 499, out var index)
? new LegacySelectIndustryIntent(index, doubleClick)
: null;
private static LegacyUiIntent? ParseIndustryComparisonText(JsonElement payload)
{
if (!HasOnlyProperties(payload, "slot", "value") ||
!TryString(payload, "slot", 16, out var slotText) ||
!TrySafeString(
payload,
"value",
LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength,
allowEmpty: true,
allowUnderscore: true,
out var value))
{
return null;
}
var slot = slotText switch
{
"first" => LegacyIndustryComparisonSlot.First,
"second" => LegacyIndustryComparisonSlot.Second,
_ => (LegacyIndustryComparisonSlot?)null
};
return slot.HasValue
? new LegacySetIndustryComparisonTextIntent(slot.Value, value)
: null;
}
private static LegacyUiIntent? ParseSwapIndustries(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacySwapIndustriesIntent() : null;
private static LegacyUiIntent? ParseIndustryAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
IsSafeOpaqueActionId(actionId)
? new LegacyActivateIndustryActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseOverseasFixedIndex(JsonElement payload) =>
HasOnlyProperties(payload, "targetId") &&
TryString(payload, "targetId", 64, out var targetId) &&
IsSafeIdentifier(targetId) &&
LegacyOverseasActionCatalog.FindFixedIndexTarget(targetId) is not null
? new LegacySelectOverseasFixedIndexIntent(targetId)
: null;
private static LegacyUiIntent? ParseOverseasSearch(
JsonElement payload,
bool stocks) =>
HasOnlyProperties(payload, "query") &&
TrySafeString(
payload,
"query",
LegacyOverseasSelectionWorkflow.MaximumQueryLength,
allowEmpty: true,
allowUnderscore: true,
out var query)
? stocks
? new LegacySearchOverseasStocksIntent(query)
: new LegacySearchOverseasIndustriesIntent(query)
: null;
private static LegacyUiIntent? ParseOverseasSelection(
JsonElement payload,
bool stocks) =>
HasOnlyProperties(payload, "selectionId") &&
TryString(payload, "selectionId", 128, out var selectionId) &&
IsSafeIdentifier(selectionId)
? stocks
? new LegacySelectOverseasStockIntent(selectionId)
: new LegacySelectOverseasIndustryIntent(selectionId)
: null;
private static LegacyUiIntent? ParseOverseasAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
IsSafeIdentifier(actionId) &&
LegacyOverseasActionCatalog.FindAction(actionId) is not null
? new LegacyActivateOverseasActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseComparisonSearch(
JsonElement payload,
bool world) =>
HasOnlyProperties(payload, "query") &&
TryString(payload, "query", 64, out var query)
? world
? new LegacySearchComparisonWorldIntent(query)
: new LegacySearchComparisonDomesticIntent(query)
: null;
private static LegacyUiIntent? ParseComparisonChoice(
JsonElement payload,
bool world) =>
HasOnlyProperties(payload, "selectionId") &&
TryString(payload, "selectionId", 128, out var selectionId) &&
IsSafeIdentifier(selectionId)
? world
? new LegacyChooseComparisonWorldIntent(selectionId)
: new LegacyChooseComparisonDomesticIntent(selectionId)
: null;
private static LegacyUiIntent? ParseComparisonSelection(
JsonElement payload,
bool world) =>
HasOnlyProperties(payload, "selectionId") &&
TryString(payload, "selectionId", 128, out var selectionId) &&
IsSafeIdentifier(selectionId)
? world
? new LegacySelectComparisonWorldIntent(selectionId)
: new LegacySelectComparisonDomesticIntent(selectionId)
: null;
private static LegacyUiIntent? ParseComparisonFixedSelection(JsonElement payload) =>
HasOnlyProperties(payload, "targetId") &&
TryString(payload, "targetId", 64, out var targetId) &&
IsSafeIdentifier(targetId)
? new LegacySelectComparisonFixedIntent(targetId)
: null;
private static LegacyUiIntent? ParseComparisonFixed(JsonElement payload) =>
HasOnlyProperties(payload, "targetId") &&
TryString(payload, "targetId", 64, out var targetId) &&
IsSafeIdentifier(targetId)
? new LegacyChooseComparisonFixedIntent(targetId)
: null;
private static LegacyUiIntent? ParseComparisonSwap(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacySwapComparisonTargetsIntent() : null;
private static LegacyUiIntent? ParseComparisonClear(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyClearComparisonTargetsIntent() : null;
private static LegacyUiIntent? ParseComparisonAdd(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyAddComparisonPairIntent() : null;
private static LegacyUiIntent? ParseComparisonPairSelection(JsonElement payload) =>
HasOnlyProperties(payload, "rowId") &&
TryString(payload, "rowId", 128, out var rowId) &&
IsSafeIdentifier(rowId)
? new LegacySelectComparisonPairIntent(rowId)
: null;
private static LegacyUiIntent? ParseComparisonPairMove(JsonElement payload)
{
if (!HasOnlyProperties(payload, "direction") ||
!TryString(payload, "direction", 8, out var direction))
{
return null;
}
return direction switch
{
"up" => new LegacyMoveComparisonPairIntent(
LegacyComparisonPairMoveDirection.Up),
"down" => new LegacyMoveComparisonPairIntent(
LegacyComparisonPairMoveDirection.Down),
_ => null
};
}
private static LegacyUiIntent? ParseComparisonDeleteSelected(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDeleteSelectedComparisonPairIntent() : null;
private static LegacyUiIntent? ParseComparisonDeleteAll(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDeleteAllComparisonPairsIntent() : null;
private static LegacyUiIntent? ParseComparisonAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
IsSafeIdentifier(actionId)
? new LegacyActivateComparisonActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseSectionIndex(
JsonElement payload,
Func<int, LegacyUiIntent> factory) =>
HasOnlyProperties(payload, "sectionIndex") &&
TryInt32(payload, "sectionIndex", 0, 63, out var sectionIndex)
? factory(sectionIndex)
: null;
private static LegacyUiIntent? ParseThemeSearch(JsonElement payload)
{
if (!HasOnlyProperties(payload, "query") ||
!TryString(
payload,
"query",
LegacyThemeSelectionService.MaximumQueryLength,
out var query))
{
return null;
}
return new LegacySearchThemesIntent(query);
}
private static LegacyUiIntent? ParseThemeSelection(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 64, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacySelectThemeIntent(resultId)
: null;
private static LegacyUiIntent? ParseThemeResultActivation(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 64, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacyActivateThemeResultIntent(resultId)
: null;
private static LegacyUiIntent? ParseThemeSort(JsonElement payload)
{
if (!HasOnlyProperties(payload, "sortId") ||
!TryString(payload, "sortId", 24, out var sortId))
{
return null;
}
return sortId is "INPUT_ORDER" or "GAIN_DESC" or "GAIN_ASC"
? new LegacySetThemeSortIntent(sortId)
: null;
}
private static LegacyUiIntent? ParseThemeAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
IsSafeIdentifier(actionId)
? new LegacyActivateThemeActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseExpertSearch(JsonElement payload) =>
HasOnlyProperties(payload, "query") &&
TryString(
payload,
"query",
LegacyExpertSelectionService.MaximumQueryLength,
out var query)
? new LegacySearchExpertsIntent(query)
: null;
private static LegacyUiIntent? ParseExpertSelection(JsonElement payload) =>
HasOnlyProperties(payload, "selectionId") &&
TryString(payload, "selectionId", 128, out var selectionId) &&
IsSafeIdentifier(selectionId)
? new LegacySelectExpertIntent(selectionId)
: null;
private static LegacyUiIntent? ParseExpertAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
string.Equals(actionId, LegacyExpertWorkflowController.ActionId, StringComparison.Ordinal)
? new LegacyActivateExpertActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseTradingHaltSearch(JsonElement payload) =>
HasOnlyProperties(payload, "query") &&
TryString(
payload,
"query",
LegacyTradingHaltSelectionService.MaximumQueryLength,
out var query)
? new LegacySearchTradingHaltsIntent(query)
: null;
private static LegacyUiIntent? ParseTradingHaltSelection(JsonElement payload) =>
HasOnlyProperties(payload, "selectionId") &&
TryString(payload, "selectionId", 128, out var selectionId) &&
IsSafeIdentifier(selectionId)
? new LegacySelectTradingHaltIntent(selectionId)
: null;
private static LegacyUiIntent? ParseTradingHaltSort(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyToggleTradingHaltSortIntent() : null;
private static LegacyUiIntent? ParseTradingHaltAction(JsonElement payload)
{
if (!HasOnlyProperties(payload, "actionId") ||
!TryString(payload, "actionId", 64, out var actionId))
{
return null;
}
return actionId is LegacyTradingHaltWorkflowController.CurrentActionId or
LegacyTradingHaltWorkflowController.CandleActionId
? new LegacyActivateTradingHaltActionIntent(actionId)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogOpen(JsonElement payload)
{
if (!HasOnlyProperties(payload, "entity") ||
!TryString(payload, "entity", 16, out var entityText))
{
return null;
}
var entity = entityText switch
{
"theme" => LegacyOperatorCatalogEntity.Theme,
"expert" => LegacyOperatorCatalogEntity.Expert,
_ => (LegacyOperatorCatalogEntity?)null
};
return entity.HasValue ? new LegacyOpenOperatorCatalogIntent(entity.Value) : null;
}
private static LegacyUiIntent? ParseOperatorCatalogEmpty(
JsonElement payload,
Func<LegacyUiIntent> factory) =>
HasOnlyProperties(payload) ? factory() : null;
private static LegacyUiIntent? ParseOperatorCatalogSearch(JsonElement payload)
{
if (!HasOnlyProperties(payload, "query", "nxtSession") ||
!TrySafeString(
payload,
"query",
64,
allowEmpty: true,
allowUnderscore: true,
out var query) ||
!TryString(payload, "nxtSession", 16, out var sessionText))
{
return null;
}
var session = sessionText switch
{
"preMarket" => ThemeSession.PreMarket,
"afterMarket" => ThemeSession.AfterMarket,
_ => (ThemeSession?)null
};
return session.HasValue
? new LegacySearchOperatorCatalogIntent(query, session.Value)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogResult(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 128, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacySelectOperatorCatalogResultIntent(resultId)
: null;
private static LegacyUiIntent? ParseOperatorCatalogThemeCreate(JsonElement payload)
{
if (!HasOnlyProperties(payload, "market") ||
!TryString(payload, "market", 8, out var marketText))
{
return null;
}
var market = marketText switch
{
"krx" => ThemeMarket.Krx,
"nxt" => ThemeMarket.Nxt,
_ => (ThemeMarket?)null
};
return market.HasValue
? new LegacyBeginCreateThemeCatalogIntent(market.Value)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogStockSearch(JsonElement payload) =>
HasOnlyProperties(payload, "query") &&
TrySafeString(
payload,
"query",
LegacyStockSearchService.MaximumQueryLength,
allowEmpty: false,
allowUnderscore: true,
out var query)
? new LegacySearchOperatorCatalogStocksIntent(query)
: null;
private static LegacyUiIntent? ParseOperatorCatalogStockSelection(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 128, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacySelectOperatorCatalogStockIntent(resultId)
: null;
private static LegacyUiIntent? ParseOperatorCatalogStockAdd(JsonElement payload)
{
if (!HasOnlyProperties(payload, "buyAmount") ||
!payload.TryGetProperty("buyAmount", out var amountElement))
{
return null;
}
if (amountElement.ValueKind == JsonValueKind.Null)
{
return new LegacyAddOperatorCatalogStockIntent(null);
}
return amountElement.ValueKind == JsonValueKind.Number &&
amountElement.TryGetDecimal(out var amount) &&
decimal.Truncate(amount) == amount &&
amount is > 0 and <= 9_007_199_254_740_991m
? new LegacyAddOperatorCatalogStockIntent(amount)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogStockActivation(JsonElement payload)
{
if (!HasOnlyProperties(payload, "resultId", "buyAmount") ||
!TryString(payload, "resultId", 128, out var resultId) ||
!IsSafeIdentifier(resultId) ||
!payload.TryGetProperty("buyAmount", out var amountElement))
{
return null;
}
if (amountElement.ValueKind == JsonValueKind.Null)
{
return new LegacyActivateOperatorCatalogStockIntent(resultId, null);
}
return amountElement.ValueKind == JsonValueKind.Number &&
amountElement.TryGetDecimal(out var amount) &&
decimal.Truncate(amount) == amount &&
amount is > 0 and <= 9_007_199_254_740_991m
? new LegacyActivateOperatorCatalogStockIntent(resultId, amount)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogDraftMove(JsonElement payload)
{
if (!HasOnlyProperties(payload, "rowId", "direction") ||
!TryString(payload, "rowId", 128, out var rowId) ||
!IsSafeIdentifier(rowId) ||
!TryString(payload, "direction", 8, out var directionText))
{
return null;
}
var direction = directionText switch
{
"up" => LegacyOperatorCatalogMoveDirection.Up,
"down" => LegacyOperatorCatalogMoveDirection.Down,
_ => (LegacyOperatorCatalogMoveDirection?)null
};
return direction.HasValue
? new LegacyMoveOperatorCatalogDraftRowIntent(rowId, direction.Value)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogDraftReorder(JsonElement payload)
{
if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") ||
!TryString(payload, "sourceRowId", 128, out var sourceRowId) ||
!TryString(payload, "targetRowId", 128, out var targetRowId) ||
!IsSafeIdentifier(sourceRowId) ||
!IsSafeIdentifier(targetRowId) ||
string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) ||
!TryString(payload, "position", 8, out var positionText))
{
return null;
}
var position = ParseDragDropPosition(positionText);
return position.HasValue
? new LegacyReorderOperatorCatalogDraftRowIntent(
sourceRowId,
targetRowId,
position.Value)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogDraftRemove(JsonElement payload) =>
HasOnlyProperties(payload, "rowId") &&
TryString(payload, "rowId", 128, out var rowId) &&
IsSafeIdentifier(rowId)
? new LegacyRemoveOperatorCatalogDraftRowIntent(rowId)
: null;
private static LegacyUiIntent? ParseOperatorCatalogDraftSelection(JsonElement payload) =>
HasOnlyProperties(payload, "rowId") &&
TryString(payload, "rowId", 128, out var rowId) &&
IsSafeIdentifier(rowId)
? new LegacySelectOperatorCatalogDraftRowIntent(rowId)
: null;
private static LegacyUiIntent? ParseOperatorCatalogDraftBuyAmount(JsonElement payload)
{
if (!HasOnlyProperties(payload, "rowId", "buyAmount") ||
!TryString(payload, "rowId", 128, out var rowId) ||
!IsSafeIdentifier(rowId) ||
!payload.TryGetProperty("buyAmount", out var amountElement))
{
return null;
}
if (amountElement.ValueKind == JsonValueKind.Null)
{
return new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, null);
}
return amountElement.ValueKind == JsonValueKind.Number &&
amountElement.TryGetDecimal(out var amount) &&
decimal.Truncate(amount) == amount &&
amount is > 0 and <= 9_007_199_254_740_991m
? new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, amount)
: null;
}
private static LegacyUiIntent? ParseOperatorCatalogSave(JsonElement payload) =>
HasOnlyProperties(payload, "name") &&
TrySafeString(
payload,
"name",
128,
allowEmpty: true,
allowUnderscore: true,
out var name) &&
string.Equals(name, name.Trim(), StringComparison.Ordinal)
? new LegacySaveOperatorCatalogIntent(name)
: null;
private static LegacyUiIntent? ParseOperatorCatalogThemeNameCheck(JsonElement payload) =>
HasOnlyProperties(payload, "name") &&
TrySafeString(
payload,
"name",
128,
allowEmpty: false,
allowUnderscore: true,
out var name) &&
string.Equals(name, name.Trim(), StringComparison.Ordinal)
? new LegacyCheckOperatorCatalogThemeNameIntent(name)
: null;
private static LegacyUiIntent? ParseNamedPlaylistEmpty(
JsonElement payload,
Func<LegacyUiIntent> factory) =>
HasOnlyProperties(payload) ? factory() : null;
private static LegacyUiIntent? ParseNamedPlaylistSelection(JsonElement payload) =>
HasOnlyProperties(payload, "definitionId", "requestId") &&
TryString(payload, "definitionId", 128, out var definitionId) &&
IsSafeIdentifier(definitionId) &&
TryString(payload, "requestId", 128, out var requestId) &&
IsSafeIdentifier(requestId)
? new LegacySelectNamedPlaylistIntent(definitionId, requestId)
: null;
private static LegacyUiIntent? ParseNamedPlaylistRefresh(JsonElement payload) =>
HasOnlyProperties(payload, "requestId") &&
TryString(payload, "requestId", 128, out var requestId) &&
IsSafeIdentifier(requestId)
? new LegacyRefreshNamedPlaylistsIntent(requestId)
: null;
private static LegacyUiIntent? ParseNamedPlaylistLoadById(JsonElement payload) =>
HasOnlyProperties(payload, "definitionId") &&
TryString(payload, "definitionId", 128, out var definitionId) &&
IsSafeIdentifier(definitionId)
? new LegacyLoadNamedPlaylistByIdIntent(definitionId)
: null;
private static LegacyUiIntent? ParseNamedPlaylistSaveTo(JsonElement payload) =>
HasOnlyProperties(payload, "definitionId") &&
TryString(payload, "definitionId", 128, out var definitionId) &&
IsSafeIdentifier(definitionId)
? new LegacySaveCurrentNamedPlaylistToIntent(definitionId)
: null;
private static LegacyUiIntent? ParseNamedPlaylistCreate(JsonElement payload) =>
HasOnlyProperties(payload, "title", "requestId") &&
TrySafeString(
payload,
"title",
128,
allowEmpty: false,
allowUnderscore: true,
out var title) &&
TryString(payload, "requestId", 128, out var requestId) &&
IsSafeIdentifier(requestId) &&
string.Equals(title, title.Trim(), StringComparison.Ordinal)
? new LegacyCreateNamedPlaylistIntent(title, requestId)
: null;
private static LegacyUiIntent? ParseNamedPlaylistDelete(JsonElement payload) =>
HasOnlyProperties(payload, "requestId") &&
TryString(payload, "requestId", 128, out var requestId) &&
IsSafeIdentifier(requestId)
? new LegacyDeleteSelectedNamedPlaylistIntent(requestId)
: null;
private static LegacyUiIntent? ParseManualFinancialOpen(JsonElement payload) =>
HasOnlyProperties(payload, "screen") &&
TryString(payload, "screen", 32, out var screenText) &&
TryManualFinancialScreen(screenText, out var screen)
? new LegacyOpenManualFinancialIntent(screen)
: null;
private static LegacyUiIntent? ParseManualFinancialEmpty(
JsonElement payload,
Func<LegacyUiIntent> factory) =>
HasOnlyProperties(payload) ? factory() : null;
private static LegacyUiIntent? ParseManualFinancialSearch(JsonElement payload) =>
HasOnlyProperties(payload, "query") &&
TrySafeString(payload, "query", 64, allowEmpty: true, allowUnderscore: true, out var query)
? new LegacySearchManualFinancialIntent(query)
: null;
private static LegacyUiIntent? ParseManualFinancialFind(JsonElement payload)
{
if (!HasOnlyProperties(payload, "query", "direction", "generation") ||
!TrySafeString(
payload,
"query",
64,
allowEmpty: false,
allowUnderscore: true,
out var query) ||
!TryString(payload, "direction", 8, out var directionText) ||
!TryInt64(payload, "generation", 0, long.MaxValue, out var generation))
{
return null;
}
var direction = directionText switch
{
"up" => LegacyManualFinancialFindDirection.Up,
"down" => LegacyManualFinancialFindDirection.Down,
_ => (LegacyManualFinancialFindDirection?)null
};
return direction.HasValue
? new LegacyFindManualFinancialIntent(query, direction.Value, generation)
: null;
}
private static LegacyUiIntent? ParseManualFinancialNameSort(JsonElement payload) =>
HasOnlyProperties(payload, "generation") &&
TryInt64(payload, "generation", 0, long.MaxValue, out var generation)
? new LegacyToggleManualFinancialNameSortIntent(generation)
: null;
private static LegacyUiIntent? ParseManualFinancialLoad(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 128, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacyLoadManualFinancialIntent(resultId)
: null;
private static LegacyUiIntent? ParseManualFinancialResultActivation(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 128, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacyActivateManualFinancialResultIntent(resultId)
: null;
private static LegacyUiIntent? ParseManualFinancialStockSearch(JsonElement payload) =>
HasOnlyProperties(payload, "stockName") &&
TrySafeString(
payload,
"stockName",
64,
allowEmpty: false,
allowUnderscore: true,
out var stockName)
? new LegacySearchManualFinancialStocksIntent(stockName)
: null;
private static LegacyUiIntent? ParseManualFinancialStockSelection(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 128, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacySelectManualFinancialStockIntent(resultId)
: null;
private static LegacyUiIntent? ParseManualFinancialSave(JsonElement payload)
{
if (!HasOnlyProperties(payload, "record") ||
!payload.TryGetProperty("record", out var recordElement) ||
recordElement.ValueKind != JsonValueKind.Object)
{
return null;
}
var record = ParseManualFinancialRecord(recordElement);
return record is null ? null : new LegacySaveManualFinancialIntent(record);
}
private static LegacyUiIntent? ParseManualFinancialAction(JsonElement payload) =>
HasOnlyProperties(payload, "actionId") &&
TryString(payload, "actionId", 64, out var actionId) &&
LegacyManualFinancialWorkflow.Screens.Any(definition =>
string.Equals(definition.ActionId, actionId, StringComparison.Ordinal))
? new LegacyActivateManualFinancialActionIntent(actionId)
: null;
private static LegacyUiIntent? ParseManualListOpen(JsonElement payload)
{
if (!HasOnlyProperties(payload, "screen") ||
!TryString(payload, "screen", 24, out var screen))
{
return null;
}
return screen switch
{
"individual" => new LegacyOpenManualListIntent(
LegacyManualListScreen.NetSell,
S5025ManualAudience.Individual),
"foreign" => new LegacyOpenManualListIntent(
LegacyManualListScreen.NetSell,
S5025ManualAudience.Foreign),
"institution" => new LegacyOpenManualListIntent(
LegacyManualListScreen.NetSell,
S5025ManualAudience.Institution),
"vi" => new LegacyOpenManualListIntent(
LegacyManualListScreen.Vi,
S5025ManualAudience.Individual),
_ => null
};
}
private static LegacyUiIntent? ParseManualListEmpty(
JsonElement payload,
Func<LegacyUiIntent> factory) =>
HasOnlyProperties(payload) ? factory() : null;
private static LegacyUiIntent? ParseManualNetSellCell(JsonElement payload)
{
if (!HasOnlyProperties(payload, "rowId", "field", "value") ||
!TryString(payload, "rowId", 64, out var rowId) ||
!IsSafeIdentifier(rowId) ||
!TryString(payload, "field", 24, out var fieldText) ||
!TrySafeString(
payload,
"value",
LegacyManualListsWorkflow.MaximumNetSellValueLength,
allowEmpty: true,
allowUnderscore: true,
out var value) ||
value.Contains('^'))
{
return null;
}
var field = fieldText switch
{
"leftName" => LegacyManualNetSellField.LeftName,
"leftAmount" => LegacyManualNetSellField.LeftAmount,
"rightName" => LegacyManualNetSellField.RightName,
"rightAmount" => LegacyManualNetSellField.RightAmount,
_ => (LegacyManualNetSellField?)null
};
return field.HasValue
? new LegacySetManualNetSellCellIntent(rowId, field.Value, value)
: null;
}
private static LegacyUiIntent? ParseManualViSearch(JsonElement payload) =>
HasOnlyProperties(payload, "query") &&
TrySafeString(
payload,
"query",
LegacyManualListsWorkflow.MaximumSearchLength,
allowEmpty: false,
allowUnderscore: true,
out var query)
? new LegacySearchManualViIntent(query)
: null;
private static LegacyUiIntent? ParseManualViResult(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 64, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacyAddManualViResultIntent(resultId)
: null;
private static LegacyUiIntent? ParseManualViSelection(JsonElement payload) =>
HasOnlyProperties(payload, "resultId") &&
TryString(payload, "resultId", 64, out var resultId) &&
IsSafeIdentifier(resultId)
? new LegacySelectManualViResultIntent(resultId)
: null;
private static LegacyUiIntent? ParseManualViMove(JsonElement payload)
{
if (!HasOnlyProperties(payload, "itemId", "direction") ||
!TryString(payload, "itemId", 64, out var itemId) ||
!IsSafeIdentifier(itemId) ||
!TryString(payload, "direction", 8, out var directionText))
{
return null;
}
var direction = directionText switch
{
"up" => LegacyManualListMoveDirection.Up,
"down" => LegacyManualListMoveDirection.Down,
_ => (LegacyManualListMoveDirection?)null
};
return direction.HasValue
? new LegacyMoveManualViItemIntent(itemId, direction.Value)
: null;
}
private static LegacyUiIntent? ParseManualViDelete(JsonElement payload) =>
HasOnlyProperties(payload, "itemId") &&
TryString(payload, "itemId", 64, out var itemId) &&
IsSafeIdentifier(itemId)
? new LegacyDeleteManualViItemIntent(itemId)
: null;
private static ManualFinancialRecord? ParseManualFinancialRecord(JsonElement element)
{
if (!element.TryGetProperty("screen", out var screenElement) ||
screenElement.ValueKind != JsonValueKind.String ||
!TryManualFinancialScreen(screenElement.GetString(), out var screen) ||
!TrySafeString(
element,
"stockName",
200,
allowEmpty: false,
allowUnderscore: true,
out var stockName))
{
return null;
}
var identity = new ManualFinancialIdentity(screen, stockName);
return screen switch
{
ManualFinancialScreenKind.RevenueComposition =>
ParseManualRevenue(element, identity),
ManualFinancialScreenKind.GrowthMetrics =>
ParseManualGrowth(element, identity),
ManualFinancialScreenKind.Sales =>
ParseManualQuarters(element, identity, operatingProfit: false),
ManualFinancialScreenKind.OperatingProfit =>
ParseManualQuarters(element, identity, operatingProfit: true),
_ => null
};
}
private static ManualRevenueCompositionRecord? ParseManualRevenue(
JsonElement element,
ManualFinancialIdentity identity)
{
if (!HasOnlyProperties(element, "screen", "stockName", "baseDate", "slices") ||
!TrySafeString(
element,
"baseDate",
200,
allowEmpty: true,
allowUnderscore: true,
out var baseDate) ||
!element.TryGetProperty("slices", out var slicesElement) ||
slicesElement.ValueKind != JsonValueKind.Array ||
slicesElement.GetArrayLength() != 5)
{
return null;
}
var slices = new ManualRevenueSlice?[5];
var index = 0;
foreach (var item in slicesElement.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Null)
{
slices[index++] = null;
continue;
}
if (item.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(item, "label", "percentageText") ||
!TrySafeString(
item,
"label",
200,
allowEmpty: false,
allowUnderscore: false,
out var label) ||
!TrySafeString(
item,
"percentageText",
64,
allowEmpty: false,
allowUnderscore: false,
out var percentageText) ||
!TryManualDoubleText(percentageText, out var percentage))
{
return null;
}
slices[index++] = new ManualRevenueSlice(label, percentageText, percentage);
}
return new ManualRevenueCompositionRecord(identity, baseDate, slices);
}
private static ManualGrowthMetricsRecord? ParseManualGrowth(
JsonElement element,
ManualFinancialIdentity identity)
{
if (!HasOnlyProperties(
element,
"screen",
"stockName",
"salesGrowth",
"operatingProfitGrowth",
"netAssetGrowth",
"netIncomeGrowth",
"periods") ||
!TryGrowthSeries(element, "salesGrowth", out var salesGrowth) ||
!TryGrowthSeries(
element,
"operatingProfitGrowth",
out var operatingProfitGrowth) ||
!TryGrowthSeries(element, "netAssetGrowth", out var netAssetGrowth) ||
!TryGrowthSeries(element, "netIncomeGrowth", out var netIncomeGrowth) ||
!TryStringArray(
element,
"periods",
4,
allowEmpty: true,
allowUnderscore: false,
out var periods))
{
return null;
}
return new ManualGrowthMetricsRecord(
identity,
salesGrowth,
operatingProfitGrowth,
netAssetGrowth,
netIncomeGrowth,
new ManualGrowthPeriodLabels(periods[0], periods[1], periods[2], periods[3]));
}
private static ManualFinancialRecord? ParseManualQuarters(
JsonElement element,
ManualFinancialIdentity identity,
bool operatingProfit)
{
if (!HasOnlyProperties(element, "screen", "stockName", "quarters") ||
!element.TryGetProperty("quarters", out var quartersElement) ||
quartersElement.ValueKind != JsonValueKind.Array ||
quartersElement.GetArrayLength() != 6)
{
return null;
}
var quarters = new ManualQuarterValue[6];
var index = 0;
foreach (var item in quartersElement.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(item, "quarter", "value") ||
!TrySafeString(
item,
"quarter",
200,
allowEmpty: false,
allowUnderscore: false,
out var quarter) ||
!item.TryGetProperty("value", out var valueElement) ||
valueElement.ValueKind != JsonValueKind.Number ||
!valueElement.TryGetInt32(out var value))
{
return null;
}
quarters[index++] = new ManualQuarterValue(quarter, value);
}
return operatingProfit
? new ManualOperatingProfitRecord(identity, quarters)
: new ManualSalesRecord(identity, quarters);
}
private static bool TryGrowthSeries(
JsonElement element,
string propertyName,
out ManualGrowthSeriesValues values)
{
values = new ManualGrowthSeriesValues(null, null, null, null);
if (!element.TryGetProperty(propertyName, out var array) ||
array.ValueKind != JsonValueKind.Array || array.GetArrayLength() != 4)
{
return false;
}
var result = new double?[4];
var index = 0;
foreach (var item in array.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Null)
{
result[index++] = null;
continue;
}
if (item.ValueKind != JsonValueKind.Number ||
!item.TryGetDouble(out var number) || !double.IsFinite(number) ||
Math.Abs(number) > 1_000_000_000_000d)
{
return false;
}
result[index++] = number;
}
values = new ManualGrowthSeriesValues(result[0], result[1], result[2], result[3]);
return true;
}
private static bool TryStringArray(
JsonElement element,
string propertyName,
int expectedCount,
bool allowEmpty,
bool allowUnderscore,
out string[] values)
{
values = [];
if (!element.TryGetProperty(propertyName, out var array) ||
array.ValueKind != JsonValueKind.Array ||
array.GetArrayLength() != expectedCount)
{
return false;
}
var result = new string[expectedCount];
var index = 0;
foreach (var item in array.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.String ||
!IsSafeBridgeText(
item.GetString() ?? string.Empty,
200,
allowEmpty,
allowUnderscore))
{
return false;
}
result[index++] = item.GetString()!;
}
values = result;
return true;
}
private static bool TryManualDoubleText(string value, out double number)
{
if (string.Equals(value, "-", StringComparison.Ordinal))
{
number = 0;
return true;
}
return double.TryParse(
value,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out number) &&
double.IsFinite(number) && Math.Abs(number) <= 1_000_000_000_000d;
}
private static bool TryManualFinancialScreen(
string? value,
out ManualFinancialScreenKind screen)
{
var parsed = value switch
{
"revenue-composition" => ManualFinancialScreenKind.RevenueComposition,
"growth-metrics" => ManualFinancialScreenKind.GrowthMetrics,
"sales" => ManualFinancialScreenKind.Sales,
"operating-profit" => ManualFinancialScreenKind.OperatingProfit,
_ => (ManualFinancialScreenKind?)null
};
screen = parsed.GetValueOrDefault();
return parsed.HasValue;
}
private static string ManualFinancialScreenId(ManualFinancialScreenKind screen) => screen switch
{
ManualFinancialScreenKind.RevenueComposition => "revenue-composition",
ManualFinancialScreenKind.GrowthMetrics => "growth-metrics",
ManualFinancialScreenKind.Sales => "sales",
ManualFinancialScreenKind.OperatingProfit => "operating-profit",
_ => throw new ArgumentOutOfRangeException(nameof(screen))
};
private static object? ProjectManualFinancialRecord(ManualFinancialRecord? record) =>
record switch
{
ManualRevenueCompositionRecord revenue => new
{
screen = ManualFinancialScreenId(revenue.Identity.Screen),
stockName = revenue.Identity.StockName,
revenue.BaseDate,
slices = revenue.Slices.Select(slice => slice is null ? null : new
{
slice.Label,
slice.PercentageText
})
},
ManualGrowthMetricsRecord growth => new
{
screen = ManualFinancialScreenId(growth.Identity.Screen),
stockName = growth.Identity.StockName,
salesGrowth = growth.SalesGrowth.ToArray(),
operatingProfitGrowth = growth.OperatingProfitGrowth.ToArray(),
netAssetGrowth = growth.NetAssetGrowth.ToArray(),
netIncomeGrowth = growth.NetIncomeGrowth.ToArray(),
periods = growth.Periods.ToArray()
},
ManualSalesRecord sales => new
{
screen = ManualFinancialScreenId(sales.Identity.Screen),
stockName = sales.Identity.StockName,
sales.Quarters
},
ManualOperatingProfitRecord profit => new
{
screen = ManualFinancialScreenId(profit.Identity.Screen),
stockName = profit.Identity.StockName,
profit.Quarters
},
null => null,
_ => throw new ArgumentOutOfRangeException(nameof(record))
};
private static bool TrySafeString(
JsonElement element,
string propertyName,
int maximumLength,
bool allowEmpty,
bool allowUnderscore,
out string value)
{
value = string.Empty;
return TryString(element, propertyName, maximumLength, out value) &&
IsSafeBridgeText(value, maximumLength, allowEmpty, allowUnderscore);
}
private static bool IsSafeBridgeText(
string value,
int maximumLength,
bool allowEmpty,
bool allowUnderscore) =>
value.Length <= maximumLength &&
(allowEmpty || value.Length > 0) &&
(allowUnderscore || !value.Contains('_')) &&
value.All(static character =>
!char.IsControl(character) && !char.IsSurrogate(character) &&
character != '\uFFFD');
private static LegacyUiIntent? ParseDismiss(JsonElement payload) =>
HasOnlyProperties(payload) ? new LegacyDismissDialogIntent() : null;
private static bool HasOnlyProperties(JsonElement element, params string[] names)
{
var expected = new HashSet<string>(names, StringComparer.Ordinal);
var count = 0;
foreach (var property in element.EnumerateObject())
{
count++;
if (!expected.Contains(property.Name))
{
return false;
}
}
return count == expected.Count;
}
private static bool TryString(
JsonElement element,
string propertyName,
int maximumLength,
out string value)
{
value = string.Empty;
return element.TryGetProperty(propertyName, out var property) &&
property.ValueKind == JsonValueKind.String &&
(value = property.GetString() ?? string.Empty).Length <= maximumLength;
}
private static bool TryBoolean(JsonElement element, string name, out bool value)
{
value = false;
if (!element.TryGetProperty(name, out var property) ||
(property.ValueKind != JsonValueKind.True && property.ValueKind != JsonValueKind.False))
{
return false;
}
value = property.GetBoolean();
return true;
}
private static bool TryTabId(
JsonElement element,
string name,
out LegacyOperatorTabId tabId)
{
tabId = default;
if (!TryString(element, name, 32, out var value))
{
return false;
}
var parsed = value switch
{
"overseas" => LegacyOperatorTabId.Overseas,
"exchange" => LegacyOperatorTabId.Exchange,
"index" => LegacyOperatorTabId.Index,
"kospiIndustry" => LegacyOperatorTabId.KospiIndustry,
"kosdaqIndustry" => LegacyOperatorTabId.KosdaqIndustry,
"comparison" => LegacyOperatorTabId.Comparison,
"theme" => LegacyOperatorTabId.Theme,
"overseasStocks" => LegacyOperatorTabId.OverseasStocks,
"expert" => LegacyOperatorTabId.Expert,
"tradingHalt" => LegacyOperatorTabId.TradingHalt,
_ => (LegacyOperatorTabId?)null
};
if (!parsed.HasValue)
{
return false;
}
tabId = parsed.Value;
return true;
}
private static bool TryInt32(
JsonElement element,
string name,
int minimum,
int maximum,
out int value)
{
value = 0;
return element.TryGetProperty(name, out var property) &&
property.ValueKind == JsonValueKind.Number &&
property.TryGetInt32(out value) &&
value >= minimum && value <= maximum;
}
private static LegacyDragDropPosition? ParseDragDropPosition(string value) => value switch
{
"before" => LegacyDragDropPosition.Before,
"after" => LegacyDragDropPosition.After,
_ => null
};
private static bool TryInt64(
JsonElement element,
string name,
long minimum,
long maximum,
out long value)
{
value = 0;
return element.TryGetProperty(name, out var property) &&
property.ValueKind == JsonValueKind.Number &&
property.TryGetInt64(out value) &&
value >= minimum && value <= maximum;
}
private static bool IsSafeIdentifier(string value)
{
if (value.Length == 0)
{
return false;
}
return value.All(character =>
char.IsAsciiLetterOrDigit(character) || character is '-' or '_');
}
private static bool IsSafeOpaqueActionId(string value) =>
value.Length is > 0 and <= 64 && value.All(character =>
char.IsLetterOrDigit(character) || character is '-' or '_');
private static JsonSerializerOptions CreateSerializerOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
return options;
}
}