Skip to content

Add Function support to LINQ DSL #134

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

Merged
merged 2 commits into from
Jul 9, 2024
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
16 changes: 15 additions & 1 deletion Fauna.Test/Helpers/Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public class AuthorCol : Collection<Author>
}

public AuthorCol Author { get => GetCollection<AuthorCol>(); }

public Function<int> Add2(int val) => Fn<int>().Call(val);
public Function<string> SayHello() => Fn<string>("SayHello").Call();
public Function<string> SayHelloArray() => Fn<string>("SayHelloArray").Call();
public Function<Author> GetAuthors() => Fn<Author>().Call();

}

public class EmbeddedSet
Expand All @@ -40,6 +46,11 @@ public static class Fixtures
public static AuthorDb AuthorDb(Client client)
{
client.QueryAsync(FQL($"Collection.byName('Author')?.delete()")).Wait();
client.QueryAsync(FQL($"Function.byName('SayHello')?.delete()")).Wait();
client.QueryAsync(FQL($"Function.byName('SayHelloArray')?.delete()")).Wait();
client.QueryAsync(FQL($"Function.byName('GetAuthors')?.delete()")).Wait();
client.QueryAsync(FQL($"Function.byName('Add2')?.delete()")).Wait();

client.QueryAsync(FQL(
$@"Collection.create({{
name: 'Author',
Expand All @@ -52,7 +63,10 @@ public static AuthorDb AuthorDb(Client client)
.Wait();
client.QueryAsync(FQL($"Author.create({{name: 'Alice', age: 32 }})")).Wait();
client.QueryAsync(FQL($"Author.create({{name: 'Bob', age: 26 }})")).Wait();

client.QueryAsync(FQL($"Function.create({{name: 'SayHello', body: '() => \"Hello!\"'}})")).Wait();
client.QueryAsync(FQL($"Function.create({{name: 'SayHelloArray', body: '() => [SayHello(), SayHello()]'}})")).Wait();
client.QueryAsync(FQL($"Function.create({{name: 'GetAuthors', body: '() => Author.all()'}})")).Wait();
client.QueryAsync(FQL($"Function.create({{name: 'Add2', body: '(t) => t + 2'}})")).Wait();
return client.DataContext<AuthorDb>();
}

Expand Down
17 changes: 15 additions & 2 deletions Fauna.Test/Linq/Context.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ public class AuthorCol : Collection<Author>
[Name("posts")]
public class PostCol : Collection<Post> { }

public AuthorCol Author { get => GetCollection<AuthorCol>(); }
public PostCol Post { get => GetCollection<PostCol>(); }
public AuthorCol Author => GetCollection<AuthorCol>();
public PostCol Post => GetCollection<PostCol>();
public Function<string> TestFunc() => Fn<string>("TestFunc").Call();
public Function<string> TestFuncInferred() => Fn<string>().Call();

}

[AllowNull]
Expand Down Expand Up @@ -70,5 +73,15 @@ public void ReturnsADataContext()
Assert.AreEqual(byName2.Name, "realByName");
Assert.AreEqual(byName2.DocType, typeof(Author));
Assert.AreEqual(byName2.Args, new object[] { "Alice" });

var fn = db.TestFunc();
Assert.AreEqual(fn.Name, "TestFunc");
Assert.AreEqual(fn.ReturnType, typeof(string));
Assert.AreEqual(fn.Args, Array.Empty<object>());

var fnInferred = db.TestFuncInferred();
Assert.AreEqual(fnInferred.Name, "TestFuncInferred");
Assert.AreEqual(fnInferred.ReturnType, typeof(string));
Assert.AreEqual(fnInferred.Args, Array.Empty<object>());
}
}
35 changes: 35 additions & 0 deletions Fauna.Test/Linq/Query.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,39 @@ public async Task Query_Take()
var names = authors.Select(a => a.Name);
Assert.AreEqual(new List<string> { "Alice" }, names);
}

[Test]
public async Task Query_Function_Value_Single()
{
var ret = await db.SayHello().SingleAsync();
Assert.AreEqual("Hello!", ret);
}

[Test]
public async Task Query_Function_Value_List()
{
var ret = await db.SayHello().ToListAsync();
Assert.AreEqual(new List<string> { "Hello!" }, ret);
}

