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

GraphQL - custom scalar support #1012

Merged
merged 9 commits into from
Dec 4, 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
7 changes: 6 additions & 1 deletion examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public class Todo
public static class MainApp
{
private const string TestSchema = @"
scalar DateTime
scalar MyCustomScalar

input MessageInput {
content: String
author: String
Expand All @@ -56,6 +59,7 @@ type Message {

type Mutation {
createMessage(input: MessageInput): Message
createAnotherMessage(x: MyCustomScalar, dt: DateTime): Message
updateMessage(id: ID!, input: MessageInput): Message
}

Expand Down Expand Up @@ -168,11 +172,12 @@ public static void Run()

// server.AllowPartialMapping();
#if GRAPHQL
var customScalars = new Dictionary<string, Type> { { "MyCustomScalar", typeof(int) } };
server
.Given(Request.Create()
.WithPath("/graphql")
.UsingPost()
.WithGraphQLSchema(TestSchema)
.WithGraphQLSchema(TestSchema, customScalars)
)
.RespondWith(Response.Create()
.WithBody("GraphQL is ok")
Expand Down
13 changes: 13 additions & 0 deletions src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Collections.Generic;
using System;

namespace WireMock.Admin.Mappings;

/// <summary>
Expand Down Expand Up @@ -75,6 +78,16 @@ public class MatcherModel
#endregion

#region XPathMatcher
/// <summary>
/// Array of namespace prefix and uri. (optional)
/// </summary>
public XmlNamespace[]? XmlNamespaceMap { get; set; }
#endregion

#region GraphQLMatcher
/// <summary>
/// Mapping of custom GraphQL Scalar name to ClrType. (optional)
/// </summary>
public IDictionary<string, Type>? CustomScalars { get; set; }
#endregion
}
65 changes: 60 additions & 5 deletions src/WireMock.Net/Matchers/GraphQLMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
using AnyOfTypes;
using GraphQL;
using GraphQL.Types;
using GraphQLParser;
using GraphQLParser.AST;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Exceptions;
using WireMock.Extensions;
using WireMock.Matchers.Models;
using WireMock.Models;
using WireMock.Util;

namespace WireMock.Matchers;

Expand All @@ -36,14 +41,40 @@ private sealed class GraphQLRequest
public MatchBehaviour MatchBehaviour { get; }

/// <summary>
/// Initializes a new instance of the <see cref="LinqMatcher"/> class.
/// An optional dictionary defining the custom Scalar and the type.
/// </summary>
public IDictionary<string, Type>? CustomScalars { get; }

/// <summary>
/// Initializes a new instance of the <see cref="GraphQLMatcher"/> class.
/// </summary>
/// <param name="schema">The schema.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</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,
MatchOperator matchOperator = MatchOperator.Or
) : this(schema, null, matchBehaviour, matchOperator)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="GraphQLMatcher"/> class.
/// </summary>
/// <param name="schema">The schema.</param>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</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, MatchOperator matchOperator = MatchOperator.Or)
public GraphQLMatcher(
AnyOf<string, StringPattern, ISchema> schema,
IDictionary<string, Type>? customScalars,
MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch,
MatchOperator matchOperator = MatchOperator.Or
)
{
Guard.NotNull(schema);
CustomScalars = customScalars;
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;

Expand Down Expand Up @@ -137,12 +168,36 @@ private static bool TryGetGraphQLRequest(string input, [NotNullWhen(true)] out G
}
}

private static ISchema BuildSchema(string typeDefinitions)
/// <param name="typeDefinitions">A textual description of the schema in SDL (Schema Definition Language) format.</param>
private ISchema BuildSchema(string typeDefinitions)
{
var schema = Schema.For(typeDefinitions);

// #984
schema.RegisterTypes(schema.BuiltInTypeMappings.Select(x => x.graphType).ToArray());
var graphTypes = schema.BuiltInTypeMappings.Select(tm => tm.graphType).ToArray();
schema.RegisterTypes(graphTypes);

var doc = Parser.Parse(typeDefinitions);
var scalarTypeDefinitions = doc.Definitions
.Where(d => d.Kind == ASTNodeKind.ScalarTypeDefinition)
.OfType<GraphQLTypeDefinition>();

foreach (var scalarTypeDefinitionName in scalarTypeDefinitions.Select(s => s.Name.StringValue))
{
var customScalarGraphTypeName = $"{scalarTypeDefinitionName}GraphType";
if (graphTypes.All(t => t.Name != customScalarGraphTypeName)) // Only process when not built-in.
{
// Check if this custom Scalar is defined in the dictionary
if (CustomScalars == null || !CustomScalars.TryGetValue(scalarTypeDefinitionName, out var clrType))
{
throw new WireMockException($"The GraphQL Scalar type '{scalarTypeDefinitionName}' is not defined in the CustomScalars dictionary.");
}

// Create a this custom Scalar GraphType (extending the WireMockCustomScalarGraphType<{clrType}> class)
var customScalarGraphType = ReflectionUtils.CreateGenericType(customScalarGraphTypeName, typeof(WireMockCustomScalarGraphType<>), clrType);
schema.RegisterType(customScalarGraphType);
}
}

return schema;
}
Expand Down
30 changes: 30 additions & 0 deletions src/WireMock.Net/Matchers/Models/WireMockCustomScalarGraphType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#if GRAPHQL
using System;
using GraphQL.Types;

namespace WireMock.Matchers.Models;

/// <inheritdoc />
public abstract class WireMockCustomScalarGraphType<T> : ScalarGraphType
{
/// <inheritdoc />
public override object? ParseValue(object? value)
{
switch (value)
{
case null:
return null;

case T:
return value;
}

if (value is string && typeof(T) != typeof(string))
{
throw new InvalidCastException($"Unable to convert value '{value}' of type '{typeof(string)}' to type '{typeof(T)}'.");
}

return (T)Convert.ChangeType(value, typeof(T));
}
}
#endif
22 changes: 15 additions & 7 deletions src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
Expand Down Expand Up @@ -25,8 +26,9 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="schema">The schema.</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema) :
this(CreateMatcherArray(matchBehaviour, schema))
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema, IDictionary<string, Type>? customScalars = null) :
this(CreateMatcherArray(matchBehaviour, schema, customScalars))
{
}

