Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GraphQL Schema matching #964

Merged
merged 18 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions WireMock.Net Solution.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PATCH/@EntryIndexedValue">PATCH</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=POST/@EntryIndexedValue">POST</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PUT/@EntryIndexedValue">PUT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QL/@EntryIndexedValue">QL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RSA/@EntryIndexedValue">RSA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SSL/@EntryIndexedValue">SSL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TE/@EntryIndexedValue">TE</s:String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@

<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
<!--<PackageReference Include="Handlebars.Net.Helpers" Version="2.*" />-->
<PackageReference Include="log4net" Version="2.0.15" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
40 changes: 40 additions & 0 deletions examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ public class Todo

public static class MainApp
{
private const string TestSchema = @"
input MessageInput {
content: String
author: String
}

type Message {
id: ID!
content: String
author: String
}

type Mutation {
createMessage(input: MessageInput): Message
updateMessage(id: ID!, input: MessageInput): Message
}

type Query {
greeting:String
students:[Student]
studentById(id:ID!):Student
}

type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}";

public static void Run()
{
var mappingBuilder = new MappingBuilder();
Expand Down Expand Up @@ -137,6 +167,16 @@ public static void Run()

// server.AllowPartialMapping();

server
.Given(Request.Create()
.WithPath("/graphql")
.UsingPost()
.WithGraphQLSchema(TestSchema)
)
.RespondWith(Response.Create()
.WithBody("GraphQL is ok")
);

// 400 ms
server
.Given(Request.Create()
Expand Down
139 changes: 139 additions & 0 deletions src/WireMock.Net/Matchers/GraphQLMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#if GRAPHQL
using System;
using System.Collections.Generic;
using System.Linq;
using AnyOfTypes;
using GraphQL;
using GraphQL.Types;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Models;

namespace WireMock.Matchers;

/// <summary>
/// GrapQLMatcher Schema Matcher
/// </summary>
/// <inheritdoc cref="IStringMatcher"/>
public class GraphQLMatcher : IStringMatcher
{
private sealed class GraphQLRequest
{
public string? Query { get; set; }

public Dictionary<string, object?>? Variables { get; set; }
}

private readonly AnyOf<string, StringPattern>[] _patterns;

private readonly ISchema _schema;

/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }

/// <inheritdoc />
public bool ThrowException { get; }

/// <summary>
/// Initializes a new instance of the <see cref="LinqMatcher"/> class.
/// </summary>
/// <param name="schema">The schema.</param>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="throwException">Throw an exception when the internal matching fails because of invalid input.</param>
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
public GraphQLMatcher(AnyOf<string, StringPattern, ISchema> schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, bool throwException = false, MatchOperator matchOperator = MatchOperator.Or)
{
Guard.NotNull(schema);
MatchBehaviour = matchBehaviour;
ThrowException = throwException;
MatchOperator = matchOperator;

var patterns = new List<AnyOf<string, StringPattern>>();
switch (schema.CurrentType)
{
case AnyOfType.First:
patterns.Add(schema.First);
_schema = BuildSchema(schema);
break;

case AnyOfType.Second:
patterns.Add(schema.Second);
_schema = BuildSchema(schema.Second.Pattern);
break;

case AnyOfType.Third:
_schema = schema.Third;
break;

default:
throw new NotSupportedException();
}
_patterns = patterns.ToArray();
}

/// <inheritdoc />
public double IsMatch(string? input)
{
var match = MatchScores.Mismatch;

try
{
var graphQLRequest = JsonConvert.DeserializeObject<GraphQLRequest>(input!)!;

var executionResult = new DocumentExecuter().ExecuteAsync(_ =>
{
_.ThrowOnUnhandledException = true;

_.Schema = _schema;
_.Query = graphQLRequest.Query;

if (graphQLRequest.Variables != null)
{
_.Variables = new Inputs(graphQLRequest.Variables);
}
}).GetAwaiter().GetResult();

if (executionResult.Errors == null || executionResult.Errors.Count == 0)
{
match = MatchScores.Perfect;
}
else
{
var exceptions = executionResult.Errors.OfType<Exception>().ToArray();
if (exceptions.Length == 1)
{
throw exceptions[0];
}

throw new AggregateException(exceptions);
}
}
catch
{
if (ThrowException)
{
throw;
}
}

return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}

/// <inheritdoc />
public AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}

/// <inheritdoc />
public MatchOperator MatchOperator { get; }

/// <inheritdoc cref="IMatcher.Name"/>
public string Name => nameof(GraphQLMatcher);

private static ISchema BuildSchema(string schema)
{
return Schema.For(schema);
}
}
#endif
36 changes: 5 additions & 31 deletions src/WireMock.Net/Matchers/LinqMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public LinqMatcher(
MatchOperator = matchOperator;
}

/// <inheritdoc cref="IStringMatcher.IsMatch"/>
/// <inheritdoc />
public double IsMatch(string? input)
{
double match = MatchScores.Mismatch;
Expand All @@ -95,7 +95,7 @@ public double IsMatch(string? input)
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}

