diff --git a/LinQL.Tests/Translation/TranslationProviderTests.cs b/LinQL.Tests/Translation/TranslationProviderTests.cs index cdd0911..6fef33e 100644 --- a/LinQL.Tests/Translation/TranslationProviderTests.cs +++ b/LinQL.Tests/Translation/TranslationProviderTests.cs @@ -13,7 +13,7 @@ public class TranslationProviderTests { [Fact] public void ScalarOnRoot() - => this.Test(x => x.GetNumber()); + => this.Test(x => x.GetNumber()); [Fact] public void Simple() @@ -316,9 +316,9 @@ private class RenamedType : RootType private interface ISimpleType { - string? Text { get; } + public string? Text { get; } - int Number { get; } + public int Number { get; } } private class SomeOtherSimpleType : ISimpleType @@ -345,11 +345,35 @@ private class InterfaceRootType : RootType } [OperationType(RootOperationType.Query)] - public class ScarlarOnRootType : RootType + public class ScalarOnRootType : RootType { public int Number { get; set; } [GraphQLOperation, GraphQLField(Name = "number")] public int GetNumber() => this.Number; } + + [OperationType(RootOperationType.Query)] + public class ScalarArray : RootType + { + 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(x => x.Strings); + + [Fact] + public void ScalarArrayOperation() + => this.Test(x => x.GetNumbers()); + + [Fact] + public void ScalarArrayOperationWithArguments() + => this.Test(x => x.GetFilteredNumbers(1)); } diff --git a/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayFields.snap b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayFields.snap new file mode 100644 index 0000000..f3ad6a4 --- /dev/null +++ b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayFields.snap @@ -0,0 +1,3 @@ +query { + strings +} diff --git a/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperation.snap b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperation.snap new file mode 100644 index 0000000..3aab8c6 --- /dev/null +++ b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperation.snap @@ -0,0 +1,3 @@ +query { + numbers +} diff --git a/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperationWithArguments.snap b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperationWithArguments.snap new file mode 100644 index 0000000..a689983 --- /dev/null +++ b/LinQL.Tests/Translation/__snapshots__/TranslationProviderTests.ScalarArrayOperationWithArguments.snap @@ -0,0 +1,8 @@ +query linql( + $var1: Int! +) + { + filteredNumbers( + number: $var1 + ) +} diff --git a/LinQL/Expressions/Extensions.cs b/LinQL/Expressions/Extensions.cs index dbd3322..b45d885 100644 --- a/LinQL/Expressions/Extensions.cs +++ b/LinQL/Expressions/Extensions.cs @@ -32,11 +32,22 @@ public static bool IsScalar(this Type type, IEnumerable 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 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), diff --git a/LinQL/Expressions/FieldExpression.cs b/LinQL/Expressions/FieldExpression.cs index 0f9315b..453dd29 100644 --- a/LinQL/Expressions/FieldExpression.cs +++ b/LinQL/Expressions/FieldExpression.cs @@ -7,11 +7,14 @@ namespace LinQL.Expressions; /// public abstract class FieldExpression : Expression { + private Dictionary arguments = []; + /// The name of the field. /// The .Net return type of the field. /// The .Net type that field is a member of. - protected FieldExpression(string field, Type fieldType, Type declaringType) - => (this.FieldName, this.Type, this.DeclaringType) = (field, fieldType, declaringType); + /// The root operation expression. + 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)!); /// /// Gets the GraphQL field name. @@ -32,4 +35,33 @@ protected FieldExpression(string field, Type fieldType, Type declaringType) /// Gets the type the field is a member of. /// public Type DeclaringType { get; } + + /// + /// Gets the arguments to be passed to the field on the server. + /// + public IReadOnlyDictionary Arguments + { + get => this.arguments; + protected set => this.arguments = value.ToDictionary(x => x.Key, x => x.Value); + } + + /// + /// Gets the root expression + /// + public IRootExpression Root { get; } + + /// + /// Add an argument to the field. + /// + /// The name of the argument. + /// The graphql type. + /// The name of the variable that will hold the argument value. + /// The updated . + public FieldExpression WithArgument(string name, string type, object? value) + { + var variable = this.Root.WithVariable(type, value); + + this.arguments.Add(name, variable.Name); + return this; + } } diff --git a/LinQL/Expressions/ScalarFieldExpression.cs b/LinQL/Expressions/ScalarFieldExpression.cs index 55ed7c0..64a5023 100644 --- a/LinQL/Expressions/ScalarFieldExpression.cs +++ b/LinQL/Expressions/ScalarFieldExpression.cs @@ -9,6 +9,8 @@ namespace LinQL.Expressions; /// The name of the field. /// The return type of the field. /// The .Net type that field is a member of. -public class ScalarFieldExpression(string field, Type fieldType, Type declaringType) : FieldExpression(field, fieldType, declaringType) +/// +public class ScalarFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression root) + : FieldExpression(field, fieldType, declaringType, root) { } diff --git a/LinQL/Expressions/TypeFieldExpression.cs b/LinQL/Expressions/TypeFieldExpression.cs index adfed9b..9a59e0e 100644 --- a/LinQL/Expressions/TypeFieldExpression.cs +++ b/LinQL/Expressions/TypeFieldExpression.cs @@ -5,61 +5,27 @@ namespace LinQL.Expressions; /// /// An expression that defines access to a field that can have child fields. /// -public class TypeFieldExpression : FieldExpression +/// +/// Create a new . +/// +/// The field name. +/// The return type of the field. +/// The .Net type that field is a member of. +/// +public class TypeFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression? root) + : FieldExpression(field, fieldType, declaringType, root) { private Dictionary fields = []; - private Dictionary arguments = []; - - /// - /// Create a new . - /// - /// The field name. - /// The return type of the field. - /// The .Net type that field is a member of. - /// - public TypeFieldExpression(string field, Type fieldType, Type declaringType, IRootExpression? root) - : base(field, fieldType, declaringType) => this.Root = root ?? (this as IRootExpression)!; - - /// - protected TypeFieldExpression(string field, Type fieldType, Type declaringType) - : this(field, fieldType, declaringType, null) - { - } - - /// - /// Gets the arguments to be passed to the field on the server. - /// - public IReadOnlyDictionary Arguments => this.arguments; - - /// - /// Gets the root expression - /// - public IRootExpression Root { get; } /// public FieldExpression WithField(FieldExpression field) => this.fields.GetOrAdd(field.FieldName, () => field); - /// - /// Add an argument to the field. - /// - /// The name of the argument. - /// The graphql type. - /// The name of the variable that will hold the argument value. - /// The updated . - 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, }; /// diff --git a/LinQL/Translation/ExpressionTranslator.cs b/LinQL/Translation/ExpressionTranslator.cs index 000052a..bfc9e57 100644 --- a/LinQL/Translation/ExpressionTranslator.cs +++ b/LinQL/Translation/ExpressionTranslator.cs @@ -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) => ( diff --git a/LinQL/Translation/GraphQLExpressionTranslator.cs b/LinQL/Translation/GraphQLExpressionTranslator.cs index 842eb53..c6f96b9 100644 --- a/LinQL/Translation/GraphQLExpressionTranslator.cs +++ b/LinQL/Translation/GraphQLExpressionTranslator.cs @@ -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; }