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

Add MongoDB Target #6

Merged
merged 5 commits into from
Oct 28, 2022
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
2 changes: 0 additions & 2 deletions .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ name: Build & Test

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
Expand Down
14 changes: 14 additions & 0 deletions AnQL.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{E4
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnQL.Functions.CliDemo", "examples\AnQL.Functions.CliDemo\AnQL.Functions.CliDemo.csproj", "{2B2BFD4B-FEB8-423D-8DA8-5A1BFE8108F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnQL.Mongo", "src\AnQL.Mongo\AnQL.Mongo.csproj", "{A2F639D9-B894-4BF0-A46F-F8964C898B46}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnQL.Mongo.Tests", "test\AnQL.Mongo.Tests\AnQL.Mongo.Tests.csproj", "{8238DC07-9BE9-4620-809C-F455A2982101}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -60,6 +64,14 @@ Global
{2B2BFD4B-FEB8-423D-8DA8-5A1BFE8108F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B2BFD4B-FEB8-423D-8DA8-5A1BFE8108F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B2BFD4B-FEB8-423D-8DA8-5A1BFE8108F5}.Release|Any CPU.Build.0 = Release|Any CPU
{A2F639D9-B894-4BF0-A46F-F8964C898B46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2F639D9-B894-4BF0-A46F-F8964C898B46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2F639D9-B894-4BF0-A46F-F8964C898B46}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2F639D9-B894-4BF0-A46F-F8964C898B46}.Release|Any CPU.Build.0 = Release|Any CPU
{8238DC07-9BE9-4620-809C-F455A2982101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8238DC07-9BE9-4620-809C-F455A2982101}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8238DC07-9BE9-4620-809C-F455A2982101}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8238DC07-9BE9-4620-809C-F455A2982101}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AC0BD618-4DD5-4430-9C16-D30F39DAF015} = {7A34A325-3C52-49FA-8B28-AEEF715D9F79}
Expand All @@ -70,5 +82,7 @@ Global
{2B12DC25-9FF0-42CD-9EEF-D46497156C64} = {7A34A325-3C52-49FA-8B28-AEEF715D9F79}
{CF03D0D0-37CF-48B8-A87C-3136665C697B} = {AA50855C-66F2-447F-B7F5-E797876980FB}
{2B2BFD4B-FEB8-423D-8DA8-5A1BFE8108F5} = {E4F2B84A-7523-4B46-9F20-CD7C3287077C}
{A2F639D9-B894-4BF0-A46F-F8964C898B46} = {7A34A325-3C52-49FA-8B28-AEEF715D9F79}
{8238DC07-9BE9-4620-809C-F455A2982101} = {AA50855C-66F2-447F-B7F5-E797876980FB}
EndGlobalSection
EndGlobal
24 changes: 24 additions & 0 deletions src/AnQL.Mongo/AnQL.Mongo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>AnQL.Functions.Time</PackageId>
<Authors>Taylor Graham</Authors>
<RepositoryUrl>https://github.com/twgraham/AnQL</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.18.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AnQL.Core\AnQL.Core.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Helpers" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions src/AnQL.Mongo/AnQLBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using AnQL.Core;
using AnQL.Mongo.Resolvers;
using MongoDB.Driver;

namespace AnQL.Mongo;

public static class AnQLBuilderExtensions
{
public static FilterDefinitionAnQLParserBuilder<T> ForFilterDefinitions<T>(this AnQLBuilder anQlBuilder)
{
return anQlBuilder.For<FilterDefinitionAnQLParserBuilder<T>, FilterDefinition<T>, T>(Create<T>);
}

private static FilterDefinitionAnQLParserBuilder<T> Create<T>(AnQLParserOptions options)
{
var builder = new FilterDefinitionAnQLParserBuilder<T>(options);

builder.RegisterFactory(typeof(string), new StringResolver<T>.Factory());
builder.RegisterSimpleType<ushort>()
.RegisterSimpleType<short>()
.RegisterSimpleType<uint>()
.RegisterSimpleType<int>()
.RegisterSimpleType<ulong>()
.RegisterSimpleType<long>()
.RegisterSimpleType<float>()
.RegisterSimpleType<double>()
.RegisterSimpleType<decimal>()
.RegisterSimpleType<DateTime>()
.RegisterSimpleType<DateTimeOffset>()
.RegisterSimpleType<DateOnly>()
.RegisterSimpleType<bool>();

return builder;
}
}
79 changes: 79 additions & 0 deletions src/AnQL.Mongo/AnQLFilterDefinitionVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using AnQL.Core;
using AnQL.Core.Extensions;
using AnQL.Core.Grammar;
using AnQL.Core.Resolvers;
using MongoDB.Driver;