/// <inheritdoc cref="IObjectMatcher.IsMatch"/>
/// <inheritdoc />
public double IsMatch(object? input)
{
double match = MatchScores.Mismatch;
Expand All @@ -110,41 +110,15 @@ public double IsMatch(object? input)
jArray = new JArray { JToken.FromObject(input) };
}

//enumerable = jArray.ToDynamicClassArray();

//JObject value;
//switch (input)
//{
// case JObject valueAsJObject:
// value = valueAsJObject;
// break;

// case { } valueAsObject:
// value = JObject.FromObject(valueAsObject);
// break;

// default:
// return MatchScores.Mismatch;
//}

// Convert a single object to a Queryable JObject-list with 1 entry.
//var queryable1 = new[] { value }.AsQueryable();
var queryable = jArray.ToDynamicClassArray().AsQueryable();

try
{
// Generate the DynamicLinq select statement.
//string dynamicSelect = JsonUtils.GenerateDynamicLinqStatement(value);

// Execute DynamicLinq Select statement.
//var queryable2 = queryable1.Select(dynamicSelect);

// Use the Any(...) method to check if the result matches.

var patternsAsStringArray = _patterns.Select(p => p.GetPattern()).ToArray();
var scores = patternsAsStringArray.Select(p => queryable.Any(p)).ToArray();

match = MatchScores.ToScore(_patterns.Select(pattern => queryable.Any(pattern.GetPattern())).ToArray(), MatchOperator);
match = MatchScores.ToScore(scores, MatchOperator);

return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}
Expand All @@ -159,7 +133,7 @@ public double IsMatch(object? input)
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}

/// <inheritdoc cref="IStringMatcher.GetPatterns"/>
/// <inheritdoc />
public AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
Expand All @@ -168,6 +142,6 @@ public AnyOf<string, StringPattern>[] GetPatterns()
/// <inheritdoc />
public MatchOperator MatchOperator { get; }

/// <inheritdoc cref="IMatcher.Name"/>
/// <inheritdoc />
public string Name => "LinqMatcher";
}
104 changes: 104 additions & 0 deletions src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Linq;
using Stef.Validation;
using WireMock.Types;

namespace WireMock.Matchers.Request;

/// <summary>
/// The request body GraphQL matcher.
/// </summary>
public class RequestMessageGraphQLMatcher : IRequestMatcher
{
/// <summary>
/// The matchers.
/// </summary>
public IMatcher[]? Matchers { get; }

/// <summary>
/// The <see cref="MatchOperator"/>
/// </summary>
public MatchOperator MatchOperator { get; } = MatchOperator.Or;

/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageGraphQLMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="schema">The schema.</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema) :
this(CreateMatcherArray(matchBehaviour, schema))
{
}

#if GRAPHQL
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageGraphQLMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="schema">The schema.</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, GraphQL.Types.ISchema schema) :
this(CreateMatcherArray(matchBehaviour, new AnyOfTypes.AnyOf<string, Models.StringPattern, GraphQL.Types.ISchema>(schema)))
{
}
#endif
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageGraphQLMatcher"/> class.
/// </summary>
/// <param name="matchers">The matchers.</param>
public RequestMessageGraphQLMatcher(params IMatcher[] matchers)
{
Matchers = Guard.NotNull(matchers);
}

/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageGraphQLMatcher"/> class.
/// </summary>
/// <param name="matchers">The matchers.</param>
/// <param name="matchOperator">The <see cref="MatchOperator"/> to use.</param>
public RequestMessageGraphQLMatcher(MatchOperator matchOperator, params IMatcher[] matchers)
{
Matchers = Guard.NotNull(matchers);
MatchOperator = matchOperator;
}

/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
var score = CalculateMatchScore(requestMessage);
return requestMatchResult.AddScore(GetType(), score);
}

private static double CalculateMatchScore(IRequestMessage requestMessage, IMatcher matcher)
{
// Check if the matcher is a IStringMatcher
// If the body is a Json or a String, use the BodyAsString to match on.
if (matcher is IStringMatcher stringMatcher && requestMessage.BodyData?.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded)
{
return stringMatcher.IsMatch(requestMessage.BodyData.BodyAsString);
}

return MatchScores.Mismatch;
}

private double CalculateMatchScore(IRequestMessage requestMessage)
{
if (Matchers == null)
{
return MatchScores.Mismatch;
}

var matchersResult = Matchers.Select(matcher => CalculateMatchScore(requestMessage, matcher)).ToArray();
return MatchScores.ToScore(matchersResult, MatchOperator);
}

#if GRAPHQL
private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, AnyOfTypes.AnyOf<string, Models.StringPattern, GraphQL.Types.ISchema> schema)
{
return new[] { new GraphQLMatcher(schema, matchBehaviour) }.Cast<IMatcher>().ToArray();
}
#else
private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, object schema)
{
throw new System.NotSupportedException("The GrapQLMatcher can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
}
#endif
}
Loading