mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-03-21 00:50:24 +01:00
GraphQL - custom scalar support (#1012)
* x * CustomScalars * more tests * . * add or set * ... * x
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace WireMock.Admin.Mappings;
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,10 +6,15 @@ using System.Linq;
|
||||
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;
|
||||
|
||||
@@ -36,14 +41,40 @@ public class GraphQLMatcher : IStringMatcher
|
||||
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.</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,
|
||||
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="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,
|
||||
IDictionary<string, Type>? customScalars,
|
||||
MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch,
|
||||
MatchOperator matchOperator = MatchOperator.Or
|
||||
)
|
||||
{
|
||||
Guard.NotNull(schema);
|
||||
CustomScalars = customScalars;
|
||||
MatchBehaviour = matchBehaviour;
|
||||
MatchOperator = matchOperator;
|
||||
|
||||
@@ -137,12 +168,36 @@ public class GraphQLMatcher : IStringMatcher
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Stef.Validation;
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,8 +38,9 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
|
||||
/// </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
|
||||
@@ -89,12 +92,17 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
|
||||
}
|
||||
|
||||
#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.");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using WireMock.Matchers;
|
||||
|
||||
namespace WireMock.RequestBuilders;
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
|
||||
@@ -12,6 +14,13 @@ public partial class Request
|
||||
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)
|
||||
@@ -19,5 +28,12 @@ public partial class Request
|
||||
_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
|
||||
}
|
||||
@@ -72,7 +72,7 @@ internal class MatcherMapper
|
||||
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
|
||||
@@ -101,8 +101,7 @@ internal class MatcherMapper
|
||||
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);
|
||||
@@ -164,6 +163,11 @@ internal class MatcherMapper
|
||||
case XPathMatcher xpathMatcher:
|
||||
model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
|
||||
break;
|
||||
#if GRAPHQL
|
||||
case GraphQLMatcher graphQLMatcher:
|
||||
model.CustomScalars = graphQLMatcher.CustomScalars;
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (matcher)
|
||||
|
||||
48
src/WireMock.Net/Util/ReflectionUtils.cs
Normal file
48
src/WireMock.Net/Util/ReflectionUtils.cs
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user