Skip to content

Commit

Permalink
Add diff view for request body
Browse files Browse the repository at this point in the history
  • Loading branch information
cezarypiatek committed Dec 17, 2023
1 parent 7feca97 commit 7f23ee7
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 35 deletions.
61 changes: 61 additions & 0 deletions src/WireMockInspector/ViewModels/JsonDiff.cs
@@ -0,0 +1,61 @@
using System;
using System.Linq;
using Newtonsoft.Json.Linq;

namespace WireMockInspector.ViewModels;

public class AlphabeticallySortedJsonDiffFormatter
{
public static readonly AlphabeticallySortedJsonDiffFormatter Instance = new AlphabeticallySortedJsonDiffFormatter();

public string Format(JToken? diff)
{
if (diff == null)
{
return string.Empty;
}

if (diff is JObject jObject)
{
if(jObject.DeepClone() is JObject sorted)
{
Sort(sorted);
return sorted.ToString();
}

return jObject.ToString();
}
return diff.ToString();
}

private static void Sort(JObject jObj)
{
var props = jObj.Properties().ToList();
foreach (var prop in props)
{
prop.Remove();
}

foreach (var prop in props.OrderBy(p => p.Name))
{
jObj.Add(prop);
if (prop.Value is JObject)
Sort((JObject)prop.Value);
}
}
}

public static class JsonHelper
{
public static string ToComparableForm(string json)
{
try
{
return AlphabeticallySortedJsonDiffFormatter.Instance.Format(JToken.Parse(json));
}
catch (Exception e)
{
return json;
}
}
}
188 changes: 160 additions & 28 deletions src/WireMockInspector/ViewModels/MainWindowViewModel.cs
Expand Up @@ -7,7 +7,11 @@
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using DiffPlex;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using DynamicData;
using JsonDiffPatchDotNet;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ReactiveUI;
Expand All @@ -19,6 +23,7 @@
using WireMock.Admin.Settings;
using WireMock.Client;
using WireMock.Types;
using ChangeType = DiffPlex.DiffBuilder.Model.ChangeType;
using Formatting = Newtonsoft.Json.Formatting;

namespace WireMockInspector.ViewModels
Expand Down Expand Up @@ -223,7 +228,7 @@ public MainWindowViewModel()
{
LastHit = hitCalculator.GetFirstPerfectHitDateAfter(m.Guid, estimatedScenarioStateDate ?? DateTime.MinValue),
Description = $"[{m.WhenStateIs}] -> [{m.SetStateTo}]",
MappingDefinition = AsMarkdownCode("json", JsonConvert.SerializeObject(m, Formatting.Indented)),
MappingDefinition = new MarkdownCode("json", JsonConvert.SerializeObject(m, Formatting.Indented)),
TriggeredBy = new RequestLogViewModel()
{
MapToLogEntries(hitCalculator.GetPerfectHitCountAfter(m.Guid, estimatedScenarioStateDate))
Expand Down Expand Up @@ -272,7 +277,7 @@ public MainWindowViewModel()
ExpectedPaths = GetExpectedPathsDescription(model),
Description = model.Title != model.Description? model.Description: null,
UpdatedOn = model.UpdatedAt,
Content = AsMarkdownCode("json", JsonConvert.SerializeObject(model, Formatting.Indented)),
Content = new MarkdownCode("json", JsonConvert.SerializeObject(model, Formatting.Indented)),
PartialHitCount = partialHitCount,
PerfectHitCount = perfectHitCount,
PerfectMatches = new RequestLogViewModel()
Expand Down Expand Up @@ -438,7 +443,7 @@ public MainWindowViewModel()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(code =>
{
SelectedMapping.Code = AsMarkdownCode("cs", code);
SelectedMapping.Code = new MarkdownCode("cs", code);
});

this.WhenAnyValue(x => x.SelectedRequest)
Expand Down Expand Up @@ -483,8 +488,8 @@ private static List<RequestLogEntry> MapToLogEntries(IEnumerable<LogEntryModel>
Timestamp = l.Request.DateTime,
Method = l.Request.Method,
Path = l.Request.Path,
RequestDefinition = AsMarkdownCode("json", JsonConvert.SerializeObject(l.Request, Formatting.Indented)),
ResponseDefinition = AsMarkdownCode("json", JsonConvert.SerializeObject(l.Response, Formatting.Indented)),
RequestDefinition = new MarkdownCode("json", JsonConvert.SerializeObject(l.Request, Formatting.Indented)),
ResponseDefinition = new MarkdownCode("json", JsonConvert.SerializeObject(l.Response, Formatting.Indented)),
StatusCode = l.Response.StatusCode?.ToString()
} )
.ToList();
Expand Down Expand Up @@ -814,24 +819,7 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod
},
NoExpectations = expectations.Request?.Params is null
},
new()
{
RuleName = "Body",
Matched = IsMatched(req, "BodyMatcher"),
ActualValue = new MarkdownActualValue
{
Value = req.Raw.Request.DetectedBodyType switch
{
"String" or "FormUrlEncoded" => WrapBodyInMarkdown(req.Raw.Request.Body?? string.Empty),
"Json" => AsMarkdownCode("json", req.Raw.Request.BodyAsJson?.ToString() ?? string.Empty),
"Bytes" => new MarkdownCode("plaintext", req.Raw.Request.BodyAsBytes?.ToString()?? string.Empty),
"File" => new MarkdownCode("plaintext","[FileContent]"),
_ => new MarkdownCode("plaintext", "")
}
},
Expectations = MapToRichExpectations(expectations.Request?.Body),
NoExpectations = expectations.Request?.Body is null
}
MapToRequestBodyViewModel(req, expectations)
},
ResponseParts = new MatchDetailsList
{
Expand Down Expand Up @@ -894,6 +882,146 @@ private static MappingDetails GetMappingDetails(RequestViewModel req, MappingMod
}
};
}

