Skip to content
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
32 changes: 28 additions & 4 deletions LinQL.Tests/Translation/TranslationProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class TranslationProviderTests
{
[Fact]
public void ScalarOnRoot()
=> this.Test<ScarlarOnRootType, object>(x => x.GetNumber());
=> this.Test<ScalarOnRootType, object>(x => x.GetNumber());

[Fact]
public void Simple()
Expand Down Expand Up @@ -316,9 +316,9 @@ private class RenamedType : RootType<RenamedType>

private interface ISimpleType
{
string? Text { get; }
public string? Text { get; }

int Number { get; }
public int Number { get; }
}

private class SomeOtherSimpleType : ISimpleType
Expand All @@ -345,11 +345,35 @@ private class InterfaceRootType : RootType<InterfaceRootType>
}

[OperationType(RootOperationType.Query)]
public class ScarlarOnRootType : RootType<ScarlarOnRootType>
public class ScalarOnRootType : RootType<ScalarOnRootType>
{
public int Number { get; set; }

[GraphQLOperation, GraphQLField(Name = "number")]
public int GetNumber() => this.Number;
}

[OperationType(RootOperationType.Query)]
public class ScalarArray : RootType<ScalarArray>
{
public required string[] Strings { get; set; }

[GraphQLOperation, GraphQLField(Name = "numbers")]
public int[] GetNumbers() => [];

[GraphQLOperation, GraphQLField(Name = "filteredNumbers")]
public int[] GetFilteredNumbers([GraphQLArgument(GQLType = "Int!")] int number) => [number];
}

[Fact]
public void ScalarArrayFields()
=> this.Test<ScalarArray, string[]>(x => x.Strings);

[Fact]
public void ScalarArrayOperation()
=> this.Test<ScalarArray, int[]>(x => x.GetNumbers());

[Fact]
public void ScalarArrayOperationWithArguments()
=> this.Test<ScalarArray, int[]>(x => x.GetFilteredNumbers(1));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query {
strings
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query {
numbers
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query linql(
$var1: Int!
)
{
filteredNumbers(
number: $var1
)
}
17 changes: 14 additions & 3 deletions LinQL/Expressions/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,22 @@ public static bool IsScalar(this Type type, IEnumerable<Scalar> scalars)
return type.IsEnum || scalars.Any(s => s.RuntimeType == type.FullName || ((type.IsPrimitive || type.Equals(typeof(string))) && s.OriginalPrimitive == type.FullName));
}

public static bool IsArrayOfScalars(this Type type, IEnumerable<Scalar> scalars)
{
if (!type.IsArray)
{
return false;
}

return type.GetElementType()?.IsScalar(scalars) ?? false;
}

public static FieldExpression ToField(this MemberInfo member, IRootExpression root) => member switch
{
PropertyInfo prop when prop.PropertyType.IsScalar(root.Scalars) => new ScalarFieldExpression(member.GetFieldName(), prop.PropertyType, member.DeclaringType!),
FieldInfo field when field.FieldType.IsScalar(root.Scalars) => new ScalarFieldExpression(member.GetFieldName(), field.FieldType, member.DeclaringType!),
MethodInfo method when method.ReturnType.IsScalar(root.Scalars) && !method.GetParameters().Any() => new ScalarFieldExpression(method.GetFieldName(), method.ReturnType, member.DeclaringType!),
PropertyInfo prop when prop.PropertyType.IsScalar(root.Scalars) || prop.PropertyType.IsArrayOfScalars(root.Scalars) => new ScalarFieldExpression(member.GetFieldName(), prop.PropertyType, member.DeclaringType!, root),
FieldInfo field when field.FieldType.IsScalar(root.Scalars) || field.FieldType.IsArrayOfScalars(root.Scalars) => new ScalarFieldExpression(member.GetFieldName(), field.FieldType, member.DeclaringType!, root),
MethodInfo method when (method.ReturnType.IsArrayOfScalars(root.Scalars) || method.ReturnType.IsScalar(root.Scalars)) && !method.GetParameters().Any() => new ScalarFieldExpression(method.GetFieldName(), method.ReturnType, member.DeclaringType!, root),
MethodInfo method when (method.ReturnType.IsArrayOfScalars(root.Scalars) || method.ReturnType.IsScalar(root.Scalars)) && method.IsOperation() => new ScalarFieldExpression(member.GetFieldName(), method.ReturnType, member.DeclaringType!, root),
MethodInfo method when method.IsOperation() => new TypeFieldExpression(member.GetFieldName(), method.ReturnType, member.DeclaringType!, root),
PropertyInfo prop => new TypeFieldExpression(prop.GetFieldName(), prop.PropertyType, member.DeclaringType!, root),
FieldInfo field => new TypeFieldExpression(field.GetFieldName(), field.FieldType, member.DeclaringType!, root),
Expand Down
36 changes: 34 additions & 2 deletions LinQL/Expressions/FieldExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ namespace LinQL.Expressions;
/// </summary>
public abstract class FieldExpression : Expression
{
private Dictionary<string, string> arguments = [];

/// <param name="field">The name of the field.</param>
/// <param name="fieldType">The .Net return type of the field.</param>
/// <param name="declaringType">The .Net type that field is a member of.</param>
protected FieldExpression(string field, Type fieldType, Type declaringType)
=> (this.FieldName, this.Type, this.DeclaringType) = (field, fieldType, declaringType);
/// <param name="root">The root operation expression.</param>
protected FieldExpression(string field, Type fieldType, Type declaringType, IRootExpression? root)
=> (this.FieldName, this.Type, this.DeclaringType, this.Root) = (field, fieldType, declaringType, root ?? (this as IRootExpression)!);

/// <summary>
/// Gets the GraphQL field name.
Expand All @@ -32,4 +35,33 @@ protected FieldExpression(string field, Type fieldType, Type declaringType)
/// Gets the type the field is a member of.
/// </summary>
public Type DeclaringType { get; }

/// <summary>
/// Gets the arguments to be passed to the field on the server.
/// </summary>
public IReadOnlyDictionary<string, string> Arguments
{
get => this.arguments;
protected set => this.arguments = value.ToDictionary(x => x.Key, x => x.Value);
}

/// <summary>
/// Gets the root expression
/// </summary>
public IRootExpression Root { get; }

/// <summary>
/// Add an argument to the field.
/// </summary>
/// <param name="name">The name of the argument.</param>
/// <param name="type">The graphql type.</param>
/// <param name="value">The name of the variable that will hold the argument value.</param>
/// <returns>The updated <see cref="FieldExpression"/>.</returns>
public FieldExpression WithArgument(string name, string type, object? value)
{
var variable = this.Root.WithVariable(type, value);

this.arguments.Add(name, variable.Name);
return this;
}
}
4 changes: 3 additions & 1 deletion LinQL/Expressions/ScalarFieldExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace LinQL.Expressions;
/// <param name="field">The name of the field.</param>
/// <param name="fieldType">The return type of the field.</param>
/// <param name="declaringType">The .Net type that field is a member of.</param>
public class ScalarFieldExpression(string field, Type fieldType, Type declaringType) : FieldExpression(field, fieldType, declaringType)
/// <param name="root"></param>
public class ScalarFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression root)
: FieldExpression(field, fieldType, declaringType, root)
{
}
54 changes: 10 additions & 44 deletions LinQL/Expressions/TypeFieldExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,27 @@ namespace LinQL.Expressions;
/// <summary>
/// An expression that defines access to a field that can have child fields.
/// </summary>
public class TypeFieldExpression : FieldExpression
/// <remarks>
/// Create a new <see cref="TypeFieldExpression"/>.
/// </remarks>
/// <param name="field">The field name.</param>
/// <param name="fieldType">The return type of the field.</param>
/// <param name="declaringType">The .Net type that field is a member of.</param>
/// <param name="root"></param>
public class TypeFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression? root)
: FieldExpression(field, fieldType, declaringType, root)
{
private Dictionary<string, Expression> fields = [];
private Dictionary<string, string> arguments = [];

/// <summary>
/// Create a new <see cref="TypeFieldExpression"/>.
/// </summary>
/// <param name="field">The field name.</param>
/// <param name="fieldType">The return type of the field.</param>
/// <param name="declaringType">The .Net type that field is a member of.</param>
/// <param name="root"></param>
public TypeFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression? root)
: base(field, fieldType, declaringType) => this.Root = root ?? (this as IRootExpression)!;

