diff --git a/src/WireMockInspector/ViewModels/MainWindowViewModel.cs b/src/WireMockInspector/ViewModels/MainWindowViewModel.cs index 8a1d229..9e11a29 100644 --- a/src/WireMockInspector/ViewModels/MainWindowViewModel.cs +++ b/src/WireMockInspector/ViewModels/MainWindowViewModel.cs @@ -6,17 +6,20 @@ using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; +using System.Xml; using DynamicData; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using ReactiveUI; using RestEase; +using TextMateSharp.Internal.Grammars.Parser; using WireMock.Admin.Mappings; using WireMock.Admin.Requests; using WireMock.Admin.Scenarios; using WireMock.Admin.Settings; using WireMock.Client; using WireMock.Types; +using Formatting = Newtonsoft.Json.Formatting; namespace WireMockInspector.ViewModels { @@ -288,7 +291,7 @@ public MainWindowViewModel() }, Scenario = model.Scenario is {} scenarioName && enrichedScenarios.TryGetValue(scenarioName, out var scenario)? scenario with {CurrentTransitionId = mappingId, ThisMappingTransition = $"[{model.WhenStateIs}] -> [{model.SetStateTo}]"}: null }; - }).OfType().OrderBy(x=>x.UpdatedOn); + }).OfType().OrderByDescending(x=>x.UpdatedOn); Mappings.AddRange(mappings); MappingSearchTerm = string.Empty; DataLoaded = true; @@ -681,8 +684,6 @@ public int RequestTypeFilter private static MappingDetails GetMappingDetails(RequestViewModel req, MappingModel expectations) { var isPerfectMatch = req.Raw.RequestMatchResult?.IsPerfectMatch == true; - var statusCode = req.Raw.Response?.StatusCode?.ToString()?? string.Empty; - var statusCodeFormatted = $"{statusCode} ({HttpStatusCodeToDescriptionConverter.TranslateStatusCode(statusCode)})"; return new MappingDetails { MatchingStatus = req.MatchingStatus, @@ -703,7 +704,12 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Value = req.Raw.Request.ClientIP }, - Expectations = ExpectationsAsJson(expectations.Request?.ClientIP), + Expectations = CastAsModel(expectations.Request?.ClientIP) switch + { + string s => new SimpleStringExpectations {Value = s}, + ClientIPModel {Matchers: {} } cim => MapToRichExpectations(cim, cim.Matchers, cim.MatchOperator), + _ => MissingExpectations.Instance, + }, NoExpectations = expectations.Request?.ClientIP is null }, new() @@ -714,7 +720,11 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Value = req.Raw.Request.Method }, - Expectations = ExpectationsAsJson(expectations.Request?.Methods), + Expectations = expectations.Request?.Methods switch + { + string[] methods=> new SimpleStringExpectations {Value = string.Join(", ", methods)}, + _ => MissingExpectations.Instance + }, NoExpectations = expectations.Request?.Methods is null }, new() @@ -725,7 +735,12 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Value = req.Raw.Request.Url }, - Expectations = ExpectationsAsJson(expectations.Request?.Url), + Expectations = CastAsModel(expectations.Request?.Url) switch + { + string s => new SimpleStringExpectations {Value = s}, + UrlModel {Matchers:{}} urlModel => MapToRichExpectations(urlModel, urlModel.Matchers, urlModel.MatchOperator), + _ => MissingExpectations.Instance + }, NoExpectations = expectations.Request?.Url is null }, new() @@ -736,7 +751,12 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Value = req.Raw.Request.Path }, - Expectations = ExpectationsAsJson(expectations.Request?.Path), + Expectations = CastAsModel(expectations.Request?.Path) switch + { + string s => new SimpleStringExpectations {Value = s}, + PathModel {Matchers: { }} pathModel => MapToRichExpectations(pathModel, pathModel.Matchers, pathModel.MatchOperator), + _ => MissingExpectations.Instance + }, NoExpectations = expectations.Request?.Path is null }, new() @@ -747,7 +767,18 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Items = req.Raw.Request.Headers?.OrderBy(x=>x.Key).SelectMany(x=> x.Value.Select(v => new KeyValuePair(x.Key, v))).ToList() ?? new List>() }, - Expectations = ExpectationsAsJson(expectations.Request?.Headers), + Expectations = expectations.Request?.Headers switch + { + IList headers => new GridExpectations() + { + Items = headers.Select(x=> new GridExpectationItem + { + Name = x.Name, + Matchers = x.Matchers!=null ? MapToRichExpectations(x, x.Matchers.ToArray(), x.MatchOperator).Matchers: Array.Empty() + }).ToList() + }, + _ => MissingExpectations.Instance + }, NoExpectations = expectations.Request?.Headers is null }, new() @@ -769,7 +800,18 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Items = req.Raw.Request.Query?.OrderBy(x=>x.Key).SelectMany(x=> x.Value.Select(v => new KeyValuePair(x.Key, v))).ToList() ?? new List>(), }, - Expectations = ExpectationsAsJson(expectations.Request?.Params), + Expectations = expectations.Request?.Params switch + { + IList paramModels => new GridExpectations() + { + Items = paramModels.Select(x=> new GridExpectationItem + { + Name = x.Name, + Matchers = x.Matchers!=null ? MapToRichExpectations(x, x.Matchers.ToArray(), null).Matchers: Array.Empty() + }).ToList() + }, + _ => MissingExpectations.Instance + }, NoExpectations = expectations.Request?.Params is null }, new() @@ -787,7 +829,7 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod _ => new MarkdownCode("plaintext", "") } }, - Expectations = ExpectationsAsJson(expectations.Request?.Body), + Expectations = MapToRichExpectations(expectations.Request?.Body), NoExpectations = expectations.Request?.Body is null } }, @@ -799,9 +841,13 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod Matched = isPerfectMatch, ActualValue = new SimpleActualValue { - Value = statusCodeFormatted + Value = FormatStatusCode(req.Raw.Response.StatusCode) }, - Expectations = ExpectationsAsJson(expectations.Response?.StatusCode?.ToString()) + Expectations = expectations.Response?.StatusCode switch + { + not null => new SimpleStringExpectations(){Value = FormatStatusCode(expectations.Response?.StatusCode)}, + _ => MissingExpectations.Instance + } }, new () { @@ -811,7 +857,19 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Items = req.Raw.Response?.Headers?.OrderBy(x=>x.Key).SelectMany(x=> x.Value.Select(v => new KeyValuePair(x.Key, v))).ToList() ?? new List>() }, - Expectations = ExpectationsAsJson(expectations.Response?.Headers) + Expectations = expectations.Response?.Headers switch + { + not null => new SimpleKeyValueExpectations + { + Items = expectations.Response?.Headers.OrderBy(x=>x.Key).SelectMany(x=> x.Value switch + { + string v=> new[]{new KeyValuePair(x.Key, v)}, + JArray vals => vals.ToObject().Select(vv=> new KeyValuePair(x.Key, vv)), + _ => Array.Empty>() + } ).ToList() ?? new List>() + }, + _ => MissingExpectations.Instance + } }, new () { @@ -821,16 +879,119 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod { Value = GetActualForRequestBody(req) }, - Expectations = expectations.Response switch + Expectations = new RawExpectations() + { + Definition = expectations.Response switch + { + {Body: {} bodyResponse} => WrapBodyInMarkdown(bodyResponse), + {BodyAsJson: {} bodyAsJson} => new MarkdownCode("json", bodyAsJson.ToString()!), + {BodyAsBytes: {} bodyAsBytes} => new MarkdownCode("plaintext", bodyAsBytes.ToString()?? string.Empty), + {BodyAsFile: {} bodyAsFile} => new MarkdownCode("plaintext",bodyAsFile), + _ => new MarkdownCode("plaintext",string.Empty) + } + } + } + } + }; + } + + private static string FormatStatusCode(object? code) + { + var statusCode = code?.ToString() ?? string.Empty; + var statusCodeFormatted = $"{statusCode} ({HttpStatusCodeToDescriptionConverter.TranslateStatusCode(statusCode)})"; + return statusCodeFormatted; + } + + private static object CastAsModel(object? input) + { + if (input is JObject o) + { + return o.ToObject(); + } + + return input; + } + + private static ExpectationsModel MapToRichExpectations(BodyModel? requestBody) + { + if (requestBody == null) + return MissingExpectations.Instance; + + var matchers = requestBody.Matcher != null ? new[] {requestBody.Matcher!} : requestBody.Matchers ?? Array.Empty(); + + return MapToRichExpectations(requestBody, matchers, requestBody.MatchOperator); + } + + private static RichExpectations MapToRichExpectations(object definition, IReadOnlyList matchers, string? matchOperator) + { + IEnumerable GetPatterns(MatcherModel m) + { + if (string.IsNullOrWhiteSpace(m.Pattern.ToString()) == false) + { + yield return m.Pattern.ToString(); + } + else + { + foreach (var pattern in m.Patterns) + { + if (string.IsNullOrWhiteSpace(pattern.ToString()) == false) { - {Body: {} bodyResponse} => WrapBodyInMarkdown(bodyResponse), - {BodyAsJson: {} bodyAsJson} => new MarkdownCode("json", bodyAsJson.ToString()!), - {BodyAsBytes: {} bodyAsBytes} => new MarkdownCode("plaintext", bodyAsBytes.ToString()?? string.Empty), - {BodyAsFile: {} bodyAsFile} => new MarkdownCode("plaintext",bodyAsFile), - _ => new MarkdownCode("plaintext",string.Empty) + yield return pattern.ToString(); } } } + } + + IEnumerable GetTags(MatcherModel m) + { + yield return m.Name; + + if (m.IgnoreCase == true) + { + yield return "Ignore case"; + } + else + { + yield return "Case sensitive"; + } + + if (m.RejectOnMatch == true) + { + yield return "Reject on match"; + } + + if (m.Regex == true) + { + yield return "Regex"; + } + + if (string.IsNullOrWhiteSpace(m.MatchOperator) == false) + { + yield return $"Match operator: {m.MatchOperator}"; + } + } + + return new RichExpectations + { + Definition = AsMarkdownCode("json", JsonConvert.SerializeObject(definition, Formatting.Indented)), + Operator = matchOperator, + Matchers = matchers.Select(x => new ExpectationMatcher() + { + Attributes = new[] + { + new KeyValuePair("Matcher", x.Name), + new KeyValuePair("Reject on match", x.RejectOnMatch == true ? "✅" : "❌"), + new KeyValuePair("Ignore case", x.IgnoreCase == true ? "✅" : "❌"), + new KeyValuePair("Regex", x.Regex == true ? "✅" : "❌"), + new KeyValuePair("Operator", x.MatchOperator?.ToString()), + }.Where(x => string.IsNullOrWhiteSpace(x.Value) == false).ToList(), + Tags = GetTags(x).ToList(), + Patterns = GetPatterns(x).Select(y => y.Trim() switch + { + var v when v.StartsWith("{") || v.StartsWith("[") => (Text) new MarkdownCode("json", y).TryToReformat(), + _ => (Text)new SimpleText(y) + } ).ToList() + }).ToList() }; } @@ -873,14 +1034,17 @@ public string RequestSearchTerm - private static MarkdownCode ExpectationsAsJson(object? data) + private static ExpectationsModel ExpectationsAsJson(object? data) { if (data == null) { - return new MarkdownCode("plaintext", string.Empty); + return MissingExpectations.Instance; } - return AsMarkdownCode("json", JsonConvert.SerializeObject(data, Formatting.Indented)); + return new RawExpectations() + { + Definition = AsMarkdownCode("json", JsonConvert.SerializeObject(data, Formatting.Indented)) + }; } private static bool? IsMatched(RequestViewModel req, string rule) diff --git a/src/WireMockInspector/ViewModels/MatchDetailsViewModel.cs b/src/WireMockInspector/ViewModels/MatchDetailsViewModel.cs index 2cb0582..e256250 100644 --- a/src/WireMockInspector/ViewModels/MatchDetailsViewModel.cs +++ b/src/WireMockInspector/ViewModels/MatchDetailsViewModel.cs @@ -11,10 +11,62 @@ namespace WireMockInspector.ViewModels; +public class ExpectationMatcher +{ + + public IReadOnlyList> Attributes { get; set; } + public List Tags { get; set; } + public List Patterns { get; set; } +} + +public abstract class ExpectationsModel +{ + +} + +public class SimpleKeyValueExpectations: ExpectationsModel +{ + public IReadOnlyList> Items { get; set; } + +} + +public class GridExpectationItem +{ + public string Name { get; set; } + public IReadOnlyList Matchers { get; set; } +} +public class GridExpectations: ExpectationsModel +{ + public IReadOnlyList Items { get; set; } + +} + +class SimpleStringExpectations : ExpectationsModel +{ + public string Value { get; set; } +} + +public class MissingExpectations:ExpectationsModel +{ + public static readonly MissingExpectations Instance = new MissingExpectations(); +} + +public class RawExpectations:ExpectationsModel +{ + public MarkdownCode Definition { get; set; } +} + +public class RichExpectations:ExpectationsModel +{ + public MarkdownCode Definition { get; set; } + public string? Operator { get; set; } + public List Matchers { get; set; } +} + public class MatchDetailsViewModel:ViewModelBase { private ActualValue _actualValue; - private MarkdownCode _expectations; + public string RuleName { get; set; } public bool? Matched { get; set; } public bool NoExpectations { get; set; } @@ -25,19 +77,20 @@ public ActualValue ActualValue set => this.RaiseAndSetIfChanged(ref _actualValue, value); } - - - public MarkdownCode Expectations + public ExpectationsModel Expectations { get => _expectations; - set => this.RaiseAndSetIfChanged(ref _expectations, value); + set => this.RaiseAndSetIfChanged(ref _expectations, value); } + private ExpectationsModel _expectations; + + + + public ICommand ReformatActualValue { get; set; } public ICommand CopyActualValue { get; set; } - public ICommand ReformatExpectations{ get; set; } - public ICommand CopyExpectations{ get; set; } public MatchDetailsViewModel() { @@ -62,16 +115,7 @@ public MatchDetailsViewModel() }); - CopyExpectations = ReactiveCommand.Create(async () => - { - if (Expectations is MarkdownCode{rawValue: var value}) - { - if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - await desktop.MainWindow!.Clipboard.SetTextAsync(value); - } - } - }); + ReformatActualValue = ReactiveCommand.Create(() => { @@ -79,47 +123,18 @@ public MatchDetailsViewModel() { ActualValue = new MarkdownActualValue() { - Value = TryToReformat(rawValue) + Value = rawValue.TryToReformat() }; } }, this.WhenAnyValue(x=>x.ActualValue).Select(x => { - return x is MarkdownActualValue {Value: { } va} && IsJsonMarkdown(va); + return x is MarkdownActualValue {Value: { } va} && va.IsJsonMarkdown(); })); - ReformatExpectations= ReactiveCommand.Create(() => - { - if (string.IsNullOrWhiteSpace(Expectations.rawValue) == false) - { - Expectations = TryToReformat(Expectations); - } - }, this.WhenAnyValue(x=>x.Expectations).Select(IsJsonMarkdown)); - } - - private static MarkdownCode TryToReformat(MarkdownCode markdownCode) - { - if (IsJsonMarkdown(markdownCode)) - { - - try - { - - var formatted = JToken.Parse(markdownCode.rawValue).ToString(Formatting.Indented); - return MainWindowViewModel.AsMarkdownCode("json", formatted); - } - catch (Exception e) - { - - } - } - - return markdownCode; + } - private static bool IsJsonMarkdown(MarkdownCode rawValue) - { - return rawValue?.lang == "json"; - } + } @@ -139,7 +154,10 @@ public class MarkdownActualValue:ActualValue public string MarkdownValue { get; set; } } -public record MarkdownCode(string lang, string rawValue) +public record Text(); + +public record SimpleText(string Value):Text; +public record MarkdownCode(string lang, string rawValue):Text { public string AsMarkdownSyntax() { @@ -152,11 +170,36 @@ public string AsMarkdownSyntax() } public override string ToString() => AsMarkdownSyntax(); + + public MarkdownCode TryToReformat() + { + if (IsJsonMarkdown()) + { + + try + { + + var formatted = JToken.Parse(this.rawValue).ToString(Formatting.Indented); + return MainWindowViewModel.AsMarkdownCode("json", formatted); + } + catch (Exception e) + { + + } + } + + return this; + } + + public bool IsJsonMarkdown() + { + return this.lang == "json"; + } }; public class KeyValueListActualValue:ActualValue { - public KeyValuePair SelectedActualValueGridItem { get; set; } + public KeyValuePair SelectedActualValueGridItem { get; set; } = new KeyValuePair(); public IReadOnlyList> Items { get; set; } } diff --git a/src/WireMockInspector/Views/CodeBlockViewer.cs b/src/WireMockInspector/Views/CodeBlockViewer.cs index fefaf66..4abd6e7 100644 --- a/src/WireMockInspector/Views/CodeBlockViewer.cs +++ b/src/WireMockInspector/Views/CodeBlockViewer.cs @@ -1,7 +1,6 @@ using System; using Avalonia; using Avalonia.Media; -using Avalonia.Styling; using AvaloniaEdit; using AvaloniaEdit.Document; using AvaloniaEdit.TextMate; @@ -10,25 +9,26 @@ namespace WireMockInspector.Views; -public class CodeBlockViewer:TextEditor, IStyleable +public class CodeBlockViewer : TextEditor { - Type IStyleable.StyleKey => typeof(TextEditor); + protected override Type StyleKeyOverride { get; } = typeof(TextEditor); + public CodeBlockViewer() { - this.Initialized += (sender, args) => - { - //First of all you need to have a reference for your TextEditor for it to be used inside AvaloniaEdit.TextMate project. - var _textEditor = this; - _textEditor.Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)); - _textEditor.TextArea.TextView.Margin = new Thickness(10); - _textEditor.ShowLineNumbers = true; - _textEditor.IsReadOnly = true; - _textEditor.FontFamily = "Cascadia Code,Consolas,Menlo,Monospace"; - }; this._registryOptions = new RegistryOptions(ThemeName.DarkPlus); this._textMateInstallation = this.InstallTextMate(_registryOptions); } + protected override void OnInitialized() + { + base.OnInitialized(); + this.Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)); + this.TextArea.TextView.Margin = new Thickness(10, 0); + this.ShowLineNumbers = true; + this.IsReadOnly = true; + this.FontFamily = "Cascadia Code,Consolas,Menlo,Monospace"; + } + static CodeBlockViewer() { CodeProperty.Changed.Subscribe(OnCodeChanged); @@ -62,7 +62,10 @@ private void SetMarkdown(ViewModels.MarkdownCode md) _currentLang = md.lang; CodeBlockViewer _textEditor = this; - _textMateInstallation.SetGrammar(_registryOptions.GetScopeByLanguageId(_registryOptions.GetLanguageByExtension("."+md.lang).Id)); + if (_registryOptions.GetLanguageByExtension("." + md.lang) is { } languageByExtension) + { + _textMateInstallation.SetGrammar(_registryOptions.GetScopeByLanguageId(languageByExtension.Id)); + } } } else diff --git a/src/WireMockInspector/Views/MappingPage.axaml b/src/WireMockInspector/Views/MappingPage.axaml index 8365b18..dce14bd 100644 --- a/src/WireMockInspector/Views/MappingPage.axaml +++ b/src/WireMockInspector/Views/MappingPage.axaml @@ -65,7 +65,7 @@ Classes.partial="{Binding HitType, Converter={x:Static viewModels:StringMatchConverter.Instance}, ConverterParameter=OnlyPartialMatch}" Classes.unmatched="{Binding HitType, Converter={x:Static viewModels:StringMatchConverter.Instance}, ConverterParameter=Unmatched}" > - + diff --git a/src/WireMockInspector/Views/RequestDetails.axaml b/src/WireMockInspector/Views/RequestDetails.axaml index 62c27f2..1d6d206 100644 --- a/src/WireMockInspector/Views/RequestDetails.axaml +++ b/src/WireMockInspector/Views/RequestDetails.axaml @@ -57,7 +57,7 @@ - + @@ -80,7 +80,7 @@ - + - - + + - + - - - - - - - - - - - (No Expectations) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (No Expectations) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WireMockInspector/Views/RequestList.axaml b/src/WireMockInspector/Views/RequestList.axaml index 0649d6c..4fdb97c 100644 --- a/src/WireMockInspector/Views/RequestList.axaml +++ b/src/WireMockInspector/Views/RequestList.axaml @@ -28,7 +28,7 @@ - +