using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Util; using JsonUtils = WireMock.Util.JsonUtils; namespace WireMock.Matchers; /// /// JsonMatcher /// public class JsonMatcher : IJsonMatcher { /// public virtual string Name => nameof(JsonMatcher); /// public object Value { get; } /// public MatchBehaviour MatchBehaviour { get; } /// public bool IgnoreCase { get; } /// /// Support Regex /// public bool Regex { get; } private readonly JToken _valueAsJToken; private readonly Func _jTokenConverter; /// /// Initializes a new instance of the class. /// /// The string value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). /// Support Regex. public JsonMatcher(string value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) { } /// /// Initializes a new instance of the class. /// /// The object value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). /// Support Regex. public JsonMatcher(object value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) { } /// /// Initializes a new instance of the class. /// /// The match behaviour. /// The value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). /// Support Regex. public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) { Guard.NotNull(value); MatchBehaviour = matchBehaviour; IgnoreCase = ignoreCase; Regex = regex; Value = value; _valueAsJToken = JsonUtils.ConvertValueToJToken(value); _jTokenConverter = ignoreCase ? Rename : jToken => jToken; } /// public MatchResult IsMatch(object? input) { var score = MatchScores.Mismatch; Exception? error = null; // When input is null or byte[], return Mismatch. if (input != null && input is not byte[]) { try { var inputAsJToken = JsonUtils.ConvertValueToJToken(input); var match = IsMatch(_jTokenConverter(_valueAsJToken), _jTokenConverter(inputAsJToken)); score = MatchScores.ToScore(match); } catch (Exception ex) { error = ex; } } return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score), error); } /// /// Compares the input against the matcher value /// /// Matcher value /// Input value /// protected virtual bool IsMatch(JToken value, JToken? input) { // If equal, return true. if (input == value) { return true; } // If input is null, return false. if (input == null) { return false; } // If using Regex and the value is a string, use the MatchRegex method. if (Regex && value.Type == JTokenType.String) { var valueAsString = value.ToString(); var (valid, result) = RegexUtils.MatchRegex(valueAsString, input.ToString()); if (valid) { return result; } } // If the value is a Guid and the input is a string, or vice versa, convert them to strings and compare the string values. if ((value.Type == JTokenType.Guid && input.Type == JTokenType.String) || (value.Type == JTokenType.String && input.Type == JTokenType.Guid)) { return JToken.DeepEquals(value.ToString().ToUpperInvariant(), input.ToString().ToUpperInvariant()); } switch (value.Type) { // If the value is an object, compare all properties. case JTokenType.Object: var valueProperties = value.ToObject>() ?? new Dictionary(); var inputProperties = input.ToObject>() ?? new Dictionary(); // If the number of properties is different, return false. if (valueProperties.Count != inputProperties.Count) { return false; } // Compare all properties. The input must match all properties of the value. foreach (var pair in valueProperties) { if (!IsMatch(pair.Value, inputProperties[pair.Key])) { return false; } } return true; // If the value is an array, compare all elements. case JTokenType.Array: var valueArray = value.ToObject() ?? EmptyArray.Value; var inputArray = input.ToObject() ?? EmptyArray.Value; // If the number of elements is different, return false. if (valueArray.Length != inputArray.Length) { return false; } return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any(); default: // Use JToken.DeepEquals() for all other types. return JToken.DeepEquals(value, input); } } private static string? ToUpper(string? input) { return input?.ToUpperInvariant(); } // https://stackoverflow.com/questions/11679804/json-net-rename-properties private static JToken Rename(JToken json) { if (json is JProperty property) { JToken propertyValue = property.Value; if (propertyValue.Type == JTokenType.String) { string stringValue = propertyValue.Value()!; propertyValue = ToUpper(stringValue); } return new JProperty(ToUpper(property.Name)!, Rename(propertyValue)); } if (json is JArray array) { var renamedValues = array.Select(Rename); return new JArray(renamedValues); } if (json is JObject obj) { var renamedProperties = obj.Properties().Select(Rename); return new JObject(renamedProperties); } return json; } }