namespace AnQL.Mongo;

public class AnQLFilterDefinitionVisitor<T> : AnQLBaseVisitor<FilterDefinition<T>>
{
private readonly ResolverMap<FilterDefinition<T>, T> _resolverMap;

public override FilterDefinition<T> SuccessQueryResult => Builders<T>.Filter.Empty;
public override FilterDefinition<T> FailedQueryResult => "{ $expr: false }";

public AnQLFilterDefinitionVisitor(ResolverMap<FilterDefinition<T>, T> resolverMap, AnQLParserOptions options) : base(options)
{
_resolverMap = resolverMap;
}

public override FilterDefinition<T> VisitExprAND(AnQLGrammarParser.ExprANDContext context)
{
var left = Visit(context.expr(0));
var right = Visit(context.expr(1));

return left & right;
}

public override FilterDefinition<T> VisitExprOR(AnQLGrammarParser.ExprORContext context)
{
var left = Visit(context.expr(0));
var right = Visit(context.expr(1));

return left | right;
}

public override FilterDefinition<T> VisitNOT(AnQLGrammarParser.NOTContext context)
{
var inner = Visit(context.expr());
return !inner;
}

public override FilterDefinition<T> VisitParens(AnQLGrammarParser.ParensContext context)
{
return Visit(context.expr());
}

public override FilterDefinition<T> VisitEqual(AnQLGrammarParser.EqualContext context)
{
return BuildFilter(QueryOperation.Equal, context.property_path(), context.value());
}

public override FilterDefinition<T> VisitAnyEqual(AnQLGrammarParser.AnyEqualContext context)
{
var filters = context.value().Select(value => BuildFilter(QueryOperation.Equal, context.property_path(), value));
return Builders<T>.Filter.Or(filters);
}

public override FilterDefinition<T> VisitGreaterThan(AnQLGrammarParser.GreaterThanContext context)
{
return BuildFilter(QueryOperation.GreaterThan, context.property_path(), context.value());
}

public override FilterDefinition<T> VisitLessThan(AnQLGrammarParser.LessThanContext context)
{
return BuildFilter(QueryOperation.LessThan, context.property_path(), context.value());
}

private FilterDefinition<T> BuildFilter(QueryOperation operation, AnQLGrammarParser.Property_pathContext propertyPathContext,
AnQLGrammarParser.ValueContext valueContext)
{
if (!_resolverMap.TryGet(propertyPathContext.GetText(), out var resolver))
return HandleUnknownProperty(propertyPathContext);

var (value, type) = valueContext.GetValueAndAnQLType();

return resolver.Resolve(operation, value, type);
}
}
23 changes: 23 additions & 0 deletions src/AnQL.Mongo/FilterDefinitionAnQLParserBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AnQL.Core;
using AnQL.Mongo.Resolvers;
using MongoDB.Driver;

namespace AnQL.Mongo;

public class FilterDefinitionAnQLParserBuilder<T> : AnQLParserBuilder<FilterDefinition<T>, T>
{
public FilterDefinitionAnQLParserBuilder(AnQLParserOptions options) : base(options)
{
}

public FilterDefinitionAnQLParserBuilder<T> RegisterSimpleType<TType>()
{
return (FilterDefinitionAnQLParserBuilder<T>)RegisterFactory(typeof(TType),
new SimpleResolver<T, TType>.Factory());
}

public override IAnQLParser<FilterDefinition<T>> Build()
{
return new AnQLParser<FilterDefinition<T>>(new AnQLFilterDefinitionVisitor<T>(ResolverMap, Options));
}
}
73 changes: 73 additions & 0 deletions src/AnQL.Mongo/Resolvers/SimpleResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Linq.Expressions;
using System.Reflection;
using AnQL.Core.Helpers;
using AnQL.Core.Resolvers;
using MongoDB.Driver;

namespace AnQL.Mongo.Resolvers;

