From 51249fec2e094f292ff20f412316fcca6d8d6578 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Sat, 17 Apr 2021 16:20:00 +0200 Subject: [PATCH] feat: add source generator with basic attribute generation --- Mallos.Searchable.sln | 17 +- source-generator-issues.md | 13 ++ .../Mallos.Searchable.CodeAnalyzer.csproj | 29 ++++ .../ReflectionHelper.cs | 17 ++ .../SearchableSourceGenerator.cs | 114 ++++++++++++++ .../SearchableSyntaxReceiver.cs | 149 ++++++++++++++++++ .../Attributes/FilterBaseAttribute.cs | 9 ++ .../Attributes/FilterGenerator.cs | 16 ++ .../Attributes/FilterKeyAttribute.cs | 16 ++ .../Attributes/FilterStringMatchAttribute.cs | 79 ++++++++++ src/Mallos.Searchable/Lexer.cs | 5 +- .../Mallos.Searchable.csproj | 2 +- .../Mallos.Searchable.Test.csproj | 2 + .../SearchableAnalyzeTest.cs | 10 +- .../SearchableAttributeTest.cs | 31 ++++ .../SearchableGeneratedTest.cs | 98 ++++++++++++ test/Mallos.Searchable.Test/SearchableTest.cs | 2 +- .../Mallos.Searchable.TestData.csproj | 14 ++ test/Mallos.Searchable.TestData/TestObject.cs | 21 +++ .../TestObjectSearchable.cs | 16 +- 20 files changed, 632 insertions(+), 28 deletions(-) create mode 100644 source-generator-issues.md create mode 100644 src/Mallos.Searchable.CodeAnalyzer/Mallos.Searchable.CodeAnalyzer.csproj create mode 100644 src/Mallos.Searchable.CodeAnalyzer/ReflectionHelper.cs create mode 100644 src/Mallos.Searchable.CodeAnalyzer/SearchableSourceGenerator.cs create mode 100644 src/Mallos.Searchable.CodeAnalyzer/SearchableSyntaxReceiver.cs create mode 100644 src/Mallos.Searchable/Attributes/FilterBaseAttribute.cs create mode 100644 src/Mallos.Searchable/Attributes/FilterGenerator.cs create mode 100644 src/Mallos.Searchable/Attributes/FilterKeyAttribute.cs create mode 100644 src/Mallos.Searchable/Attributes/FilterStringMatchAttribute.cs create mode 100644 test/Mallos.Searchable.Test/SearchableAttributeTest.cs create mode 100644 test/Mallos.Searchable.Test/SearchableGeneratedTest.cs create mode 100644 test/Mallos.Searchable.TestData/Mallos.Searchable.TestData.csproj create mode 100644 test/Mallos.Searchable.TestData/TestObject.cs rename test/{Mallos.Searchable.Test => Mallos.Searchable.TestData}/TestObjectSearchable.cs (79%) diff --git a/Mallos.Searchable.sln b/Mallos.Searchable.sln index 6356309..99b8469 100644 --- a/Mallos.Searchable.sln +++ b/Mallos.Searchable.sln @@ -9,14 +9,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mallos.Searchable", "src\Ma EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D375808D-C9F7-4748-9253-BF93762A3E74}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mallos.Searchable.Test", "test\Mallos.Searchable.Test\Mallos.Searchable.Test.csproj", "{54CF0052-174A-43EE-9B57-6B940BC7DB59}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mallos.Searchable.Test", "test\Mallos.Searchable.Test\Mallos.Searchable.Test.csproj", "{54CF0052-174A-43EE-9B57-6B940BC7DB59}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8406D9E0-8BD7-47CD-955E-3DD53500F70B}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig README.md = README.md + source-generator-issues.md = source-generator-issues.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mallos.Searchable.CodeAnalyzer", "src\Mallos.Searchable.CodeAnalyzer\Mallos.Searchable.CodeAnalyzer.csproj", "{D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mallos.Searchable.TestData", "test\Mallos.Searchable.TestData\Mallos.Searchable.TestData.csproj", "{81C8953E-24EA-4C91-BD34-29DB60F9CF1C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +36,14 @@ Global {54CF0052-174A-43EE-9B57-6B940BC7DB59}.Debug|Any CPU.Build.0 = Debug|Any CPU {54CF0052-174A-43EE-9B57-6B940BC7DB59}.Release|Any CPU.ActiveCfg = Release|Any CPU {54CF0052-174A-43EE-9B57-6B940BC7DB59}.Release|Any CPU.Build.0 = Release|Any CPU + {D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B}.Release|Any CPU.Build.0 = Release|Any CPU + {81C8953E-24EA-4C91-BD34-29DB60F9CF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81C8953E-24EA-4C91-BD34-29DB60F9CF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81C8953E-24EA-4C91-BD34-29DB60F9CF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81C8953E-24EA-4C91-BD34-29DB60F9CF1C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -38,6 +51,8 @@ Global GlobalSection(NestedProjects) = preSolution {16741BDE-60CD-48BA-9ADE-992B6906E5DB} = {E85FD8AD-8255-4D2C-99BD-2750358B7074} {54CF0052-174A-43EE-9B57-6B940BC7DB59} = {D375808D-C9F7-4748-9253-BF93762A3E74} + {D2510BCB-B1F5-4409-86AD-54CD7CBE1C7B} = {E85FD8AD-8255-4D2C-99BD-2750358B7074} + {81C8953E-24EA-4C91-BD34-29DB60F9CF1C} = {D375808D-C9F7-4748-9253-BF93762A3E74} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C40FF5E9-F426-4AC5-B158-22E7487733C4} diff --git a/source-generator-issues.md b/source-generator-issues.md new file mode 100644 index 0000000..ffef777 --- /dev/null +++ b/source-generator-issues.md @@ -0,0 +1,13 @@ +# Source Generator issues +## Summary +[Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md) + +When working with the source generators I hit a lot of issues. +Here is a collection of the issues I hit which forced me to make +other design decisions which I wouldn't otherwise. + +## Issues +* Can only read the currently referenced project, would be nice to get a better overview of type usage. +* Unable to get more information from external attributes, than the name and argument list. + * Would be nice to be able to visit the syntax nodes in external libraries too. + * Only option I see here is to use reflection, but that creates other issues. diff --git a/src/Mallos.Searchable.CodeAnalyzer/Mallos.Searchable.CodeAnalyzer.csproj b/src/Mallos.Searchable.CodeAnalyzer/Mallos.Searchable.CodeAnalyzer.csproj new file mode 100644 index 0000000..278c44c --- /dev/null +++ b/src/Mallos.Searchable.CodeAnalyzer/Mallos.Searchable.CodeAnalyzer.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + 8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + diff --git a/src/Mallos.Searchable.CodeAnalyzer/ReflectionHelper.cs b/src/Mallos.Searchable.CodeAnalyzer/ReflectionHelper.cs new file mode 100644 index 0000000..0ecf96f --- /dev/null +++ b/src/Mallos.Searchable.CodeAnalyzer/ReflectionHelper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Mallos.Searchable.CodeAnalyzer +{ + static class ReflectionHelper + { + public static IEnumerable AllOfType() + { + return Assembly.GetAssembly(typeof(T)) + .GetTypes() + .Where(x => x.IsSubclassOf(typeof(T))); + } + } +} diff --git a/src/Mallos.Searchable.CodeAnalyzer/SearchableSourceGenerator.cs b/src/Mallos.Searchable.CodeAnalyzer/SearchableSourceGenerator.cs new file mode 100644 index 0000000..fd9a8d4 --- /dev/null +++ b/src/Mallos.Searchable.CodeAnalyzer/SearchableSourceGenerator.cs @@ -0,0 +1,114 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Mallos.Searchable.CodeAnalyzer +{ + [Generator] + public class SearchableSourceGenerator : ISourceGenerator + { + private readonly List filterNames = new List(); + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SearchableSyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + filterNames.Clear(); + + SearchableSyntaxReceiver syntaxReceiver = (SearchableSyntaxReceiver)context.SyntaxReceiver; + Generate(context, syntaxReceiver); + } + + private void Generate(GeneratorExecutionContext context, SearchableSyntaxReceiver syntaxReceiver) + { + var sb = new StringBuilder(); + sb.AppendLine("using Mallos.Searchable;"); + sb.AppendLine("using Mallos.Searchable.Attributes;"); + sb.AppendLine("using System;"); + sb.AppendLine("using System.Collections.Generic;"); + + foreach (var ns in syntaxReceiver.Generators.Select(x => x.ClassNamespace).Distinct()) + { + sb.AppendLine($"using {ns};"); + } + + foreach (var node in syntaxReceiver.Generators) + { + foreach (var member in node.Members) + { + if (member.Member is PropertyDeclarationSyntax property) + { + GenerateClass(sb, node.TargetClassName, node.ClassName, property.Identifier.ValueText, member); + } + + if (member.Member is FieldDeclarationSyntax field) + { + GenerateClass(sb, node.TargetClassName, node.ClassName, field.Declaration.Type.ToString(), member); + } + } + + sb.AppendLine($@" +public class {node.TargetClassName} : Searchable<{node.ClassName}> +{{ + public {node.TargetClassName}() + {{ +{string.Join("\n", filterNames.Select(x => $" Filters.Add(new {x}());"))} + }} + + protected override IEnumerable<{node.ClassName}> FreeTextFilter( + IEnumerable<{node.ClassName}> values, bool negative, string text) + {{ + return base.FreeTextFilter(values, negative, text); + }} +}}"); + } + + string finalSource = sb.ToString(); + + SourceText sourceText = SourceText.From(finalSource, Encoding.UTF8); + + context.AddSource("GeneratedSearchables.cs", sourceText); + } + + private void GenerateClass( + StringBuilder sb, + string generatorName, + string targetName, + string identifierName, + GeneratorNodeAttributeGroup member) + { + foreach (var attr in member.Attributes) + { + var keyName = attr.GetKey() ?? identifierName; + var filterClassName = $"{generatorName}Filter_{attr.Rule.Name}_{keyName}"; + filterNames.Add(filterClassName); + + var arguments = string.Join(",\n ", attr.Rule.ArgumentList.Arguments); + var argumentsText = arguments.Length > 0 + ? $",\n {arguments}" + : string.Empty; + + sb.AppendLine($@" +public class {filterClassName} : SimpleFilter<{targetName}> +{{ + public override string Key => ""{keyName}""; + + protected override bool Check({targetName} item, string value) + {{ + return {attr.RuleType}.Execute( + item.{identifierName}, + value{argumentsText} + ); + }} +}}"); + } + } + } +} diff --git a/src/Mallos.Searchable.CodeAnalyzer/SearchableSyntaxReceiver.cs b/src/Mallos.Searchable.CodeAnalyzer/SearchableSyntaxReceiver.cs new file mode 100644 index 0000000..f811156 --- /dev/null +++ b/src/Mallos.Searchable.CodeAnalyzer/SearchableSyntaxReceiver.cs @@ -0,0 +1,149 @@ +namespace Mallos.Searchable.CodeAnalyzer +{ + using Mallos.Searchable.Attributes; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using System; + using System.Collections.Generic; + using System.Linq; + + class GeneratorNodeAttribute + { + public AttributeSyntax Key { get; } + + public AttributeSyntax Rule { get; } + + public Type RuleType { get; } + + public GeneratorNodeAttribute(AttributeSyntax key, AttributeSyntax rule, Type ruleType) + { + this.Key = key; + this.Rule = rule; + this.RuleType = ruleType; + } + + public string GetKey() + { + if (this.Rule == null) + return null; + + return this.Key.ArgumentList.Arguments[0].ToString().Replace("\"", ""); + } + } + + class GeneratorNodeAttributeGroup + { + public MemberDeclarationSyntax Member { get; } + + public List Attributes { get; } + + public GeneratorNodeAttributeGroup(MemberDeclarationSyntax member, List attributes) + { + this.Member = member; + this.Attributes = attributes; + } + } + + class GeneratorNode + { + public AttributeSyntax FilterGenerator { get; } + + public TypeDeclarationSyntax ObjectSyntax { get; } + + public GeneratorNodeAttributeGroup[] Members { get; } + + public string ClassNamespace => "Mallos.Searchable.Test"; + + public string ClassName => this.ObjectSyntax.Identifier.ValueText; + + public string TargetClassName { get; } + + public GeneratorNode( + AttributeSyntax filterGenerator, + TypeDeclarationSyntax objectSyntax, + GeneratorNodeAttributeGroup[] members) + { + this.FilterGenerator = filterGenerator; + this.ObjectSyntax = objectSyntax; + this.Members = members; + + // Process Attribute + var filterGeneratorAttribute = this.FilterGenerator.ArgumentList.Arguments[0]; + this.TargetClassName = filterGeneratorAttribute.ToString().Replace("\"", ""); + } + } + + class SearchableSyntaxReceiver : ISyntaxReceiver + { + public List Generators { get; private set; } = new List(); + + private readonly List filterAttributes; + + public SearchableSyntaxReceiver() + { + this.filterAttributes = ReflectionHelper.AllOfType().ToList(); + } + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax + || syntaxNode is StructDeclarationSyntax + || syntaxNode is RecordDeclarationSyntax) + { + var syntax = (TypeDeclarationSyntax)syntaxNode; + + var attr = GetAttribute(syntax, "FilterGenerator"); + if (attr != null) + { + var members = syntax.Members + .Where(x => x.IsKind(SyntaxKind.PropertyDeclaration) || x.IsKind(SyntaxKind.FieldDeclaration)) + .Select(x => new GeneratorNodeAttributeGroup(x, GetFilterAttributes(x))) + .ToArray(); + + Generators.Add(new GeneratorNode(attr, syntax, members)); + } + } + } + + private List GetFilterAttributes(MemberDeclarationSyntax syntax) + { + var attrs = new List(); + if (syntax.AttributeLists != null) + { + foreach (var list in syntax.AttributeLists) + { + AttributeSyntax keyAttr = null; + AttributeSyntax filterAttr = null; + Type filterAttrType = null; + foreach (var attr in list.Attributes) + { + if (attr.Name.ToString().Contains("FilterKey")) + keyAttr = attr; + + var foundFilterType = filterAttributes.Find(x => x.Name.Contains(attr.Name.ToString())); + if (foundFilterType != null) + { + filterAttr = attr; + filterAttrType = foundFilterType; + } + } + attrs.Add(new GeneratorNodeAttribute(keyAttr, filterAttr, filterAttrType)); + } + } + return attrs; + } + + private static AttributeSyntax GetAttribute(TypeDeclarationSyntax syntax, string name) + { + if (syntax.AttributeLists != null) + { + foreach (var list in syntax.AttributeLists) + foreach (var attr in list.Attributes) + if (attr.Name.ToString() == name) + return attr; + } + return null; + } + } +} diff --git a/src/Mallos.Searchable/Attributes/FilterBaseAttribute.cs b/src/Mallos.Searchable/Attributes/FilterBaseAttribute.cs new file mode 100644 index 0000000..de529ec --- /dev/null +++ b/src/Mallos.Searchable/Attributes/FilterBaseAttribute.cs @@ -0,0 +1,9 @@ +namespace Mallos.Searchable.Attributes +{ + using System; + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = true)] + public abstract class FilterBaseAttribute : Attribute + { + } +} diff --git a/src/Mallos.Searchable/Attributes/FilterGenerator.cs b/src/Mallos.Searchable/Attributes/FilterGenerator.cs new file mode 100644 index 0000000..9a913a0 --- /dev/null +++ b/src/Mallos.Searchable/Attributes/FilterGenerator.cs @@ -0,0 +1,16 @@ +namespace Mallos.Searchable.Attributes +{ + using System; + + // TODO: Add record for .NET 5 + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] + public sealed class FilterGenerator : Attribute + { + public string ClassName { get; } + + public FilterGenerator(string className) + { + this.ClassName = className; + } + } +} diff --git a/src/Mallos.Searchable/Attributes/FilterKeyAttribute.cs b/src/Mallos.Searchable/Attributes/FilterKeyAttribute.cs new file mode 100644 index 0000000..bff57ec --- /dev/null +++ b/src/Mallos.Searchable/Attributes/FilterKeyAttribute.cs @@ -0,0 +1,16 @@ +namespace Mallos.Searchable.Attributes +{ + using System; + using System.Runtime.CompilerServices; + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = true)] + public sealed class FilterKeyAttribute : Attribute + { + public string Key { get; } + + public FilterKeyAttribute([CallerMemberName] string key = null) + { + this.Key = key; + } + } +} diff --git a/src/Mallos.Searchable/Attributes/FilterStringMatchAttribute.cs b/src/Mallos.Searchable/Attributes/FilterStringMatchAttribute.cs new file mode 100644 index 0000000..fcfe4b8 --- /dev/null +++ b/src/Mallos.Searchable/Attributes/FilterStringMatchAttribute.cs @@ -0,0 +1,79 @@ +namespace Mallos.Searchable.Attributes +{ + using System; + + public enum FilterMatchType + { + Match, + Contains, + StartWith, + EndWith + } + + public sealed class FilterStringMatchAttribute : FilterBaseAttribute + { + public FilterMatchType FilterMatchType { get; } + + public StringComparison ComparisonType { get; } + + public FilterStringMatchAttribute() + : this(FilterMatchType.Contains) + { + } + + public FilterStringMatchAttribute(FilterMatchType FilterMatchType) + : this(FilterMatchType, StringComparison.OrdinalIgnoreCase) + { + } + + public FilterStringMatchAttribute(FilterMatchType FilterMatchType, StringComparison comparisonType) + { + this.FilterMatchType = FilterMatchType; + this.ComparisonType = comparisonType; + } + + public static bool Execute( + string value, + string query, + FilterMatchType FilterMatchType = FilterMatchType.Contains, + StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) + { + switch (FilterMatchType) + { + case FilterMatchType.Match: + return value.Equals(query, comparisonType); + + case FilterMatchType.Contains: +#if NETSTANDARD2_1 + return value.Contains(query, comparisonType); +#else + // NOTE: I am not sure this is correct + switch (comparisonType) + { + default: + case StringComparison.InvariantCulture: + case StringComparison.CurrentCulture: + case StringComparison.Ordinal: + return value.Contains(query); + + case StringComparison.CurrentCultureIgnoreCase: + case StringComparison.OrdinalIgnoreCase: + return value.ToLower().Contains(query.ToLower()); + + case StringComparison.InvariantCultureIgnoreCase: + return value.ToLowerInvariant().Contains(query.ToLowerInvariant()); + } +#endif + + case FilterMatchType.StartWith: + return value.StartsWith(query, comparisonType); + + case FilterMatchType.EndWith: + return value.EndsWith(query, comparisonType); + + default: + throw new NotSupportedException(nameof(comparisonType)); + } + } + } +} diff --git a/src/Mallos.Searchable/Lexer.cs b/src/Mallos.Searchable/Lexer.cs index d99684e..a43a521 100644 --- a/src/Mallos.Searchable/Lexer.cs +++ b/src/Mallos.Searchable/Lexer.cs @@ -170,18 +170,15 @@ out var unicodeValue } } - return new Token(TokenType.String, result[0..^1], row, col); + return new Token(TokenType.String, result.Substring(0, result.Length - 1), row, col); } else { sb.Clear(); - char lastChar = 'a'; while (!singleCharTokens.ContainsKey(currentChar)) { sb.Append(currentChar); - - lastChar = currentChar; NextChar(); } diff --git a/src/Mallos.Searchable/Mallos.Searchable.csproj b/src/Mallos.Searchable/Mallos.Searchable.csproj index 83c16cf..042c946 100644 --- a/src/Mallos.Searchable/Mallos.Searchable.csproj +++ b/src/Mallos.Searchable/Mallos.Searchable.csproj @@ -1,7 +1,7 @@ - netstandard2.1 + netstandard2.0 A simple string queryable system inspired by Honeybadger's search. search, filter, query, tags Mallos.Searchable diff --git a/test/Mallos.Searchable.Test/Mallos.Searchable.Test.csproj b/test/Mallos.Searchable.Test/Mallos.Searchable.Test.csproj index b37dbc9..84b0799 100644 --- a/test/Mallos.Searchable.Test/Mallos.Searchable.Test.csproj +++ b/test/Mallos.Searchable.Test/Mallos.Searchable.Test.csproj @@ -20,7 +20,9 @@ + + diff --git a/test/Mallos.Searchable.Test/SearchableAnalyzeTest.cs b/test/Mallos.Searchable.Test/SearchableAnalyzeTest.cs index 217e58d..c67c4e4 100644 --- a/test/Mallos.Searchable.Test/SearchableAnalyzeTest.cs +++ b/test/Mallos.Searchable.Test/SearchableAnalyzeTest.cs @@ -4,14 +4,8 @@ namespace Mallos.Searchable.Test public class SearchableAnalyzeTest { - readonly TestObjectSearchable searchable = new TestObjectSearchable(); - readonly TestObject[] testdata = new TestObject[] - { - new ("1"), - new ("2"), - new ("3") - }; - + readonly TestObjectSearchable searchable = new(); + [Fact] public void Analyze_IsFilter_ValueMatch() { diff --git a/test/Mallos.Searchable.Test/SearchableAttributeTest.cs b/test/Mallos.Searchable.Test/SearchableAttributeTest.cs new file mode 100644 index 0000000..286878e --- /dev/null +++ b/test/Mallos.Searchable.Test/SearchableAttributeTest.cs @@ -0,0 +1,31 @@ +namespace Mallos.Searchable.Test +{ + using System.Linq; + using Xunit; + + public class SearchableAttributeTest + { + readonly TestObjectSearchable searchable = new(); + + [Fact] + public void Search_IsFilter_FoundMatch() + { + // Arrange + var query = "is:one"; + var values = new TestObject[] + { + new ("1"), + new ("2"), + new ("3"), + new ("1 2") + }; + + // Act + var result = searchable.Search(values, query).ToArray(); + + // Assert + Assert.Single(result); + Assert.Equal(values[0].Value, result[0].Value); + } + } +} diff --git a/test/Mallos.Searchable.Test/SearchableGeneratedTest.cs b/test/Mallos.Searchable.Test/SearchableGeneratedTest.cs new file mode 100644 index 0000000..4588dd0 --- /dev/null +++ b/test/Mallos.Searchable.Test/SearchableGeneratedTest.cs @@ -0,0 +1,98 @@ +namespace Mallos.Searchable.Test +{ + using System.Linq; + using Xunit; + + public class SearchableGeneratedTest + { + readonly MyGeneratedSearchable searchable = new(); + + [Fact] + public void Search_IsFilter_FoundMatch() + { + // Arrange + var query = "is:one"; + var values = new TestObject[] + { + new ("1"), + new ("2"), + new ("3"), + new ("1 2") + }; + + // Act + var result = searchable.Search(values, query).ToArray(); + + // Assert + Assert.Single(result); + Assert.Equal(values[0].Value, result[0].Value); + } + + [Fact] + public void Search_IsFilter_NegativeFoundMatches() + { + // Arrange + var query = "-is:one"; + var values = new TestObject[] + { + new ("1"), + new ("2"), + new ("3"), + new ("1 2") + }; + + // Act + var result = searchable.Search(values, query).ToArray(); + + // Assert + Assert.Equal(3, result.Length); + Assert.Equal(values[1].Value, result[0].Value); + Assert.Equal(values[2].Value, result[1].Value); + Assert.Equal(values[3].Value, result[2].Value); + } + + [Fact] + public void Search_Filter_FoundMatches() + { + // Arrange + var query = "value:1"; + var values = new TestObject[] + { + new ("1"), + new ("2"), + new ("3"), + new ("1 2") + }; + + // Act + var result = searchable.Search(values, query).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal(values[0].Value, result[0].Value); + Assert.Equal(values[3].Value, result[1].Value); + } + + [Fact] + public void Search_Filter_NegativeFoundMatches() + { + // Arrange + var query = "-value:1"; + var values = new TestObject[] + { + new ("1"), + new ("2"), + new ("3"), + new ("1 2") + }; + + // Act + var result = searchable.Search(values, query).ToArray(); + + // Assert + Assert.Equal(2, result.Length); + Assert.Equal(values[1].Value, result[0].Value); + Assert.Equal(values[2].Value, result[1].Value); + } + } +} diff --git a/test/Mallos.Searchable.Test/SearchableTest.cs b/test/Mallos.Searchable.Test/SearchableTest.cs index c3bde11..8cb0636 100644 --- a/test/Mallos.Searchable.Test/SearchableTest.cs +++ b/test/Mallos.Searchable.Test/SearchableTest.cs @@ -5,7 +5,7 @@ namespace Mallos.Searchable.Test public class SearchableTest { - readonly TestObjectSearchable searchable = new TestObjectSearchable(); + readonly TestObjectSearchable searchable = new(); [Fact] public void Search_IsFilter_FoundMatch() diff --git a/test/Mallos.Searchable.TestData/Mallos.Searchable.TestData.csproj b/test/Mallos.Searchable.TestData/Mallos.Searchable.TestData.csproj new file mode 100644 index 0000000..95abf52 --- /dev/null +++ b/test/Mallos.Searchable.TestData/Mallos.Searchable.TestData.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + true + $(BaseIntermediateOutputPath)\GeneratedFiles + + + + + + + + diff --git a/test/Mallos.Searchable.TestData/TestObject.cs b/test/Mallos.Searchable.TestData/TestObject.cs new file mode 100644 index 0000000..e151673 --- /dev/null +++ b/test/Mallos.Searchable.TestData/TestObject.cs @@ -0,0 +1,21 @@ +namespace Mallos.Searchable.Test +{ + using Mallos.Searchable.Attributes; + using System; + + [FilterGenerator("MyGeneratedSearchable")] + public class TestObject + { + [FilterKey("v"), FilterStringMatch(FilterMatchType.Match)] + [FilterKey("value"), FilterStringMatch(FilterMatchType.Contains, StringComparison.OrdinalIgnoreCase)] + public string Value { get; set; } + + [FilterKey("key"), FilterStringMatch()] + public string Key { get; set; } + + public TestObject(string value) + { + this.Value = value; + } + } +} diff --git a/test/Mallos.Searchable.Test/TestObjectSearchable.cs b/test/Mallos.Searchable.TestData/TestObjectSearchable.cs similarity index 79% rename from test/Mallos.Searchable.Test/TestObjectSearchable.cs rename to test/Mallos.Searchable.TestData/TestObjectSearchable.cs index ca06dbe..b159d87 100644 --- a/test/Mallos.Searchable.Test/TestObjectSearchable.cs +++ b/test/Mallos.Searchable.TestData/TestObjectSearchable.cs @@ -3,17 +3,7 @@ namespace Mallos.Searchable.Test using System.Collections.Generic; using System.Linq; - class TestObject - { - public TestObject(string value) - { - this.Value = value; - } - - public string Value { get; set; } - } - - class IsOne : IFilter + public class IsOne : IFilter { public string Key => "one"; @@ -24,7 +14,7 @@ public IEnumerable Negative(IEnumerable query, string va => query.Where(x => x.Value != "1"); } - class Value : IFilter + public class Value : IFilter { public string Key => "value"; @@ -35,7 +25,7 @@ public IEnumerable Negative(IEnumerable query, string va => query.Where(x => !x.Value.Contains(value)); } - class TestObjectSearchable : Searchable + public class TestObjectSearchable : Searchable { public TestObjectSearchable() {