using JdeScoping.DataSync.Configuration; using System.Collections.ObjectModel; using System.Text.Json; using System.Text.RegularExpressions; using System.Windows.Input; namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps; /// /// Base class for transformer step view models. /// public abstract class TransformerStepViewModelBase : PipelineStepViewModelBase { protected static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// /// Initializes a new instance of the class. /// /// Callback invoked when the transformer changes. protected TransformerStepViewModelBase(Action onChanged) : base(onChanged) { } /// public override PipelineStepType StepType => PipelineStepType.Transformer; /// public override string Icon => "󰁖"; // mdi-cog-transfer /// /// Gets the transformer type name. /// public abstract string TransformerType { get; } /// /// Converts this view model back to a TransformElement. /// public abstract TransformElement ToModel(); /// /// Helper to create a JsonElement from an object. /// /// The object to serialize into a JsonElement. /// A JsonElement containing the serialized configuration. protected static JsonElement CreateConfigElement(object config) { var json = JsonSerializer.Serialize(config, JsonOptions); using var doc = JsonDocument.Parse(json); return doc.RootElement.Clone(); } } /// /// View model for ColumnDrop transformer. /// public class ColumnDropTransformerViewModel : TransformerStepViewModelBase { private string _columnsText; /// /// Initializes a new instance of the class with an existing configuration. /// /// The transform element containing the column configuration. /// Callback invoked when the configuration changes. public ColumnDropTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged) { _columnsText = string.Empty; if (element.Config.HasValue) { if (element.Config.Value.TryGetProperty("columns", out var columnsProp) && columnsProp.ValueKind == JsonValueKind.Array) { var columns = columnsProp.EnumerateArray().Select(c => c.GetString() ?? "").Where(c => !string.IsNullOrEmpty(c)); _columnsText = string.Join("\n", columns); } } } /// /// Initializes a new instance of the class with default values. /// /// Callback invoked when the configuration changes. public ColumnDropTransformerViewModel(Action onChanged) : base(onChanged) { _columnsText = string.Empty; } /// public override string TransformerType => "ColumnDrop"; /// public override string DisplayName => "Column Drop"; /// public override string Summary => GetColumnCount() > 0 ? $"Drop {GetColumnCount()} columns" : "No columns"; /// /// Gets or sets the columns to drop as newline-separated text. /// public string ColumnsText { get => _columnsText; set { if (SetProperty(ref _columnsText, value ?? string.Empty)) { OnPropertyChanged(nameof(Summary)); NotifyChanged(); } } } /// /// Gets the columns as a list. /// public List GetColumns() { return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); } private int GetColumnCount() { return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Length; } /// public override TransformElement ToModel() => new() { TransformType = TransformerType, Config = CreateConfigElement(new { columns = GetColumns() }) }; } /// /// View model for ColumnRename transformer. /// public class ColumnRenameTransformerViewModel : TransformerStepViewModelBase { /// /// Initializes a new instance of the class with an existing configuration. /// /// The transform element containing the mapping configuration. /// Callback invoked when the configuration changes. public ColumnRenameTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged) { Mappings = []; if (element.Config.HasValue) { if (element.Config.Value.TryGetProperty("mappings", out var mappingsProp) && mappingsProp.ValueKind == JsonValueKind.Object) { foreach (var prop in mappingsProp.EnumerateObject()) { var newName = prop.Value.GetString() ?? ""; Mappings.Add(new ColumnMappingViewModel(prop.Name, newName, () => { OnPropertyChanged(nameof(Summary)); NotifyChanged(); })); } } } AddMappingCommand = new RelayCommand(AddMapping); } /// /// Initializes a new instance of the class with default values. /// /// Callback invoked when the configuration changes. public ColumnRenameTransformerViewModel(Action onChanged) : base(onChanged) { Mappings = []; AddMappingCommand = new RelayCommand(AddMapping); } /// public override string TransformerType => "ColumnRename"; /// public override string DisplayName => "Column Rename"; /// public override string Summary => Mappings.Count > 0 ? $"Rename {Mappings.Count} columns" : "No mappings"; /// /// Gets the collection of column mappings (old name -> new name). /// public ObservableCollection Mappings { get; } /// /// Gets the command to add a new mapping. /// public ICommand AddMappingCommand { get; } /// /// Adds a new mapping. /// public void AddMapping() { Mappings.Add(new ColumnMappingViewModel("", "", () => { OnPropertyChanged(nameof(Summary)); NotifyChanged(); })); OnPropertyChanged(nameof(Summary)); NotifyChanged(); } /// /// Removes a mapping. /// /// The column mapping to remove. public void RemoveMapping(ColumnMappingViewModel mapping) { if (Mappings.Remove(mapping)) { OnPropertyChanged(nameof(Summary)); NotifyChanged(); } } /// public override TransformElement ToModel() => new() { TransformType = TransformerType, Config = CreateConfigElement(new { mappings = Mappings.ToDictionary(m => m.OldName, m => m.NewName) }) }; } /// /// View model for a column rename mapping. /// public class ColumnMappingViewModel : ViewModelBase { private string _oldName; private string _newName; private readonly Action _onChanged; /// /// Initializes a new instance of the class. /// /// The original column name. /// The new column name. /// Callback invoked when the mapping changes. public ColumnMappingViewModel(string oldName, string newName, Action onChanged) { _oldName = oldName; _newName = newName; _onChanged = onChanged; } /// /// Gets or sets the original column name. /// public string OldName { get => _oldName; set { if (SetProperty(ref _oldName, value ?? string.Empty)) _onChanged(); } } /// /// Gets or sets the new column name. /// public string NewName { get => _newName; set { if (SetProperty(ref _newName, value ?? string.Empty)) _onChanged(); } } } /// /// View model for JdeDate transformer (converts JDE Julian date/time to DateTime). /// public class JdeDateTransformerViewModel : TransformerStepViewModelBase { private string? _dateColumn; private string? _timeColumn; private string? _outputColumn; /// /// Initializes a new instance of the class from a TransformElement. /// /// The transform element containing date/time column configuration. /// Callback invoked when the configuration changes. public JdeDateTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged) { _dateColumn = null; _timeColumn = null; _outputColumn = null; if (element.Config.HasValue) { if (element.Config.Value.TryGetProperty("dateColumn", out var dateProp)) _dateColumn = dateProp.GetString(); if (element.Config.Value.TryGetProperty("timeColumn", out var timeProp)) _timeColumn = timeProp.GetString(); if (element.Config.Value.TryGetProperty("outputColumn", out var outputProp)) _outputColumn = outputProp.GetString(); } } /// /// Initializes a new instance of the class with default values. /// /// Callback invoked when the configuration changes. public JdeDateTransformerViewModel(Action onChanged) : base(onChanged) { _dateColumn = null; _timeColumn = null; _outputColumn = null; } /// public override string TransformerType => "JdeDate"; /// public override string DisplayName => "JDE Date Convert"; /// public override string Icon => "󰃭"; // mdi-calendar /// public override string Summary => !string.IsNullOrEmpty(_outputColumn) ? $"→ {_outputColumn}" : "Configure..."; /// /// Gets or sets the date column name. /// public string? DateColumn { get => _dateColumn; set { if (SetProperty(ref _dateColumn, value)) { OnPropertyChanged(nameof(Summary)); NotifyChanged(); } } } /// /// Gets or sets the time column name. /// public string? TimeColumn { get => _timeColumn; set { if (SetProperty(ref _timeColumn, value)) NotifyChanged(); } } /// /// Gets or sets the output column name. /// public string? OutputColumn { get => _outputColumn; set { if (SetProperty(ref _outputColumn, value)) { OnPropertyChanged(nameof(Summary)); NotifyChanged(); } } } /// public override TransformElement ToModel() => new() { TransformType = TransformerType, Config = CreateConfigElement(new { dateColumn = _dateColumn, timeColumn = _timeColumn, outputColumn = _outputColumn }) }; } /// /// Specifies behavior when a regex pattern does not match the input value. /// public enum NonMatchBehavior { /// Keep the original value unchanged. KeepOriginal, /// Return null/DBNull. ReturnNull, /// Return an empty string. ReturnEmpty } /// /// View model for Regex transformer. /// public class RegexTransformerViewModel : TransformerStepViewModelBase { private string _columnName = string.Empty; private string _pattern = string.Empty; private string? _replacement = string.Empty; private bool _isFindReplaceMode = true; private bool _ignoreCase; private NonMatchBehavior _nonMatchBehavior = NonMatchBehavior.KeepOriginal; // Test feature fields private string _testInput = string.Empty; private string _testResultValue = string.Empty; private string _testResultLabel = string.Empty; private string _testResultIcon = string.Empty; private string _testResultBackground = string.Empty; private bool _hasTestResult; private bool _hasTestError; private string _testErrorMessage = string.Empty; /// /// Initializes a new instance of the class from a TransformElement. /// /// The transform element containing regex configuration. /// Callback invoked when the configuration changes. public RegexTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged) { _columnName = string.Empty; _pattern = string.Empty; _replacement = null; _isFindReplaceMode = true; _ignoreCase = false; _nonMatchBehavior = NonMatchBehavior.KeepOriginal; if (element.Config.HasValue) { if (element.Config.Value.TryGetProperty("columnName", out var colProp)) _columnName = colProp.GetString() ?? string.Empty; if (element.Config.Value.TryGetProperty("pattern", out var patternProp)) _pattern = patternProp.GetString() ?? string.Empty; if (element.Config.Value.TryGetProperty("replacement", out var replaceProp)) { _replacement = replaceProp.ValueKind == JsonValueKind.Null ? null : replaceProp.GetString(); _isFindReplaceMode = _replacement != null; } else { // No replacement property means Match & Extract mode _replacement = null; _isFindReplaceMode = false; } if (element.Config.Value.TryGetProperty("ignoreCase", out var ignoreProp)) _ignoreCase = ignoreProp.GetBoolean(); if (element.Config.Value.TryGetProperty("nonMatchBehavior", out var behaviorProp)) { var behaviorStr = behaviorProp.GetString(); if (Enum.TryParse(behaviorStr, true, out var behavior)) _nonMatchBehavior = behavior; } } TestPatternCommand = new RelayCommand(ExecuteTestPattern); } /// /// Initializes a new instance of the class with default values. /// /// Callback invoked when the configuration changes. public RegexTransformerViewModel(Action onChanged) : base(onChanged) { TestPatternCommand = new RelayCommand(ExecuteTestPattern); } /// public override string TransformerType => "Regex"; /// public override string DisplayName => "Regex Transform"; /// public override string Icon => "󰑑"; // mdi-regex /// public override string Summary => !string.IsNullOrEmpty(_columnName) ? $"{_columnName}: {(_isFindReplaceMode ? "Replace" : "Extract")}" : "Configure..."; /// Gets or sets the column name to transform. public string ColumnName { get => _columnName; set { if (SetProperty(ref _columnName, value ?? string.Empty)) { OnPropertyChanged(nameof(Summary)); NotifyChanged(); } } } /// Gets or sets the regex pattern. public string Pattern { get => _pattern; set { if (SetProperty(ref _pattern, value ?? string.Empty)) { ClearTestResult(); NotifyChanged(); } } } /// Gets or sets the replacement string (Find & Replace mode). public string? Replacement { get => _replacement; set { if (SetProperty(ref _replacement, value)) { ClearTestResult(); NotifyChanged(); } } } /// Gets or sets whether Find & Replace mode is active. public bool IsFindReplaceMode { get => _isFindReplaceMode; set { if (SetProperty(ref _isFindReplaceMode, value)) { OnPropertyChanged(nameof(IsMatchExtractMode)); OnPropertyChanged(nameof(PatternHelpText)); OnPropertyChanged(nameof(Summary)); ClearTestResult(); NotifyChanged(); } } } /// Gets or sets whether Match & Extract mode is active. public bool IsMatchExtractMode { get => !_isFindReplaceMode; set => IsFindReplaceMode = !value; } /// Gets the help text for the pattern field based on current mode. public string PatternHelpText => _isFindReplaceMode ? "Pattern to search for in the column value" : "Pattern with capture group - first group (parentheses) will be extracted"; /// Gets or sets whether matching is case-insensitive. public bool IgnoreCase { get => _ignoreCase; set { if (SetProperty(ref _ignoreCase, value)) { ClearTestResult(); NotifyChanged(); } } } /// Gets or sets the behavior when pattern doesn't match. public NonMatchBehavior NonMatchBehavior { get => _nonMatchBehavior; set { if (SetProperty(ref _nonMatchBehavior, value)) { ClearTestResult(); NotifyChanged(); } } } /// Gets the available non-match behavior options for binding. public static IReadOnlyList NonMatchBehaviorOptions { get; } = [NonMatchBehavior.KeepOriginal, NonMatchBehavior.ReturnNull, NonMatchBehavior.ReturnEmpty]; // Test feature properties /// /// Gets or sets the test input value for pattern testing. /// public string TestInput { get => _testInput; set => SetProperty(ref _testInput, value ?? string.Empty); } /// /// Gets or sets the result value from pattern testing. /// public string TestResultValue { get => _testResultValue; set => SetProperty(ref _testResultValue, value); } /// /// Gets or sets the label describing the test result (e.g., "Output" or "No Match"). /// public string TestResultLabel { get => _testResultLabel; set => SetProperty(ref _testResultLabel, value); } /// /// Gets or sets the icon for the test result. /// public string TestResultIcon { get => _testResultIcon; set => SetProperty(ref _testResultIcon, value); } /// /// Gets or sets the background color for the test result display. /// public string TestResultBackground { get => _testResultBackground; set => SetProperty(ref _testResultBackground, value); } /// /// Gets or sets a value indicating whether a test result is available. /// public bool HasTestResult { get => _hasTestResult; set => SetProperty(ref _hasTestResult, value); } /// /// Gets or sets a value indicating whether a test error occurred. /// public bool HasTestError { get => _hasTestError; set => SetProperty(ref _hasTestError, value); } /// /// Gets or sets the error message from test execution. /// public string TestErrorMessage { get => _testErrorMessage; set => SetProperty(ref _testErrorMessage, value); } /// /// Gets the command to execute pattern testing. /// public ICommand TestPatternCommand { get; } private void ExecuteTestPattern() { ClearTestResult(); if (string.IsNullOrEmpty(_pattern)) { HasTestError = true; TestErrorMessage = "Pattern is required"; return; } try { var options = _ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; var regex = new Regex(_pattern, options); string result; bool matched; if (_isFindReplaceMode) { result = regex.Replace(_testInput, _replacement ?? string.Empty); matched = regex.IsMatch(_testInput); } else { var match = regex.Match(_testInput); if (match.Success && match.Groups.Count > 1) { result = match.Groups[1].Value; matched = true; } else { matched = false; result = _nonMatchBehavior switch { NonMatchBehavior.ReturnNull => "(null)", NonMatchBehavior.ReturnEmpty => "(empty)", _ => _testInput }; } } HasTestResult = true; TestResultValue = result; TestResultLabel = matched ? "Output" : "No Match"; TestResultIcon = matched ? "✓" : "—"; TestResultBackground = matched ? "#22C55E" : "#F59E0B"; } catch (RegexParseException ex) { HasTestError = true; TestErrorMessage = ex.Message; } } private void ClearTestResult() { HasTestResult = false; HasTestError = false; TestResultValue = string.Empty; TestResultLabel = string.Empty; TestErrorMessage = string.Empty; } /// public override TransformElement ToModel() => new() { TransformType = TransformerType, Config = CreateConfigElement(new { columnName = _columnName, pattern = _pattern, replacement = _isFindReplaceMode ? _replacement : null, ignoreCase = _ignoreCase, nonMatchBehavior = _nonMatchBehavior.ToString() }) }; } /// /// Factory methods for creating transformer view models. /// public static class TransformerFactory { /// /// Creates a transformer view model from a TransformElement. /// /// The transform element containing configuration. /// Callback invoked when the view model changes. /// A transformer view model of the appropriate type, or null if the transformer type is unknown. public static TransformerStepViewModelBase? Create(TransformElement element, Action onChanged) { return element.TransformType?.ToLowerInvariant() switch { "columndrop" => new ColumnDropTransformerViewModel(element, onChanged), "columnrename" => new ColumnRenameTransformerViewModel(element, onChanged), "jdedate" => new JdeDateTransformerViewModel(element, onChanged), "regex" => new RegexTransformerViewModel(element, onChanged), _ => null // Unknown transformer type }; } /// /// Creates a new transformer view model by type name. /// /// The type name of the transformer to create. /// Callback invoked when the view model changes. /// A new transformer view model of the specified type, or null if the type name is unknown. public static TransformerStepViewModelBase? CreateNew(string typeName, Action onChanged) { return typeName?.ToLowerInvariant() switch { "columndrop" => new ColumnDropTransformerViewModel(onChanged), "columnrename" => new ColumnRenameTransformerViewModel(onChanged), "jdedate" => new JdeDateTransformerViewModel(onChanged), "regex" => new RegexTransformerViewModel(onChanged), _ => null }; } /// /// Gets the list of available transformer type names. /// public static IReadOnlyList AvailableTypes => ["ColumnDrop", "ColumnRename", "JdeDate", "Regex"]; }