Expand All @@ -36,8 +38,9 @@ public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema
/// </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)))
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars = null) :
this(CreateMatcherArray(matchBehaviour, new AnyOfTypes.AnyOf<string, WireMock.Models.StringPattern, GraphQL.Types.ISchema>(schema), customScalars))
{
}
#endif
Expand Down Expand Up @@ -89,12 +92,17 @@ private IReadOnlyList<MatchResult> CalculateMatchResults(IRequestMessage request
}

#if GRAPHQL
private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, AnyOfTypes.AnyOf<string, Models.StringPattern, GraphQL.Types.ISchema> schema)
private static IMatcher[] CreateMatcherArray(
MatchBehaviour matchBehaviour,
AnyOfTypes.AnyOf<string, WireMock.Models.StringPattern,
GraphQL.Types.ISchema> schema,
IDictionary<string, Type>? customScalars
)
{
return new[] { new GraphQLMatcher(schema, matchBehaviour) }.Cast<IMatcher>().ToArray();
return new[] { new GraphQLMatcher(schema, customScalars, matchBehaviour) }.Cast<IMatcher>().ToArray();
}
#else
private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, object schema)
private static IMatcher[] CreateMatcherArray(MatchBehaviour matchBehaviour, object schema, IDictionary<string, Type>? customScalars)
{
throw new System.NotSupportedException("The GrapQLMatcher can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
}
Expand Down
20 changes: 20 additions & 0 deletions src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using WireMock.Matchers;

namespace WireMock.RequestBuilders;
Expand All @@ -15,6 +17,15 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);

/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);

#if GRAPHQL
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a ISchema.
Expand All @@ -23,5 +34,14 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);

/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
#endif
}
16 changes: 16 additions & 0 deletions src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System;
using WireMock.Matchers;
using WireMock.Matchers.Request;

Expand All @@ -12,12 +14,26 @@ public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBeha
return this;
}

/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
return this;
}

#if GRAPHQL
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
return this;
}

/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
return this;
}
#endif
}
10 changes: 7 additions & 3 deletions src/WireMock.Net/Serialization/MatcherMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public MatcherMapper(WireMockServerSettings settings)
return CreateExactObjectMatcher(matchBehaviour, stringPatterns[0]);
#if GRAPHQL
case nameof(GraphQLMatcher):
return new GraphQLMatcher(stringPatterns[0].GetPattern(), matchBehaviour, matchOperator);
return new GraphQLMatcher(stringPatterns[0].GetPattern(), matcher.CustomScalars, matchBehaviour, matchOperator);
#endif

#if MIMEKIT
Expand Down Expand Up @@ -101,8 +101,7 @@ public MatcherMapper(WireMockServerSettings settings)
return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns);

case nameof(XPathMatcher):
var xmlNamespaces = matcher.XmlNamespaceMap;
return new XPathMatcher(matchBehaviour, matchOperator, xmlNamespaces, stringPatterns);
return new XPathMatcher(matchBehaviour, matchOperator, matcher.XmlNamespaceMap, stringPatterns);

case nameof(WildcardMatcher):
return new WildcardMatcher(matchBehaviour, stringPatterns, ignoreCase, matchOperator);
Expand Down Expand Up @@ -164,6 +163,11 @@ public MatcherMapper(WireMockServerSettings settings)
case XPathMatcher xpathMatcher:
model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
break;
#if GRAPHQL
case GraphQLMatcher graphQLMatcher:
model.CustomScalars = graphQLMatcher.CustomScalars;
break;
#endif
}

switch (matcher)
Expand Down
48 changes: 48 additions & 0 deletions src/WireMock.Net/Util/ReflectionUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace WireMock.Util;

internal static class ReflectionUtils
{
private const string DynamicModuleName = "WireMockDynamicModule";
private static readonly AssemblyName AssemblyName = new("WireMockDynamicAssembly");
private const TypeAttributes ClassAttributes =
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout;
private static readonly ConcurrentDictionary<string, Type> TypeCache = new();

public static Type CreateType(string typeName, Type? parentType = null)
{
return TypeCache.GetOrAdd(typeName, key =>
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(DynamicModuleName);

var typeBuilder = moduleBuilder.DefineType(key, ClassAttributes, parentType);

// Create the type and cache it
return typeBuilder.CreateTypeInfo()!.AsType();
});
}

public static Type CreateGenericType(string typeName, Type genericTypeDefinition, params Type[] typeArguments)
{
var genericKey = $"{typeName}_{genericTypeDefinition.Name}_{string.Join(", ", typeArguments.Select(t => t.Name))}";

return TypeCache.GetOrAdd(genericKey, _ =>
{
var genericType = genericTypeDefinition.MakeGenericType(typeArguments);

// Create the type based on the genericType and cache it
return CreateType(typeName, genericType);
});
}
}
Loading
Loading