public class SimpleResolver<T, TValue> : IAnQLPropertyResolver<FilterDefinition<T>>
{
private readonly Options _options = new();

protected Expression<Func<T, TValue>> PropertyPath { get; }

public SimpleResolver(Expression<Func<T, TValue>> propertyPath, Action<Options>? configureOptions = null)
{
PropertyPath = propertyPath;
configureOptions?.Invoke(_options);
}

public virtual FilterDefinition<T> Resolve(QueryOperation op, string value, AnQLValueType valueType)
{
var converter = _options.ValueConverter ?? DefaultConverter;
var convertedValue = converter(value, valueType);

return op switch
{
QueryOperation.Equal => BuildEqual(convertedValue),
QueryOperation.GreaterThan => BuildGreaterThan(convertedValue),
QueryOperation.LessThan => BuildLessThan(convertedValue),
_ => throw new ArgumentOutOfRangeException(nameof(op), op, null)
};
}

protected FilterDefinition<T> BuildEqual(TValue value)
=> Builders<T>.Filter.Eq(PropertyPath, value);

protected FilterDefinition<T> BuildGreaterThan(TValue value)
=> Builders<T>.Filter.Gt(PropertyPath, value);

protected FilterDefinition<T> BuildLessThan(TValue value)
=> Builders<T>.Filter.Lt(PropertyPath, value);

private TValue DefaultConverter(string queryValue, AnQLValueType valueType)
{
return (TValue) Convert.ChangeType(queryValue, typeof(TValue));
}

public class Factory : IResolverFactory<T, FilterDefinition<T>>
{
private static BindingFlags _flags = BindingFlags.CreateInstance |
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.OptionalParamBinding;

public IAnQLPropertyResolver<FilterDefinition<T>> Build(Expression<Func<T, object>> propertyPath)
{
var propertyType = ExpressionHelper.GetPropertyPathType(propertyPath);
var resolverType = typeof(SimpleResolver<,>).MakeGenericType(typeof(T), propertyType);
var unconvertedPath = ExpressionHelper.StripConvert(propertyPath);

var resolver = Activator.CreateInstance(resolverType, _flags, null, new object?[] { unconvertedPath }, null)
?? throw new Exception("Unable to create resolver");

return (IAnQLPropertyResolver<FilterDefinition<T>>)resolver;
}
}

public class Options
{
public Func<string, AnQLValueType, TValue>? ValueConverter { get; set; }
}
}
52 changes: 52 additions & 0 deletions src/AnQL.Mongo/Resolvers/StringResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using AnQL.Core.Helpers;
using AnQL.Core.Resolvers;
using MongoDB.Driver;

namespace AnQL.Mongo.Resolvers;

public class StringResolver<T> : SimpleResolver<T, string>
{
private readonly Options _options = new();

public StringResolver(Expression<Func<T, string>> propertyPath, Action<Options>? configureOptions = null)
: base(propertyPath)
{
configureOptions?.Invoke(_options);
}

public override FilterDefinition<T> Resolve(QueryOperation op, string value, AnQLValueType valueType)
{
if (_options.RegexMatching && op == QueryOperation.Equal)
return Builders<T>.Filter.Regex(new ExpressionFieldDefinition<T>(PropertyPath), new Regex(value, _options.RegexOptions));

return op switch
{
QueryOperation.Equal => BuildEqual(value),
QueryOperation.GreaterThan => BuildGreaterThan(value),
QueryOperation.LessThan => BuildLessThan(value),
_ => throw new ArgumentOutOfRangeException(nameof(op))
};
}

public new class Factory : IResolverFactory<T, FilterDefinition<T>>
{
public IAnQLPropertyResolver<FilterDefinition<T>> Build(Expression<Func<T, object>> propertyPath)
{
var propertyType = ExpressionHelper.GetPropertyPathType(propertyPath);
if (propertyType != typeof(string))
throw new ArgumentException("Property should be a string", nameof(propertyPath));

var stringExpression = (Expression<Func<T, string>>) ExpressionHelper.StripConvert(propertyPath);

return new StringResolver<T>(stringExpression);
}
}

public new class Options
{
public bool RegexMatching { get; set; } = false;
public RegexOptions RegexOptions { get; set; } = RegexOptions.None;
}
}
28 changes: 28 additions & 0 deletions test/AnQL.Mongo.Tests/AnQL.Mongo.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AnQL.Mongo\AnQL.Mongo.csproj" />
</ItemGroup>

</Project>
Loading