[Test]
public async Task Query_Function_With_Param()
{
var ret = await db.Add2(8).SingleAsync();
Assert.AreEqual(10, ret);
}

[Test]
public async Task Query_Function_Array()
{
var ret = await db.SayHelloArray().ToListAsync();
Assert.AreEqual(new List<string> { "Hello!", "Hello!" }, ret);
}

[Test]
public async Task Query_Function_Set()
{
var ret = await db.GetAuthors().Select(x => x.Name).ToListAsync();
Assert.AreEqual(new List<string> { "Alice", "Bob" }, ret);
}
}
20 changes: 20 additions & 0 deletions Fauna.Test/Serialization/Deserializer.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,15 @@ public void DeserializeIntoList()
Assert.AreEqual(expected, p);
}

[Test]
public void DeserializeIntoListFromSingleValue()
{
const string given = @"""item1""";
var expected = new List<string> { "item1" };
var p = Deserialize<List<string>>(given);
Assert.AreEqual(expected, p);
}

[Test]
public void DeserializeIntoGenericListWithPrimitive()
{
Expand Down Expand Up @@ -1029,6 +1038,17 @@ public void DeserializeIntoPageWithPrimitive()
Assert.AreEqual("next_page_cursor", result.After);
}

[Test]
public void DeserializeIntoPageWithSingleValue()
{
const string given = @"""SingleValue""";

var result = Deserialize<Page<string>>(given);
Assert.IsNotNull(result);
Assert.AreEqual(new List<string> { "SingleValue" }, result.Data);
Assert.IsNull(result.After);
}

[Test]
public void DeserializeIntoPageWithUserDefinedClass()
{
Expand Down
55 changes: 55 additions & 0 deletions Fauna/Linq/DataContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,61 @@ internal Index(Collection coll, string name, object[] args, DataContext ctx)
}
}

// UDF / Function DSL

public interface IFunction : Linq.IQuerySource
{
public string Name { get; }
public Type ReturnType { get; }
public object[] Args { get; }
}

protected class FunctionCall<T>
{
private readonly string _name;
private readonly DataContext _ctx;

public FunctionCall(string name, DataContext ctx)
{
_name = name;
_ctx = ctx;
}

public Function<T> Call() => Call(Array.Empty<object>());

public Function<T> Call(object a1) => Call(new[] { a1 });

public Function<T> Call(object a1, object a2) => Call(new[] { a1, a2 });

public Function<T> Call(object a1, object a2, object a3) => Call(new[] { a1, a2, a3 });

public Function<T> Call(object[] args) => new(_name, args, _ctx);

}

protected FunctionCall<T> Fn<T>(string name = "", [CallerMemberName] string callerName = "")
{
var fnName = name == "" ? callerName : name;
return new FunctionCall<T>(fnName, this);
}

public class Function<T> : Linq.QuerySource<T>, IFunction
{
public string Name { get; }

public Type ReturnType => typeof(T);

public object[] Args { get; }

internal Function(string name, object[] args, DataContext ctx)
{
Name = name;
Args = args;
Ctx = ctx;
SetQuery<T>(Linq.IntermediateQueryHelpers.Function(this));
}
}

protected Col GetCollection<Col>() where Col : Collection
{
CheckInitialization();
Expand Down
2 changes: 1 addition & 1 deletion Fauna/Linq/DataContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public DB Build(Client client)
}

private static bool IsColType(Type ty) =>
ty.GetInterfaces().Where(iface => iface == typeof(DataContext.Collection)).Any();
ty.GetInterfaces().Any(iface => iface == typeof(DataContext.Collection));

