Skip to content

Commit

Permalink
Add MongoDB Target (#6)
Browse files Browse the repository at this point in the history
* Add Mongo project with basic builder and visitor
* Don't restrict build-test workflow to master
* Add some basic resolvers for simple types and strings
  • Loading branch information
twgraham authored Oct 28, 2022
1 parent 1631067 commit 01b1b7a
Show file tree
Hide file tree
Showing 15 changed files with 715 additions and 2 deletions.
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

0 comments on commit 01b1b7a

Please sign in to comment.