Skip to content

Commit 2568ea5

Browse files
committed
QueryableExtensions: Add support for GUID types.
This commit adds support for automatically converting strings from a UCAST expression into GUID types when the LINQ object property we're matching aginst is a GUID. This helps in cases where a database model has a UUID/GUID column. Signed-off-by: Philip Conrad <philip@chariot-chaser.net>
1 parent 84817aa commit 2568ea5

File tree

2 files changed

+35
-7
lines changed

2 files changed

+35
-7
lines changed

src/OpenPolicyAgent.Ucast.Linq/QueryableExtensions.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public static Type GetHigherPrecedenceNumericType(Type a, Type b)
3636
throw new InvalidOperationException($"Cannot determine precedence between {a} and {b}");
3737
}
3838

39+
public static bool IsGuidType(Type type)
40+
{
41+
return type == typeof(Guid);
42+
}
43+
3944
/// <summary>
4045
/// Builds a LINQ Lambda Expression from the UCAST tree, and then invokes
4146
/// it under a LINQ Where expression on some queryable data source.<br />
@@ -105,14 +110,18 @@ private static Expression BuildFieldExpression<T>(UCASTNode node, ParameterExpre
105110
/// </summary>
106111
/// <typeparam name="T">The LINQ</typeparam>
107112
/// <param name="node">Current UCAST node in the conditions tree.</param>
108-
/// <param name="property">Derefered property lookup expression on the LINQ data source.</param>
113+
/// <param name="property">Deferred property lookup expression on the LINQ data source.</param>
109114
/// <param name="mapper">Dictionary mapping UCAST property names to lambdas that generate LINQ Expressions.</param>
110115
/// <returns>Result, a LINQ BinaryExpression.</returns>
111116
/// <exception cref="ArgumentException">Thrown when arguments are of incompatible types.</exception>
112117
private static BinaryExpression BuildFieldExpressionFromProperty<T>(UCASTNode node, Expression property, MappingConfiguration<T> mapper)
113118
{
114119
Expression value = Expression.Constant(node.Value);
115120

121+
// If there is a type mismatch in an expression, it is usually from
122+
// differing numeric types, or from things like a GUID vs String
123+
// comparison. We try to make smart conversions here to ensure types
124+
// are matched for the BinaryExpression result.
116125
Type lhsType = property.Type;
117126
Type rhsType = value.Type;
118127
if (lhsType != rhsType)
@@ -130,6 +139,21 @@ private static BinaryExpression BuildFieldExpressionFromProperty<T>(UCASTNode no
130139
value = Expression.Convert(value, exprType);
131140
}
132141
}
142+
// Convert GUID strings automatically when the property is a Guid.
143+
// Rego doesn't have native GUID typess, so it'll always be a
144+
// GUID-formatted string that the policy is trying to match
145+
// against the property.
146+
else if (IsGuidType(lhsType) && rhsType == typeof(string))
147+
{
148+
if (Guid.TryParse(node.Value?.ToString(), out Guid guid))
149+
{
150+
value = Expression.Constant(guid);
151+
}
152+
else
153+
{
154+
throw new ArgumentException($"Expected a GUID-formatted string, but got '{node.Value}'");
155+
}
156+
}
133157
}
134158

135159
// Switch expression:
@@ -164,7 +188,6 @@ private static Expression BuildFieldInExpression<T>(UCASTNode node, Expression p
164188
var eq = new UCASTNode("field", "eq", node.Field);
165189
var childValues = (List<object>)node.Value;
166190

167-
168191
// Iterate over all children, and determine highest-precedent type among
169192
// them. Convert LHS value if needed. RHS type conversions will happen
170193
// automatically during expression building later.

test/OpenPolicyAgent.Ucast.Linq.Tests/UnitTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public static IEnumerable<object[]> EqTestData()
7979
yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.id", Value = (long)2 }, testdata.Where(d => d.Id == 2).ToList() };
8080
yield return new object[] { new UCASTNode { Type = "field", Op = "eq", Field = "data.flood_stage", Value = true }, testdata.Where(d => d.FloodStage).ToList() };
8181
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() };
82+
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() };
8283
}
8384

