diff --git a/src/OpenPolicyAgent.Ucast.Linq/QueryableExtensions.cs b/src/OpenPolicyAgent.Ucast.Linq/QueryableExtensions.cs index 52051f5..4eb7cec 100644 --- a/src/OpenPolicyAgent.Ucast.Linq/QueryableExtensions.cs +++ b/src/OpenPolicyAgent.Ucast.Linq/QueryableExtensions.cs @@ -36,6 +36,11 @@ public static Type GetHigherPrecedenceNumericType(Type a, Type b) throw new InvalidOperationException($"Cannot determine precedence between {a} and {b}"); } + public static bool IsGuidType(Type type) + { + return type == typeof(Guid); + } + /// /// Builds a LINQ Lambda Expression from the UCAST tree, and then invokes /// it under a LINQ Where expression on some queryable data source.
@@ -105,14 +110,18 @@ private static Expression BuildFieldExpression(UCASTNode node, ParameterExpre ///
/// The LINQ /// Current UCAST node in the conditions tree. - /// Derefered property lookup expression on the LINQ data source. + /// Deferred property lookup expression on the LINQ data source. /// Dictionary mapping UCAST property names to lambdas that generate LINQ Expressions. - /// Result, a LINQ Expression (Usually a BinaryExpression). + /// Result, a LINQ BinaryExpression. /// Thrown when arguments are of incompatible types. - private static Expression BuildFieldExpressionFromProperty(UCASTNode node, Expression property, MappingConfiguration mapper) + private static BinaryExpression BuildFieldExpressionFromProperty(UCASTNode node, Expression property, MappingConfiguration mapper) { Expression value = Expression.Constant(node.Value); + // If there is a type mismatch in an expression, it is usually from + // differing numeric types, or from things like a GUID vs String + // comparison. We try to make smart conversions here to ensure types + // are matched for the BinaryExpression result. Type lhsType = property.Type; Type rhsType = value.Type; if (lhsType != rhsType) @@ -130,6 +139,21 @@ private static Expression BuildFieldExpressionFromProperty(UCASTNode node, Ex value = Expression.Convert(value, exprType); } } + // Convert GUID strings automatically when the property is a Guid. + // Rego doesn't have native GUID typess, so it'll always be a + // GUID-formatted string that the policy is trying to match + // against the property. + else if (IsGuidType(lhsType) && rhsType == typeof(string)) + { + if (Guid.TryParse(node.Value?.ToString(), out Guid guid)) + { + value = Expression.Constant(guid); + } + else + { + throw new ArgumentException($"Expected a GUID-formatted string, but got '{node.Value}'"); + } + } } // Switch expression: @@ -164,7 +188,6 @@ private static Expression BuildFieldInExpression(UCASTNode node, Expression p var eq = new UCASTNode("field", "eq", node.Field); var childValues = (List)node.Value; - // Iterate over all children, and determine highest-precedent type among // them. Convert LHS value if needed. RHS type conversions will happen // automatically during expression building later. diff --git a/test/OpenPolicyAgent.Ucast.Linq.Tests/UnitTest.cs b/test/OpenPolicyAgent.Ucast.Linq.Tests/UnitTest.cs index 0abd479..17bddf2 100644 --- a/test/OpenPolicyAgent.Ucast.Linq.Tests/UnitTest.cs +++ b/test/OpenPolicyAgent.Ucast.Linq.Tests/UnitTest.cs @@ -79,6 +79,7 @@ public static IEnumerable EqTestData() yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.id", Value = (long)2 }, testdata.Where(d => d.Id == 2).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.flood_stage", Value = true }, testdata.Where(d => d.FloodStage).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.water_level_meters", Value = 5.8 }, testdata.Where(d => d.WaterLevelMeters == 5.8).ToList() }; + yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.uuid", Value = "123e4567-e89b-12d3-a456-426614174000" }, testdata.Where(d => d.Uuid == new Guid("123e4567-e89b-12d3-a456-426614174000")).ToList() }; } public static IEnumerable NeTestData() @@ -89,6 +90,7 @@ public static IEnumerable NeTestData() yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.id", Value = (long)2 }, testdata.Where(d => d.Id != 2).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.flood_stage", Value = true }, testdata.Where(d => !d.FloodStage).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.water_level_meters", Value = 5.8 }, testdata.Where(d => d.WaterLevelMeters != 5.8).ToList() }; + yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.uuid", Value = "123e4567-e89b-12d3-a456-426614174000" }, testdata.Where(d => d.Uuid != new Guid("123e4567-e89b-12d3-a456-426614174000")).ToList() }; } public static IEnumerable GtTestData() @@ -128,6 +130,7 @@ public static IEnumerable InTestData() yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.id", Value = new List() { (long)2, (long)5 } }, testdata.Where(d => new List() { (long)2, (long)5 }.Contains((long)d.Id)).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.flood_stage", Value = new List() { true } }, testdata.Where(d => new List() { true }.Contains(d.FloodStage)).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.water_level_meters", Value = new List() { 2.5, 5.8 } }, testdata.Where(d => new List() { 2.5, 5.8 }.Contains(d.WaterLevelMeters)).ToList() }; + yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.uuid", Value = new List() { "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" } }, testdata.Where(d => new List() { new Guid("123e4567-e89b-12d3-a456-426614174000"), new Guid("123e4567-e89b-12d3-a456-426614174001") }.Contains(d.Uuid)).ToList() }; } } @@ -140,6 +143,7 @@ public static IEnumerable NinTestData() yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.id", Value = new List() { (long)2, (long)5 } }, testdata.Where(d => !new List() { (long)2, (long)5 }.Contains((long)d.Id)).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.flood_stage", Value = new List() { true } }, testdata.Where(d => !new List() { true }.Contains(d.FloodStage)).ToList() }; yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.water_level_meters", Value = new List() { 2.5, 5.8 } }, testdata.Where(d => !new List() { 2.5, 5.8 }.Contains(d.WaterLevelMeters)).ToList() }; + yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.uuid", Value = new List() { "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" } }, testdata.Where(d => !new List() { new Guid("123e4567-e89b-12d3-a456-426614174000"), new Guid("123e4567-e89b-12d3-a456-426614174001") }.Contains(d.Uuid)).ToList() }; } } } @@ -503,6 +507,7 @@ public class UnitTestDataSource public class HydrologyData { public int Id { get; set; } + public Guid Uuid { get; set; } public string? Name { get; set; } public DateTime LastUpdated { get; set; } public bool FloodStage { get; set; } @@ -513,11 +518,11 @@ public class HydrologyData public static List GetTestHydrologyData() { return [ - new HydrologyData { Id = 1, Name = "River Alpha", LastUpdated = new DateTime(2024, 12, 10, 8, 30, 0), FloodStage = false, WaterLevelMeters = 2.5, FlowRateMinute = 100.5 }, - new HydrologyData { Id = 2, Name = "Lake Beta", LastUpdated = new DateTime(2024, 12, 9, 15, 45, 0), FloodStage = true, WaterLevelMeters = 5.8, FlowRateMinute = null }, - new HydrologyData { Id = 3, Name = "Stream Gamma", LastUpdated = new DateTime(2024, 12, 8, 12, 0, 0), FloodStage = false, WaterLevelMeters = 0.75, FlowRateMinute = 25.3 }, - new HydrologyData { Id = 4, Name = "Reservoir Delta", LastUpdated = new DateTime(2024, 12, 7, 9, 15, 0), FloodStage = false, WaterLevelMeters = 15.2, FlowRateMinute = 500.0 }, - new HydrologyData { Id = 5, Name = null, LastUpdated = new DateTime(2024, 12, 6, 18, 30, 0), FloodStage = true, WaterLevelMeters = 3.1, FlowRateMinute = 75.8 } + new HydrologyData { Id = 1, Uuid = new Guid("123e4567-e89b-12d3-a456-426614174000"), Name = "River Alpha", LastUpdated = new DateTime(2024, 12, 10, 8, 30, 0), FloodStage = false, WaterLevelMeters = 2.5, FlowRateMinute = 100.5 }, + new HydrologyData { Id = 2, Uuid = new Guid("123e4567-e89b-12d3-a456-426614174001"), Name = "Lake Beta", LastUpdated = new DateTime(2024, 12, 9, 15, 45, 0), FloodStage = true, WaterLevelMeters = 5.8, FlowRateMinute = null }, + new HydrologyData { Id = 3, Uuid = new Guid("123e4567-e89b-12d3-a456-426614174002"), Name = "Stream Gamma", LastUpdated = new DateTime(2024, 12, 8, 12, 0, 0), FloodStage = false, WaterLevelMeters = 0.75, FlowRateMinute = 25.3 }, + new HydrologyData { Id = 4, Uuid = new Guid("123e4567-e89b-12d3-a456-426614174003"), Name = "Reservoir Delta", LastUpdated = new DateTime(2024, 12, 7, 9, 15, 0), FloodStage = false, WaterLevelMeters = 15.2, FlowRateMinute = 500.0 }, + new HydrologyData { Id = 5, Uuid = new Guid("123e4567-e89b-12d3-a456-426614174004"), Name = null, LastUpdated = new DateTime(2024, 12, 6, 18, 30, 0), FloodStage = true, WaterLevelMeters = 3.1, FlowRateMinute = 75.8 } ]; }