From d38f45c38e6a77d953a2cbc51782d9c023d146b2 Mon Sep 17 00:00:00 2001 From: woksin Date: Sun, 1 Nov 2026 22:09:45 +0100 Subject: [PATCH] Compare enum guard values using declared member numbers --- Documentation/architecture/guarded-actions.md | 3 ++- .../when_running_the_shared_corpus.cs | 10 +++++++ .../DotNET/Engine/Guards/GuardConditions.cs | 26 +++++++++++++----- .../when_round_tripping_stage_properties.cs | 16 ++++++++++- .../Model/Guards/ComparisonGuardCondition.cs | 11 ++++++-- .../engine/guards/evaluateGuardCondition.ts | 21 ++++++++++----- .../model/guards/ComparisonGuardCondition.ts | 3 ++- guarded-selection-fixtures.json | 27 +++++++++++++++++++ scene-model-shape.json | 2 +- 9 files changed, 99 insertions(+), 20 deletions(-) diff --git a/Documentation/architecture/guarded-actions.md b/Documentation/architecture/guarded-actions.md index 17f5e589..f5e93ec7 100644 --- a/Documentation/architecture/guarded-actions.md +++ b/Documentation/architecture/guarded-actions.md @@ -69,7 +69,8 @@ A comparison contains `kind: "comparison"`, a `left` subject reference, an `oper - Equality is type-strict. Missing fields never match. A **present null** matches a null literal with `Equal`; `NotEqual` involving null is false. - Ordering requires numeric values; strings are not coerced to numbers. - `Contains` supports a string substring or scalar array membership; `StartsWith` requires strings. String matching is case-sensitive. Opt-in strict operands narrow `Contains` to strings. -- An optional `enumeration` lists member names in ordinal order. A subject ordinal or member name resolves to the member name. Enum names compare case-insensitively; unknown names and out-of-range ordinals do not match. +- An optional `enumeration` lists member names in declaration order. An optional `enumerationNumbers` supplies the declared whole-number value for each entry, for example `"enumeration": ["open", "closed"], "enumerationNumbers": [1, 0]`. Both numeric subject values and numeric literals resolve through these numbers to the member name. Without `enumerationNumbers`, numbers resolve by position as before. String member names compare case-insensitively; unknown names and numbers do not match, including for `NotEqual`. +- When present, `enumerationNumbers` must be an array of distinct whole numbers with the same length as `enumeration`; otherwise the condition is unsupported, just like an invalid `enumeration`. - A logical node is `{"kind":"logical","operator":"And","left":condition,"right":condition}`, or uses `Or`. Both branches must be understood, even if one branch would determine the answer. Unknown operators, node kinds and legacy string guards fail closed with `SCENE-GUARD-001`. A guarded action renders a disabled button and diagnostic rather than using its fallback. There is no JavaScript expression execution. diff --git a/Source/DotNET/Engine.Specs/for_GuardedSelection/when_running_the_shared_corpus.cs b/Source/DotNET/Engine.Specs/for_GuardedSelection/when_running_the_shared_corpus.cs index 76f84739..49f1cbba 100644 --- a/Source/DotNET/Engine.Specs/for_GuardedSelection/when_running_the_shared_corpus.cs +++ b/Source/DotNET/Engine.Specs/for_GuardedSelection/when_running_the_shared_corpus.cs @@ -32,6 +32,16 @@ void should_evaluate_a_typed_enum_comparison() GuardConditions.Evaluate(condition, new { status = 1 }).ShouldEqual(true); } + [Fact] + void should_evaluate_a_typed_enum_comparison_with_declared_numbers() + { + var condition = new ComparisonGuardCondition(new("item.status"), GuardOperator.Equal, "open", ["open", "closed"]) + { + EnumerationNumbers = [10, 20] + }; + GuardConditions.Evaluate(condition, new { status = 10 }).ShouldEqual(true); + } + [Fact] void should_evaluate_every_shared_predicate() { diff --git a/Source/DotNET/Engine/Guards/GuardConditions.cs b/Source/DotNET/Engine/Guards/GuardConditions.cs index dff5d361..a40fb0e1 100644 --- a/Source/DotNET/Engine/Guards/GuardConditions.cs +++ b/Source/DotNET/Engine/Guards/GuardConditions.cs @@ -59,17 +59,27 @@ static bool ValidComparison(JsonElement condition, string operation) if (!ValidPath(Text(Get(Get(condition, "left"), "subject")))) return false; if (!Enum.TryParse(operation, out var parsed)) return false; if (!Enum.IsDefined(parsed) || operation != parsed.ToString()) return false; - return ValidEnumeration(Get(condition, "enumeration")) && ValidLiteral(parsed, Get(condition, "right")); + return ValidEnumeration(Get(condition, "enumeration"), Get(condition, "enumerationNumbers")) && ValidLiteral(parsed, Get(condition, "right")); } - static bool ValidEnumeration(JsonElement value) + static bool ValidEnumeration(JsonElement value, JsonElement enumerationNumbers) { + if (enumerationNumbers.ValueKind != JsonValueKind.Undefined && !ValidEnumerationNumbers(enumerationNumbers, value)) return false; if (value.ValueKind == JsonValueKind.Undefined) return true; if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() == 0) return false; if (value.EnumerateArray().Any(member => member.ValueKind != JsonValueKind.String)) return false; return value.EnumerateArray().Select(member => member.GetString()).Distinct().Count() == value.GetArrayLength(); } + static bool ValidEnumerationNumbers(JsonElement value, JsonElement enumeration) + { + if (value.ValueKind != JsonValueKind.Array || enumeration.ValueKind != JsonValueKind.Array) return false; + if (value.GetArrayLength() != enumeration.GetArrayLength() || !value.EnumerateArray().All(WholeNumber)) return false; + return value.EnumerateArray().Select(member => member.GetDouble()).Distinct().Count() == value.GetArrayLength(); + } + + static bool WholeNumber(JsonElement value) => value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out var number) && double.IsFinite(number) && number == Math.Truncate(number); + static bool ValidLiteral(GuardOperator operation, JsonElement right) { if (!Scalar(right)) return false; @@ -90,8 +100,9 @@ static JsonElement Resolve(JsonElement reference, JsonElement item) static bool Compare(JsonElement condition, JsonElement item, string operation) { var enumeration = Get(condition, "enumeration"); - var left = Normalize(Resolve(Get(condition, "left"), item), enumeration); - var right = Normalize(Get(condition, "right"), enumeration); + var enumerationNumbers = Get(condition, "enumerationNumbers"); + var left = Normalize(Resolve(Get(condition, "left"), item), enumeration, enumerationNumbers); + var right = Normalize(Get(condition, "right"), enumeration, enumerationNumbers); if (left.ValueKind == JsonValueKind.Undefined || right.ValueKind == JsonValueKind.Undefined) return false; if (left.ValueKind == JsonValueKind.Null || right.ValueKind == JsonValueKind.Null) return operation == "Equal" && left.ValueKind == JsonValueKind.Null && right.ValueKind == JsonValueKind.Null; return CompareValues(left, right, operation); @@ -106,19 +117,20 @@ static bool Compare(JsonElement condition, JsonElement item, string operation) _ => Numeric(left, right, operation) }; - static JsonElement Normalize(JsonElement value, JsonElement enumeration) + static JsonElement Normalize(JsonElement value, JsonElement enumeration, JsonElement enumerationNumbers) { if (enumeration.ValueKind != JsonValueKind.Array || value.ValueKind == JsonValueKind.Null) return value; - if (Ordinal(value, enumeration.GetArrayLength(), out var ordinal)) return enumeration[ordinal]; + if (Ordinal(value, enumerationNumbers, enumeration.GetArrayLength(), out var ordinal)) return enumeration[ordinal]; if (value.ValueKind == JsonValueKind.String) return enumeration.EnumerateArray().FirstOrDefault(member => string.Equals(member.GetString(), value.GetString(), StringComparison.OrdinalIgnoreCase)); return default; } - static bool Ordinal(JsonElement value, int length, out int ordinal) + static bool Ordinal(JsonElement value, JsonElement enumerationNumbers, int length, out int ordinal) { ordinal = 0; if (value.ValueKind != JsonValueKind.Number) return false; var number = value.GetDouble(); + if (enumerationNumbers.ValueKind == JsonValueKind.Array) number = enumerationNumbers.EnumerateArray().Select(member => member.GetDouble()).ToList().IndexOf(number); if (number != Math.Truncate(number)) return false; if (number < 0 || number >= length) return false; ordinal = (int)number; diff --git a/Source/DotNET/Model.Specs/for_GuardedActionProperties/when_round_tripping_stage_properties.cs b/Source/DotNET/Model.Specs/for_GuardedActionProperties/when_round_tripping_stage_properties.cs index 068b2f0a..0d4f6693 100644 --- a/Source/DotNET/Model.Specs/for_GuardedActionProperties/when_round_tripping_stage_properties.cs +++ b/Source/DotNET/Model.Specs/for_GuardedActionProperties/when_round_tripping_stage_properties.cs @@ -11,7 +11,7 @@ namespace Cratis.Scene.Model.for_GuardedActionProperties; public class when_round_tripping_stage_properties : Specification { const string StageProperties = """ - {"label":"Approve","alternatives":[{"command":"Approve","condition":{"kind":"logical","operator":"And","left":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"Pending","enumeration":["Pending","Approved"]},"right":{"kind":"comparison","left":{"subject":"item.amount"},"operator":"GreaterThan","right":0}},"arguments":{"id":{"subject":"item.id"}}}],"otherwise":{"outcome":"Hidden"},"navigateToScreen":"detail","navigateByParameter":"id"} + {"label":"Approve","alternatives":[{"command":"Approve","condition":{"kind":"logical","operator":"And","left":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"Pending","enumeration":["Pending","Approved"],"enumerationNumbers":[10,20]},"right":{"kind":"comparison","left":{"subject":"item.amount"},"operator":"GreaterThan","right":0}},"arguments":{"id":{"subject":"item.id"}}}],"otherwise":{"outcome":"Hidden"},"navigateToScreen":"detail","navigateByParameter":"id"} """; readonly JsonSerializerOptions _options = new(JsonSerializerDefaults.Web); GuardedActionProperties _properties = null!; @@ -53,10 +53,24 @@ void should_write_canonical_guard_enumeration_values() json.GetProperty("otherwise").GetProperty("outcome").GetString().ShouldEqual("Execute"); } + [Fact] + void should_preserve_the_four_member_constructor_and_deconstruction() + { + var (left, operation, right, enumeration) = new ComparisonGuardCondition(new("item.status"), GuardOperator.Equal, "Pending", ["Pending", "Approved"]) + { + EnumerationNumbers = [10, 20] + }; + left.Subject.ShouldEqual("item.status"); + operation.ShouldEqual(GuardOperator.Equal); + right.ShouldEqual("Pending"); + enumeration.ShouldEqual("Pending", "Approved"); + } + [Fact] void should_preserve_the_command() => _roundTrip.Alternatives[0].Command.ShouldEqual("Approve"); [Fact] void should_accept_stage_properties_without_a_subject_binding() => _roundTrip.Item.ShouldBeNull(); [Fact] void should_preserve_explicit_arguments() => _roundTrip.Alternatives[0].Arguments!["id"].Subject.ShouldEqual("item.id"); [Fact] void should_preserve_the_nested_guard() => _roundTrip.Alternatives[0].Condition.ShouldBeOfExactType(); + [Fact] void should_preserve_declared_enumeration_numbers() => ((ComparisonGuardCondition)((LogicalGuardCondition)_roundTrip.Alternatives[0].Condition).Left).EnumerationNumbers.ShouldEqual(10d, 20d); [Fact] void should_preserve_interaction_branches() => _interaction.GetProperty("alternatives").GetArrayLength().ShouldEqual(1); [Fact] void should_preserve_an_empty_fallback() => _interaction.GetProperty("otherwise").GetArrayLength().ShouldEqual(0); } diff --git a/Source/DotNET/Model/Guards/ComparisonGuardCondition.cs b/Source/DotNET/Model/Guards/ComparisonGuardCondition.cs index 5400244f..cfb0c1b8 100644 --- a/Source/DotNET/Model/Guards/ComparisonGuardCondition.cs +++ b/Source/DotNET/Model/Guards/ComparisonGuardCondition.cs @@ -11,9 +11,16 @@ namespace Cratis.Scene.Model.Guards; /// The subject field. /// The comparison. /// A string, number, boolean or null literal. -/// The member names, in ordinal order. +/// The member names, in declaration order. public record ComparisonGuardCondition( SubjectReference Left, GuardOperator Operator, [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] object? Right, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList? Enumeration = null) : GuardCondition; + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] IReadOnlyList? Enumeration = null) : GuardCondition +{ + /// + /// Gets the distinct whole-number value for each member; absent means positional numbering. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? EnumerationNumbers { get; init; } +} diff --git a/Source/JavaScript/engine/guards/evaluateGuardCondition.ts b/Source/JavaScript/engine/guards/evaluateGuardCondition.ts index e5f9ee58..2de55239 100644 --- a/Source/JavaScript/engine/guards/evaluateGuardCondition.ts +++ b/Source/JavaScript/engine/guards/evaluateGuardCondition.ts @@ -60,12 +60,18 @@ export function resolveSubjectReference(reference: SubjectReference, item: unkno return value; } -function validEnumeration(value: unknown): boolean { +function validEnumeration(value: unknown, enumerationNumbers: unknown): boolean { + if (enumerationNumbers !== undefined && !validEnumerationNumbers(enumerationNumbers, value)) return false; if (value === undefined) return true; if (!Array.isArray(value) || value.length === 0) return false; return value.every(member => typeof member === 'string') && new Set(value).size === value.length; } +function validEnumerationNumbers(value: unknown, enumeration: unknown): boolean { + if (!Array.isArray(value) || !Array.isArray(enumeration)) return false; + return value.length === enumeration.length && value.every(Number.isInteger) && new Set(value).size === value.length; +} + function strictContains(operator: unknown, options?: GuardEvaluationOptions): boolean { return operator === 'Contains' && options?.strictOperands === true; } @@ -85,7 +91,7 @@ function validComparison(condition: Record, options?: GuardEval if (!subjectPath(condition.left)) return false; if (typeof condition.operator !== 'string') return false; if (!Object.hasOwn(comparisons, condition.operator)) return false; - return validEnumeration(condition.enumeration) && validLiteral(condition.operator, condition.right, options); + return validEnumeration(condition.enumeration, condition.enumerationNumbers) && validLiteral(condition.operator, condition.right, options); } export function canEvaluateGuard(condition: unknown, options?: GuardEvaluationOptions): boolean { @@ -101,9 +107,10 @@ function ordinal(value: unknown, length: number): value is number { return value >= 0 && value < length; } -function normalize(value: unknown, enumeration: unknown): unknown { +function normalize(value: unknown, enumeration: unknown, enumerationNumbers: unknown): unknown { if (!Array.isArray(enumeration) || value === null) return value; - if (ordinal(value, enumeration.length)) return enumeration[value]; + const index = typeof value === 'number' && Array.isArray(enumerationNumbers) ? enumerationNumbers.indexOf(value) : value; + if (ordinal(index, enumeration.length)) return enumeration[index]; if (typeof value === 'string') return enumeration.find(member => member.toLowerCase() === value.toLowerCase()); return undefined; } @@ -115,10 +122,10 @@ function unmatchedOperands(left: unknown, right: unknown): boolean { function compare(condition: Record, item: unknown, options?: GuardEvaluationOptions): boolean | undefined { const value = resolveSubjectReference(condition.left as SubjectReference, item); if (value === undefined) return false; - if (value === null) return condition.operator === 'Equal' && value === normalize(condition.right, condition.enumeration); + if (value === null) return condition.operator === 'Equal' && value === normalize(condition.right, condition.enumeration, condition.enumerationNumbers); if (strictContains(condition.operator, options) && !stringOperands(value, condition.right)) return undefined; - const right = normalize(condition.right, condition.enumeration); - const left = normalize(value, condition.enumeration); + const right = normalize(condition.right, condition.enumeration, condition.enumerationNumbers); + const left = normalize(value, condition.enumeration, condition.enumerationNumbers); if (unmatchedOperands(left, right)) return false; return comparisons[condition.operator as string](left, right); } diff --git a/Source/JavaScript/model/guards/ComparisonGuardCondition.ts b/Source/JavaScript/model/guards/ComparisonGuardCondition.ts index a4c11bed..6e7f0d3d 100644 --- a/Source/JavaScript/model/guards/ComparisonGuardCondition.ts +++ b/Source/JavaScript/model/guards/ComparisonGuardCondition.ts @@ -10,6 +10,7 @@ export interface ComparisonGuardCondition { operator: GuardOperator; right: string | number | boolean | null; enumeration?: string[]; + enumerationNumbers?: number[]; } -export const ComparisonGuardConditionPropertyNames: (keyof ComparisonGuardCondition)[] = ['left', 'operator', 'right', 'enumeration']; +export const ComparisonGuardConditionPropertyNames: (keyof ComparisonGuardCondition)[] = ['left', 'operator', 'right', 'enumeration', 'enumerationNumbers']; diff --git a/guarded-selection-fixtures.json b/guarded-selection-fixtures.json index 557ae203..cbd3dd36 100644 --- a/guarded-selection-fixtures.json +++ b/guarded-selection-fixtures.json @@ -45,6 +45,29 @@ {"name":"enum ordinal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"Approved","enumeration":["Pending","Approved"]}, "item":{"status":1}, "evaluable":true,"result":true}, {"name":"enum invalid ordinal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"NotEqual","right":"Approved","enumeration":["Pending","Approved"]}, "item":{"status":-1}, "evaluable":true,"result":false}, {"name":"enum invalid name", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"NotEqual","right":"Approved","enumeration":["Pending","Approved"]}, "item":{"status":"Unknown"}, "evaluable":true,"result":false}, + {"name":"enum reordered number is not closed", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[1,0]}, "item":{"status":1}, "evaluable":true,"result":false}, + {"name":"enum reordered number is open", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"NotEqual","right":"open","enumeration":["open","closed"],"enumerationNumbers":[1,0]}, "item":{"status":1}, "evaluable":true,"result":false}, + {"name":"enum reordered zero is closed", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[1,0]}, "item":{"status":0}, "evaluable":true,"result":true}, + {"name":"enum sparse open number", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":10}, "evaluable":true,"result":true}, + {"name":"enum sparse closed number", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":20}, "evaluable":true,"result":true}, + {"name":"enum sparse unknown does not match open", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":1}, "evaluable":true,"result":false}, + {"name":"enum sparse unknown does not match closed", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":1}, "evaluable":true,"result":false}, + {"name":"enum sparse unknown does not satisfy inequality", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"NotEqual","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":1}, "evaluable":true,"result":false}, + {"name":"enum reordered numeric literal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":0,"enumeration":["open","closed"],"enumerationNumbers":[1,0]}, "item":{"status":"closed"}, "evaluable":true,"result":true}, + {"name":"enum sparse numeric literal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":10,"enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":"open"}, "evaluable":true,"result":true}, + {"name":"enum unknown numeric literal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"NotEqual","right":1,"enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":"open"}, "evaluable":true,"result":false}, + {"name":"enum numbered names ignore case", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"CLOSED","enumeration":["open","closed"],"enumerationNumbers":[10,20]}, "item":{"status":"Closed"}, "evaluable":true,"result":true}, + {"name":"enum negative member number", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[-10,20]}, "item":{"status":-10}, "evaluable":true,"result":true}, + {"name":"enum duplicate numbers with numeric subject", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum duplicate numbers with numeric literal", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":10,"enumeration":["open","closed"],"enumerationNumbers":[10,10]}, "item":{"status":"open"}, "evaluable":false,"result":null}, + {"name":"enum duplicate numbers with string operands", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}, "item":{"status":"open"}, "evaluable":false,"result":null}, + {"name":"enum numbers length mismatch", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[10]}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum numbers empty", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[]}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum numbers fractional", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":[1.5,20]}, "item":{"status":1.5}, "evaluable":false,"result":null}, + {"name":"enum numbers nonnumeric member", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":["10",20]}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum numbers not an array", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":10}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum numbers null", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"open","enumeration":["open","closed"],"enumerationNumbers":null}, "item":{"status":10}, "evaluable":false,"result":null}, + {"name":"enum numbers without names", "condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":10,"enumerationNumbers":[10,20]}, "item":{"status":10}, "evaluable":false,"result":null}, {"name":"and", "condition":{"kind":"logical","operator":"And","left":{"kind":"comparison","left":{"subject":"item.value"},"operator":"Equal","right":2},"right":{"kind":"comparison","left":{"subject":"item.value"},"operator":"GreaterThan","right":1}}, "item":{"value":2}, "evaluable":true,"result":true}, {"name":"or", "condition":{"kind":"logical","operator":"Or","left":{"kind":"comparison","left":{"subject":"item.value"},"operator":"Equal","right":1},"right":{"kind":"comparison","left":{"subject":"item.value"},"operator":"Equal","right":2}}, "item":{"value":2}, "evaluable":true,"result":true}, {"name":"unknown in unselected logical branch", "condition":{"kind":"logical","operator":"Or","left":{"kind":"comparison","left":{"subject":"item.value"},"operator":"Equal","right":2},"right":{"kind":"script"}}, "item":{"value":2}, "evaluable":false,"result":null} @@ -52,6 +75,8 @@ "interactions": [ {"name":"unmatched without otherwise", "item":{}, "alternatives":[],"hasOtherwise":false,"expected":{"kind":"unmatched"}}, {"name":"unmatched with otherwise", "item":{}, "alternatives":[],"hasOtherwise":true,"expected":{"kind":"otherwise"}}, + {"name":"enum duplicate numbers with numeric subject cannot select otherwise", "item":{"status":10}, "alternatives":[{"condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}}],"hasOtherwise":true,"expected":{"kind":"unevaluable"}}, + {"name":"enum duplicate numbers with string operands cannot select otherwise", "item":{"status":"open"}, "alternatives":[{"condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}}],"hasOtherwise":true,"expected":{"kind":"unevaluable"}}, {"name":"no subject with otherwise", "item":null, "alternatives":[],"hasOtherwise":true,"expected":{"kind":"noSubject"}} ], "selections": [ @@ -60,6 +85,8 @@ {"name":"hidden fallback", "item":{}, "alternatives":[],"otherwise":{"outcome":"Hidden"},"expected":{"kind":"hidden"}}, {"name":"execute fallback", "item":{}, "alternatives":[],"otherwise":{"outcome":"Execute","command":"Fallback"},"expected":{"kind":"otherwise"}}, {"name":"unknown cannot select fallback", "item":{}, "alternatives":[{"condition":{"kind":"script"}}],"otherwise":{"outcome":"Execute","command":"Fallback"},"expected":{"kind":"unevaluable"}}, + {"name":"enum duplicate numbers with numeric subject cannot select fallback", "item":{"status":10}, "alternatives":[{"condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}}],"otherwise":{"outcome":"Execute","command":"Fallback"},"expected":{"kind":"unevaluable"}}, + {"name":"enum duplicate numbers with string operands cannot select fallback", "item":{"status":"open"}, "alternatives":[{"condition":{"kind":"comparison","left":{"subject":"item.status"},"operator":"Equal","right":"closed","enumeration":["open","closed"],"enumerationNumbers":[10,10]}}],"otherwise":{"outcome":"Execute","command":"Fallback"},"expected":{"kind":"unevaluable"}}, {"name":"missing subject cannot select fallback", "item":null, "alternatives":[],"otherwise":{"outcome":"Execute","command":"Fallback"},"expected":{"kind":"noSubject"}} ] } diff --git a/scene-model-shape.json b/scene-model-shape.json index 20c123a3..dec45d8b 100644 --- a/scene-model-shape.json +++ b/scene-model-shape.json @@ -3,7 +3,7 @@ "types": { "GuardCondition": [], "SubjectReference": ["subject"], - "ComparisonGuardCondition": ["left", "operator", "right", "enumeration"], + "ComparisonGuardCondition": ["left", "operator", "right", "enumeration", "enumerationNumbers"], "LogicalGuardCondition": ["left", "operator", "right"], "GuardedActionAlternative": ["command", "condition", "arguments"], "GuardedActionFallback": ["outcome", "command", "arguments"],