private static void ValidateColType(Type ty)
{
Expand Down
3 changes: 3 additions & 0 deletions Fauna/Linq/IntermediateQueryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public static Query CollectionAll(DataContext.Collection col) =>
public static Query CollectionIndex(DataContext.Index idx) =>
MethodCall(Expr(idx.Collection.Name), idx.Name, idx.Args.Select(Const));

public static Query Function(DataContext.IFunction fnc) =>
FnCall(fnc.Name, fnc.Args.Select(Const));

public static QueryExpr Concat(this Query q1, string str)
{
var frags = new List<IQueryFragment>();
Expand Down
11 changes: 8 additions & 3 deletions Fauna/Linq/QuerySourceDsl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,14 @@ private Query AbortIfEmpty(Query setq) =>

private Query Singularize(Query setq) =>
QH.Expr(@"({
let s = (").Concat(setq).Concat(@").take(2).toArray()
if (s.length > 1) abort(['not single'])
s.take(1)
let s = (").Concat(setq).Concat(@")
let s = if (s isa Set) s.toArray() else s
if (s isa Array) {
if (s.length > 1) abort(['not single'])
s.take(1)
} else {
[s]
}
})");

private Exception TranslateException(Exception ex) =>
Expand Down
16 changes: 5 additions & 11 deletions Fauna/Linq/SubQuerySwitch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,12 @@ protected override Query ApplyDefault(Expression? expr) =>

protected override Query ConstantExpr(ConstantExpression expr)
{
if (expr.Value is DataContext.Collection col)
return expr.Value switch
{
return QH.CollectionAll(col);
}
else if (expr.Value is DataContext.Index idx)
{
return QH.CollectionIndex(idx);
}
else
{
return QH.Const(expr.Value);
}
DataContext.Collection col => QH.CollectionAll(col),
DataContext.Index idx => QH.CollectionIndex(idx),
_ => QH.Const(expr.Value)
};
}

protected override Query LambdaExpr(LambdaExpression expr)
Expand Down
16 changes: 13 additions & 3 deletions Fauna/Serialization/ListDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,25 @@ public ListDeserializer(IDeserializer<T> elemDeserializer)

public override List<T> Deserialize(MappingContext context, ref Utf8FaunaReader reader)
{
if (reader.CurrentTokenType != TokenType.StartArray)
if (reader.CurrentTokenType == TokenType.StartPage)
throw new SerializationException(
$"Unexpected token while deserializing into {typeof(List<T>)}: {reader.CurrentTokenType}");
$"Unexpected token while deserializing into {typeof(List<T>)}: {reader.CurrentTokenType}");

var wrapInList = reader.CurrentTokenType != TokenType.StartArray;

var lst = new List<T>();
while (reader.Read() && reader.CurrentTokenType != TokenType.EndArray)

if (wrapInList)
{
lst.Add(_elemDeserializer.Deserialize(context, ref reader));
}
else
{
while (reader.Read() && reader.CurrentTokenType != TokenType.EndArray)
{
lst.Add(_elemDeserializer.Deserialize(context, ref reader));
}
}

return lst;
}
Expand Down
49 changes: 31 additions & 18 deletions Fauna/Serialization/PageDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,44 @@ public PageDeserializer(IDeserializer<T> elemDeserializer)

public override Page<T> Deserialize(MappingContext context, ref Utf8FaunaReader reader)
{
var endToken = reader.CurrentTokenType switch
var wrapInPage = false;
TokenType endToken = TokenType.None;
switch (reader.CurrentTokenType)
{
TokenType.StartPage => TokenType.EndPage,
TokenType.StartObject => TokenType.EndObject,
var other =>
throw new SerializationException(
$"Unexpected token while deserializing into {typeof(Page<T>)}: {other}"),
};
case TokenType.StartPage:
endToken = TokenType.EndPage;
break;
case TokenType.StartObject:
endToken = TokenType.EndObject;
break;
default:
wrapInPage = true;
break;
}

List<T>? data = null;
string? after = null;

while (reader.Read() && reader.CurrentTokenType != endToken)
if (wrapInPage)
{
var fieldName = reader.GetString()!;
reader.Read();

switch (fieldName)
data = _dataDeserializer.Deserialize(context, ref reader);
}
else
{
while (reader.Read() && reader.CurrentTokenType != endToken)
{
case "data":
data = _dataDeserializer.Deserialize(context, ref reader);
break;
case "after":
after = reader.GetString()!;
break;
var fieldName = reader.GetString()!;
reader.Read();

switch (fieldName)
{
case "data":
data = _dataDeserializer.Deserialize(context, ref reader);
break;
case "after":
after = reader.GetString()!;
break;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ class PersonDb : DataContext
}

public PersonCollection Person { get => GetCollection<PersonCollection>(); }
public Function<int> AddTwo(int val) => Fn<int>().Call(val);
public Function<int> TimesTwo(int val) => Fn<int>("MultiplyByTwo").Call(val);
}
```

Expand Down
Loading