private static (string, string) ProcessDiff(JToken left, JToken right, JToken diff)
{
var leftViewBuilder = new StringBuilder();
var rightViewBuilder = new StringBuilder();

foreach (var prop in diff.Children<JProperty>())
{
string key = prop.Name;
var value = prop.Value;

if (value is JArray {Count: 2})
{
// Modification
leftViewBuilder.AppendLine($"m: {key}: {left[key]}");
rightViewBuilder.AppendLine($"m: {key}: {right[key]}");
}
else if (value is JArray {Count: 3})
{
// No Changes
leftViewBuilder.AppendLine($"{key}: {left[key]}");
rightViewBuilder.AppendLine($"{key}: {right[key]}");
}
else if (value is JArray {Count: 1})
{
// Addition or Deletion
if (left[key] == null)
{
// Addition
leftViewBuilder.AppendLine($"");
rightViewBuilder.AppendLine($"{key}: {right[key]}");
}
else
{
// Deletion
leftViewBuilder.AppendLine($"{key}: {left[key]}");
rightViewBuilder.AppendLine($"");
}
}
}

return (leftViewBuilder.ToString(), rightViewBuilder.ToString());
}

private static MatchDetailsViewModel MapToRequestBodyViewModel(RequestViewModel req, MappingModel expectations)
{
var actualValue = req.Raw.Request.DetectedBodyType switch
{
"String" or "FormUrlEncoded" => WrapBodyInMarkdown(req.Raw.Request.Body?? string.Empty),
"Json" => new MarkdownCode("json", req.Raw.Request.BodyAsJson?.ToString() ?? string.Empty),
"Bytes" => new MarkdownCode("plaintext", req.Raw.Request.BodyAsBytes?.ToString()?? string.Empty),
"File" => new MarkdownCode("plaintext","[FileContent]"),
_ => new MarkdownCode("plaintext", "")
};

var expectationsModel = MapToRichExpectations(expectations.Request?.Body);
CodeDiffViewModel diffModel = null;
if (actualValue is {lang: "json"} && expectationsModel is RichExpectations {Matchers.Count: 1} re)
{

if (re.Matchers[0].Patterns[0] is MarkdownCode {lang: "json", rawValue: var expectedValue})
{


var b = new SideBySideDiffBuilder(new Differ());
var ignoreCase = re.Matchers[0].Tags.Contains("Ignore case");
var diff = b.BuildDiffModel
(
oldText: JsonHelper.ToComparableForm(expectedValue),
newText: JsonHelper.ToComparableForm(actualValue.rawValue),
ignoreWhitespace: false,
ignoreCase: ignoreCase
);

foreach (var (a1, a2) in diff.OldText.Lines.Zip(diff.NewText.Lines))
{
if (a1.Type == ChangeType.Modified && a2.Type == ChangeType.Modified)
{
// INFO: When new line was added, we don't want to report previous line as change (',' is added at the end of line in json)
if (string.Equals(a1.Text.TrimEnd(','), a2.Text.TrimEnd(','), ignoreCase? StringComparison.OrdinalIgnoreCase: StringComparison.Ordinal))
{
a1.Type = ChangeType.Unchanged;
a2.Type = ChangeType.Unchanged;
}

}
}

diffModel = new CodeDiffViewModel
{
Left = new MarkdownCode("json", PresentDiffLinexs(diff.OldText.Lines), diff.OldText.Lines),
Right = new MarkdownCode("json", PresentDiffLinexs(diff.NewText.Lines),diff.NewText.Lines)
};
}
}

return new()
{
RuleName = "Body",
Matched = IsMatched(req, "BodyMatcher"),
ActualValue = new MarkdownActualValue
{
Value = actualValue
},
Expectations = expectationsModel,
NoExpectations = expectations.Request?.Body is null,
Diff = diffModel
};
}