8485
public static IEnumerable<object[]> NeTestData()
@@ -89,6 +90,7 @@ public static IEnumerable<object[]> NeTestData()
8990
yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.id", Value = (long)2 }, testdata.Where(d => d.Id != 2).ToList() };
9091
yield return new object[] { new UCASTNode { Type = "field", Op = "ne", Field = "data.flood_stage", Value = true }, testdata.Where(d => !d.FloodStage).ToList() };
9192
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() };
93+
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() };
9294
}
9395

9496
public static IEnumerable<object[]> GtTestData()
@@ -128,6 +130,7 @@ public static IEnumerable<object[]> InTestData()
128130
yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.id", Value = new List<object>() { (long)2, (long)5 } }, testdata.Where(d => new List<object>() { (long)2, (long)5 }.Contains((long)d.Id)).ToList() };
129131
yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.flood_stage", Value = new List<object>() { true } }, testdata.Where(d => new List<object>() { true }.Contains(d.FloodStage)).ToList() };
130132
yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.water_level_meters", Value = new List<object>() { 2.5, 5.8 } }, testdata.Where(d => new List<object>() { 2.5, 5.8 }.Contains(d.WaterLevelMeters)).ToList() };
133+
yield return new object[] { new UCASTNode { Type = "field", Op = "in", Field = "data.uuid", Value = new List<object>() { "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" } }, testdata.Where(d => new List<object>() { new Guid("123e4567-e89b-12d3-a456-426614174000"), new Guid("123e4567-e89b-12d3-a456-426614174001") }.Contains(d.Uuid)).ToList() };
131134
}
132135
}
133136

@@ -140,6 +143,7 @@ public static IEnumerable<object[]> NinTestData()
140143
yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.id", Value = new List<object>() { (long)2, (long)5 } }, testdata.Where(d => !new List<object>() { (long)2, (long)5 }.Contains((long)d.Id)).ToList() };
141144
yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.flood_stage", Value = new List<object>() { true } }, testdata.Where(d => !new List<object>() { true }.Contains(d.FloodStage)).ToList() };
142145
yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.water_level_meters", Value = new List<object>() { 2.5, 5.8 } }, testdata.Where(d => !new List<object>() { 2.5, 5.8 }.Contains(d.WaterLevelMeters)).ToList() };
146+
yield return new object[] { new UCASTNode { Type = "field", Op = "nin", Field = "data.uuid", Value = new List<object>() { "123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001" } }, testdata.Where(d => !new List<object>() { new Guid("123e4567-e89b-12d3-a456-426614174000"), new Guid("123e4567-e89b-12d3-a456-426614174001") }.Contains(d.Uuid)).ToList() };
143147
}
144148
}
145149
}
@@ -503,6 +507,7 @@ public class UnitTestDataSource
503507
public class HydrologyData
504508
{
505509
public int Id { get; set; }
510+
public Guid Uuid { get; set; }
506511
public string? Name { get; set; }
507512
public DateTime LastUpdated { get; set; }
508513
public bool FloodStage { get; set; }
@@ -513,11 +518,11 @@ public class HydrologyData
513518
public static List<HydrologyData> GetTestHydrologyData()
514519
{
515520
return [
516-
new HydrologyData { Id = 1, Name = "River Alpha", LastUpdated = new DateTime(2024, 12, 10, 8, 30, 0), FloodStage = false, WaterLevelMeters = 2.5, FlowRateMinute = 100.5 },
517-
new HydrologyData { Id = 2, Name = "Lake Beta", LastUpdated = new DateTime(2024, 12, 9, 15, 45, 0), FloodStage = true, WaterLevelMeters = 5.8, FlowRateMinute = null },
518-
new HydrologyData { Id = 3, Name = "Stream Gamma", LastUpdated = new DateTime(2024, 12, 8, 12, 0, 0), FloodStage = false, WaterLevelMeters = 0.75, FlowRateMinute = 25.3 },
519-
new HydrologyData { Id = 4, Name = "Reservoir Delta", LastUpdated = new DateTime(2024, 12, 7, 9, 15, 0), FloodStage = false, WaterLevelMeters = 15.2, FlowRateMinute = 500.0 },
520-
new HydrologyData { Id = 5, Name = null, LastUpdated = new DateTime(2024, 12, 6, 18, 30, 0), FloodStage = true, WaterLevelMeters = 3.1, FlowRateMinute = 75.8 }
521+
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 },
522+
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 },
523+
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 },
524+
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 },
525+
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 }
521526
];
522527
}
523528

0 commit comments

Comments
 (0)