/// <inheritdoc />
protected TypeFieldExpression(string field, Type fieldType, Type declaringType)
: this(field, fieldType, declaringType, null)
{
}

/// <summary>
/// Gets the arguments to be passed to the field on the server.
/// </summary>
public IReadOnlyDictionary<string, string> Arguments => this.arguments;

/// <summary>
/// Gets the root expression
/// </summary>
public IRootExpression Root { get; }

/// <inheritdoc/>
public FieldExpression WithField(FieldExpression field)
=> this.fields.GetOrAdd(field.FieldName, () => field);

/// <summary>
/// Add an argument to the field.
/// </summary>
/// <param name="name">The name of the argument.</param>
/// <param name="type">The graphql type.</param>
/// <param name="value">The name of the variable that will hold the argument value.</param>
/// <returns>The updated <see cref="TypeFieldExpression"/>.</returns>
public TypeFieldExpression WithArgument(string name, string type, object? value)
{
var variable = this.Root.WithVariable(type, value);

this.arguments.Add(name, variable.Name);
return this;
}

internal TypeFieldExpression ReplaceType(Type type)
=> new(this.FieldName, type, this.DeclaringType, this.Root)
{
fields = this.fields,
arguments = this.arguments,
Arguments = this.Arguments,
};

/// <inheritdoc />
Expand Down
4 changes: 2 additions & 2 deletions LinQL/Translation/ExpressionTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ private TypeFieldExpression TraverseChainedOn(MethodCallExpression member)
return parent;
}

private static TypeFieldExpression VisitFieldWithArguments(MethodCallExpression node, IRootExpression root)
private static FieldExpression VisitFieldWithArguments(MethodCallExpression node, IRootExpression root)
{
var field = (TypeFieldExpression)node.Method.ToField(root);
var field = node.Method.ToField(root);

return node.Method.GetParameters()
.Zip(node.Arguments, (p, i) => (
Expand Down
22 changes: 21 additions & 1 deletion LinQL/Translation/GraphQLExpressionTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,27 @@ private Expression VisitField(TypeFieldExpression field)

private Expression VisitScalar(ScalarFieldExpression scalar)
{
this.query.AppendLine(scalar.FieldName.ToCamelCase());
if (scalar.Arguments.Any())
{
this.query.AppendLine($"{scalar.FieldName.ToCamelCase()}(");
using (this.query.Indent())
{
var last = scalar.Arguments.Last();

foreach (var argument in scalar.Arguments.Take(scalar.Arguments.Count - 1))
{
this.query.AppendLine($"{argument.Key}: ${argument.Value},");
}

this.query.AppendLine($"{last.Key}: ${last.Value}");
}

this.query.AppendLine(")");
}
else
{
this.query.AppendLine(scalar.FieldName.ToCamelCase());
}

return scalar;
}
Expand Down
Loading