private static string PresentDiffLinexs(List<DiffPiece> lines)
{
var leftBuilder = new StringBuilder();
foreach (var oldLine in lines)
{
switch (oldLine.Type)
{
case ChangeType.Unchanged:
leftBuilder.AppendLine(oldLine.Text);
break;
case ChangeType.Deleted:
leftBuilder.AppendLine(oldLine.Text);
break;
case ChangeType.Inserted:
leftBuilder.AppendLine(oldLine.Text);
break;
case ChangeType.Imaginary:
leftBuilder.AppendLine(oldLine.Text);
break;
case ChangeType.Modified:
leftBuilder.AppendLine(oldLine.Text);
break;
default:
throw new ArgumentOutOfRangeException();
}
}

var l = leftBuilder.ToString();
return l;
}

private static string FormatStatusCode(object? code)
{
Expand Down Expand Up @@ -973,7 +1101,7 @@ IEnumerable<string> GetTags(MatcherModel m)

return new RichExpectations
{
Definition = AsMarkdownCode("json", JsonConvert.SerializeObject(definition, Formatting.Indented)),
Definition = new MarkdownCode("json", JsonConvert.SerializeObject(definition, Formatting.Indented)),
Operator = matchOperator,
Matchers = matchers.Select(x => new ExpectationMatcher()
{
Expand All @@ -988,13 +1116,18 @@ IEnumerable<string> GetTags(MatcherModel m)
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(),
var v when IsJsonString(v) => (Text) new MarkdownCode("json", y).TryToReformat(),
_ => (Text)new SimpleText(y)
} ).ToList()
}).ToList()
};
}

private static bool IsJsonString(string v)
{
return v.StartsWith("{") || v.StartsWith("[");
}

private static MarkdownCode GetActualForRequestBody(RequestViewModel req)
{
return req.Raw.Response?.DetectedBodyType.ToString() switch
Expand All @@ -1009,7 +1142,7 @@ private static MarkdownCode GetActualForRequestBody(RequestViewModel req)
private static MarkdownCode WrapBodyInMarkdown(string bodyResponse)
{
var cleanBody = bodyResponse.Trim();
if (cleanBody.StartsWith("[") || cleanBody.StartsWith("{"))
if (IsJsonString(cleanBody))
{
return new MarkdownCode("json", bodyResponse);

Expand All @@ -1022,7 +1155,6 @@ private static MarkdownCode WrapBodyInMarkdown(string bodyResponse)
return new MarkdownCode("plaintext", bodyResponse);
}

public static MarkdownCode AsMarkdownCode(string lang, string code) => new MarkdownCode(lang, code);

public string RequestSearchTerm
{
Expand All @@ -1043,7 +1175,7 @@ private static ExpectationsModel ExpectationsAsJson(object? data)

return new RawExpectations()
{
Definition = AsMarkdownCode("json", JsonConvert.SerializeObject(data, Formatting.Indented))
Definition = new MarkdownCode("json", JsonConvert.SerializeObject(data, Formatting.Indented))
};
}

Expand Down

0 comments on commit 7f23ee7

Please sign in to comment.