diff --git a/README.md b/README.md index 2751eea..7628dc5 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,11 @@ Static omitted/default paths may intentionally read a root/default location. Dynamic pointer expressions that evaluate to `null` or undefined fail instead of silently becoming the current document scope or root value. +Dynamic text operands also reject `null` and undefined. This applies to +operator fields such as `$get.key`, `$objectSet.key`, `$binding.name`, +`$steps.step`, `$appendChange.op`, and `$pointerSet.op`. A static empty string +is still allowed when it is explicitly authored. + Use `$pointerJoin` when building document paths from dynamic path segments. Each item is treated as one JSON Pointer segment and escaped safely: @@ -379,8 +384,25 @@ $join: | `$gt`, `$gte`, `$lt`, `$lte` | Numeric comparisons. | | `$and`, `$or`, `$not` | Boolean logic with short-circuiting. | | `$truthy`, `$empty` | Truthiness checks. | +| `$exists` | Return false only for undefined values. | | `$coalesce` | First non-empty value. | +`$exists` is useful for optional-field validation because it distinguishes a +missing value from present falsy values. It returns `true` for `null`, `false`, +`0`, empty text, and empty list/object values: + +```yaml +$or: + - $not: + $exists: + $event: /message/request/note + - $is: + node: + $event: /message/request/note + pattern: + type: Text +``` + ### Numeric | Operator | Purpose | @@ -440,6 +462,29 @@ BEX-looking operators inside Blue type-definition fields such as `type`, | `$return` | Return the result value. | | `$fail` | Fail deterministically. | +`$forEach` can bind list indexes and object keys when those are needed for +patch paths: + +```yaml +$forEach: + in: + $event: /message/request/orders + item: order + index: i + do: + - $appendChange: + op: replace + path: + $pointerJoin: + - orders + - $var: i + - status + val: received +``` + +For object iteration, use `key` and `item` to bind the object key and value +separately. The older form with only `item` still binds `{ key, val }`. + ## Results And Accumulators `BexExecutionResult` contains: @@ -453,6 +498,13 @@ BEX-looking operators inside Blue type-definition fields such as `type`, BEX computes these values only. The host decides whether patches are applied, events are emitted, or accumulators are treated as ordinary data. +`$resultValue` reads the document value after applying accumulated patches in +order. Parent reads reflect descendant object patches, so reading +`/hotelOrder` after replacing `/hotelOrder/status` returns the original +`hotelOrder` object with the updated status. Current materialization supports +object paths and list index replacement. Array insertion/removal semantics are +not modeled as full JSON Patch array shifts. + `$appendChanges` validates each patch entry the same way as `$appendChange`. Supported patch operations are `add`, `replace`, and `remove`. `add` and `replace` require a non-undefined `val`; `remove` does not include a value. @@ -480,7 +532,7 @@ The engine is compiled-first: - static pointers are parsed at compile time; - document and binding reads use cursor-backed values where possible; - `$objectSet` and `$pointerSet` use overlay values; -- `$resultValue` uses an indexed overlay; +- `$resultValue` materializes accumulated patch overlays for reads; - output conversion to `Node`, `FrozenNode`, or simple Java values is explicit. Every result includes `BexMetrics`: diff --git a/build.gradle.kts b/build.gradle.kts index 73d1f11..583eaaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.yaml:snakeyaml:1.31") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/src/main/java/blue/bex/compile/BexCompiler.java b/src/main/java/blue/bex/compile/BexCompiler.java index 14f5c35..b35a10e 100644 --- a/src/main/java/blue/bex/compile/BexCompiler.java +++ b/src/main/java/blue/bex/compile/BexCompiler.java @@ -272,9 +272,15 @@ private CompiledStatement compileStatement(FrozenNode statement, CompileScope sc compileStatements(prop(body, "then"), scope, bodyPointer + "/then"), compileStatements(prop(body, "else"), scope, bodyPointer + "/else")); } else if ("$forEach".equals(op)) { - int slot = scope.declareOrGetSlot(requiredText(prop(body, "item"), "$forEach.item")); + String itemName = requiredText(prop(body, "item"), "$forEach.item"); + String keyName = prop(body, "key") != null ? requiredText(prop(body, "key"), "$forEach.key") : null; + String indexName = prop(body, "index") != null ? requiredText(prop(body, "index"), "$forEach.index") : null; + validateDistinctForEachBindings(itemName, keyName, indexName); + int slot = scope.declareOrGetSlot(itemName); + int keySlot = keyName != null ? scope.declareOrGetSlot(keyName) : -1; + int indexSlot = indexName != null ? scope.declareOrGetSlot(indexName) : -1; compiled = new ForEachStatement(compileExpr(required(prop(body, "in"), "$forEach.in"), scope, bodyPointer + "/in"), - slot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do")); + slot, keySlot, indexSlot, compileStatements(prop(body, "do"), scope, bodyPointer + "/do")); } else if ("$appendChange".equals(op)) { compiled = new AppendChangeStatement(textOrExpr(required(prop(body, "op"), "$appendChange.op"), scope, null, bodyPointer + "/op"), pointerOperand(required(prop(body, "path"), "$appendChange.path"), scope, bodyPointer + "/path"), @@ -393,6 +399,7 @@ private CompiledExpression compileOperator(String op, FrozenNode body, CompileSc if ("$not".equals(op)) return new NotExpr(compileExpr(body, scope, pointer)); if ("$truthy".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.TRUTHY); if ("$empty".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EMPTY); + if ("$exists".equals(op)) return new UnaryExpr(compileExpr(body, scope, pointer), UnaryOp.EXISTS); if ("$coalesce".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); if ("$default".equals(op)) return new CoalesceExpr(compileExprList(body, scope, pointer)); if ("$add".equals(op)) return new NumericExpr(compileExprList(body, scope, pointer), NumericOp.ADD); @@ -531,7 +538,7 @@ private TextOperand textOrExpr(FrozenNode node, CompileScope scope, String defau if (node.getValue() != null && node.getProperties() == null && node.getItems() == null) { return new StaticTextExpr(String.valueOf(node.getValue())); } - return new DynamicTextExpr(compileExpr(node, scope, pointer)); + return new DynamicTextExpr(compileExpr(node, scope, pointer), pointer); } private PointerOperand pointerOperand(FrozenNode node, CompileScope scope, String pointer) { @@ -671,6 +678,18 @@ private CompiledStatement sourceStatement(String functionName, String pointer, S return new SourceStatement(BexSourcePath.of(functionName, pointer, operator), statement); } + private void validateDistinctForEachBindings(String itemName, String keyName, String indexName) { + if (keyName != null && keyName.equals(itemName)) { + throw new BexException("$forEach.key must use a different binding name than $forEach.item"); + } + if (indexName != null && indexName.equals(itemName)) { + throw new BexException("$forEach.index must use a different binding name than $forEach.item"); + } + if (keyName != null && indexName != null && keyName.equals(indexName)) { + throw new BexException("$forEach.key must use a different binding name than $forEach.index"); + } + } + private String escape(String segment) { return segment.replace("~", "~0").replace("/", "~1"); } diff --git a/src/main/java/blue/bex/compile/BexOperands.java b/src/main/java/blue/bex/compile/BexOperands.java index 8e473a1..b77a0c1 100644 --- a/src/main/java/blue/bex/compile/BexOperands.java +++ b/src/main/java/blue/bex/compile/BexOperands.java @@ -33,14 +33,16 @@ public String get(CompiledFrame frame) { final class DynamicTextExpr implements TextOperand { private final CompiledExpression expr; + private final String label; - DynamicTextExpr(CompiledExpression expr) { + DynamicTextExpr(CompiledExpression expr, String label) { this.expr = expr; + this.label = label; } @Override public String get(CompiledFrame frame) { - return expr.eval(frame).asText(); + return TextOperands.text(expr.eval(frame), label); } } @@ -173,3 +175,15 @@ static String pointerText(BexValue value) { return value.asText(); } } + +final class TextOperands { + private TextOperands() { + } + + static String text(BexValue value, String label) { + if (value == null || value.isUndefined() || value.isNull()) { + throw new BexException(label + " cannot be null or undefined"); + } + return value.asText(); + } +} diff --git a/src/main/java/blue/bex/compile/BexStatements.java b/src/main/java/blue/bex/compile/BexStatements.java index bbc49df..ed11707 100644 --- a/src/main/java/blue/bex/compile/BexStatements.java +++ b/src/main/java/blue/bex/compile/BexStatements.java @@ -10,6 +10,7 @@ import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import java.math.BigInteger; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -104,11 +105,15 @@ protected Control doExec(CompiledFrame frame) { final class ForEachStatement extends Stmt { private final CompiledExpression input; private final int itemSlot; + private final int keySlot; + private final int indexSlot; private final List body; - ForEachStatement(CompiledExpression input, int itemSlot, List body) { + ForEachStatement(CompiledExpression input, int itemSlot, int keySlot, int indexSlot, List body) { this.input = input; this.itemSlot = itemSlot; + this.keySlot = keySlot; + this.indexSlot = indexSlot; this.body = body; } @@ -119,10 +124,18 @@ protected Control doExec(CompiledFrame frame) { for (String key : value.keys()) { frame.runtime().metrics().incrementLoopIterations(); frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); - Map entry = new LinkedHashMap<>(); - entry.put("key", BexValues.scalar(key)); - entry.put("val", value.get(key)); - frame.set(itemSlot, BexValues.map(entry)); + if (keySlot >= 0) { + frame.set(keySlot, BexValues.scalar(key)); + frame.set(itemSlot, value.get(key)); + } else { + Map entry = new LinkedHashMap<>(); + entry.put("key", BexValues.scalar(key)); + entry.put("val", value.get(key)); + frame.set(itemSlot, BexValues.map(entry)); + } + if (indexSlot >= 0) { + frame.set(indexSlot, BexValues.undefined()); + } for (CompiledStatement statement : body) { if (statement.exec(frame) == Control.RETURN) return Control.RETURN; } @@ -132,6 +145,12 @@ protected Control doExec(CompiledFrame frame) { frame.runtime().metrics().incrementLoopIterations(); frame.runtime().gas().charge(frame.runtime().gas().schedule().forEachItem); frame.set(itemSlot, value.get(String.valueOf(i))); + if (indexSlot >= 0) { + frame.set(indexSlot, BexValues.scalar(BigInteger.valueOf(i))); + } + if (keySlot >= 0) { + frame.set(keySlot, BexValues.undefined()); + } for (CompiledStatement statement : body) { if (statement.exec(frame) == Control.RETURN) return Control.RETURN; } diff --git a/src/main/java/blue/bex/compile/TypeStringExpressions.java b/src/main/java/blue/bex/compile/TypeStringExpressions.java index 05891e4..8b0b302 100644 --- a/src/main/java/blue/bex/compile/TypeStringExpressions.java +++ b/src/main/java/blue/bex/compile/TypeStringExpressions.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.regex.Pattern; -enum UnaryOp { UNWRAP, TEXT, INTEGER, NUMBER, BOOLEAN, OBJECT, LIST, TRUTHY, EMPTY, KEYS, ENTRIES, SIZE } +enum UnaryOp { UNWRAP, TEXT, INTEGER, NUMBER, BOOLEAN, OBJECT, LIST, TRUTHY, EMPTY, EXISTS, KEYS, ENTRIES, SIZE } final class UnaryExpr extends Expr { private final CompiledExpression expression; @@ -55,6 +55,8 @@ protected BexValue doEval(CompiledFrame frame) { return BexValues.scalar(BexValues.truthy(value)); case EMPTY: return BexValues.scalar(BexValues.empty(value)); + case EXISTS: + return BexValues.scalar(!value.isUndefined()); case KEYS: List keys = new ArrayList<>(); for (String key : value.keys()) keys.add(BexValues.scalar(key)); diff --git a/src/main/java/blue/bex/result/BexResultOverlay.java b/src/main/java/blue/bex/result/BexResultOverlay.java index 7b81dbe..29ba732 100644 --- a/src/main/java/blue/bex/result/BexResultOverlay.java +++ b/src/main/java/blue/bex/result/BexResultOverlay.java @@ -6,17 +6,14 @@ import blue.language.utils.JsonPointer; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; /** - * Indexed overlay implementing observable reverse-scan $resultValue semantics. + * Ordered overlay materializing accumulated patch effects for $resultValue reads. */ public final class BexResultOverlay { private final BexDocumentView document; private final List entries = new ArrayList<>(); - private final Map latestByPath = new LinkedHashMap<>(); private final BexMetrics metrics; public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { @@ -26,7 +23,6 @@ public BexResultOverlay(BexDocumentView document, BexMetrics metrics) { public void append(BexPatchEntry entry) { entries.add(entry); - latestByPath.put(entry.absolutePath(), entry); } public BexValue valueAt(String absolutePointer, List segments) { @@ -34,35 +30,64 @@ public BexValue valueAt(String absolutePointer, List segments) { metrics.incrementResultValueReads(); } String pointer = JsonPointer.canonicalize(absolutePointer); - BexPatchEntry exact = latestByPath.get(pointer); - if (exact != null) { - if (metrics != null) { - metrics.incrementResultOverlayExactHits(); - } - return "remove".equals(exact.op()) ? BexValues.undefined() : exact.val(); - } List selected = segments != null ? segments : JsonPointer.split(pointer); - for (int length = selected.size() - 1; length >= 0; length--) { - String ancestorPointer = JsonPointer.toPointer(selected.subList(0, length)); - BexPatchEntry ancestor = latestByPath.get(ancestorPointer); - if (ancestor == null) { - continue; - } - if ("remove".equals(ancestor.op())) { - if (metrics != null) { - metrics.incrementResultOverlayAncestorHits(); - } - return BexValues.undefined(); - } - BexValue value = ancestor.val().at(selected.subList(length, selected.size())); - if (metrics != null) { - metrics.incrementResultOverlayAncestorHits(); + recordOverlayMetric(pointer, selected); + if (entries.isEmpty()) { + return document.canonicalAt(pointer); + } + BexValue materialized = document.canonicalAt("/"); + for (BexPatchEntry entry : entries) { + materialized = apply(materialized, entry); + } + return materialized.at(selected); + } + + private BexValue apply(BexValue root, BexPatchEntry entry) { + if (entry.absoluteSegments().isEmpty()) { + return "remove".equals(entry.op()) ? BexValues.undefined() : entry.val(); + } + return BexValues.pointerSet(root, entry.absoluteSegments(), entry.val(), + "remove".equals(entry.op()) ? "remove" : "set"); + } + + private void recordOverlayMetric(String pointer, List selected) { + if (metrics == null) { + return; + } + boolean exact = false; + boolean ancestor = false; + boolean descendant = false; + for (BexPatchEntry entry : entries) { + if (entry.absolutePath().equals(pointer)) { + exact = true; + } else if (isPrefix(entry.absoluteSegments(), selected)) { + ancestor = true; + } else if (isPrefix(selected, entry.absoluteSegments())) { + descendant = true; } - return value; } - if (metrics != null) { + if (exact) { + metrics.incrementResultOverlayExactHits(); + return; + } + if (ancestor) { + metrics.incrementResultOverlayAncestorHits(); + return; + } + if (!descendant) { metrics.incrementResultOverlayDocumentFallbacks(); } - return document.canonicalAt(pointer); + } + + private boolean isPrefix(List prefix, List segments) { + if (prefix.size() >= segments.size()) { + return false; + } + for (int i = 0; i < prefix.size(); i++) { + if (!prefix.get(i).equals(segments.get(i))) { + return false; + } + } + return true; } } diff --git a/src/test/java/blue/bex/BexRichFixtureTest.java b/src/test/java/blue/bex/BexRichFixtureTest.java new file mode 100644 index 0000000..0b612e1 --- /dev/null +++ b/src/test/java/blue/bex/BexRichFixtureTest.java @@ -0,0 +1,317 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.BexStepResults; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.compile.BexCompiledProgram; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexFrozenWriter; +import blue.bex.value.BexNodeWriter; +import blue.bex.value.BexValue; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class BexRichFixtureTest { + private static final String FIXTURE_ROOT = "rich-fixtures"; + private static final Blue YAML_BLUE = new Blue(); + private static final String TINY_EVENT_PROGRAM = String.join("\n", + "type: Blue/BEX Program", + "do:", + " - $appendEvent:", + " eventKind: Tiny", + " payload: x"); + + private final Blue blue = new Blue(blueId -> { + if ("HotelOrderType".equals(blueId)) { + return Collections.singletonList(YAML_BLUE.yamlToNode(String.join("\n", + "status:", + " type: Text"))); + } + if ("RestaurantOrderType".equals(blueId)) { + return Collections.singletonList(YAML_BLUE.yamlToNode(String.join("\n", + "restaurantStatus:", + " type: Text"))); + } + return Collections.emptyList(); + }); + private final BexEngine engine = BexEngine.builder().blue(blue).build(); + private final Yaml yaml = new Yaml(); + + @TestFactory + Collection richFixtures() throws Exception { + List paths = fixturePaths(); + List tests = new ArrayList<>(); + for (Path path : paths) { + tests.add(DynamicTest.dynamicTest(displayName(path), () -> runFixture(path))); + } + return tests; + } + + private void runFixture(Path path) throws Exception { + Map fixture = readFixture(path); + Map expectation = map(fixture.get("expectation")); + String outcome = string(expectation.get("outcome")); + assertNotNull(outcome, "Fixture outcome is required: " + path); + + if ("parse-error".equals(outcome)) { + RuntimeException ex = assertThrows(RuntimeException.class, () -> parseProgram(fixture)); + assertErrorContains(ex, expectation); + return; + } + if ("parse-error-or-output-conversion-error".equals(outcome)) { + assertParseOrOutputConversionError(fixture, expectation); + return; + } + + Node program = parseProgram(fixture); + BexProgramSource source = BexProgramSource.inline(FrozenNode.fromResolvedNode(program)); + BexExecutionContext context = context(fixture); + + if ("compile-error".equals(outcome)) { + BexException ex = assertThrows(BexException.class, () -> engine.compile(source)); + assertErrorContains(ex, expectation); + return; + } + + BexCompiledProgram compiled = engine.compile(source); + if ("runtime-error".equals(outcome)) { + BexException ex = assertThrows(BexException.class, () -> engine.execute(compiled, context)); + assertErrorContains(ex, expectation); + return; + } + if ("output-conversion-error".equals(outcome)) { + BexExecutionResult result = engine.execute(compiled, context); + assertOutputConversionError(result.value(), expectation); + return; + } + if ("gas-property".equals(outcome)) { + assertGasProperty(compiled, context, expectation); + return; + } + if (!"success".equals(outcome)) { + fail("Unsupported fixture outcome: " + outcome); + } + + BexExecutionResult result = engine.execute(compiled, context); + assertSuccessExpectations(result, expectation); + } + + private void assertParseOrOutputConversionError(Map fixture, Map expectation) { + Node program; + try { + program = parseProgram(fixture); + } catch (RuntimeException ex) { + assertErrorContains(ex, expectation); + return; + } + BexProgramSource source = BexProgramSource.inline(FrozenNode.fromResolvedNode(program)); + try { + BexExecutionResult result = engine.compileAndExecute(source, context(fixture)); + assertOutputConversionError(result.value(), expectation); + } catch (BexException ex) { + assertErrorContains(ex, expectation); + } + } + + private void assertOutputConversionError(BexValue value, Map expectation) { + BexException thrown = null; + try { + BexNodeWriter.toNode(value); + } catch (BexException ex) { + thrown = ex; + } + if (thrown == null) { + thrown = assertThrows(BexException.class, () -> BexFrozenWriter.toFrozen(value)); + } + assertErrorContains(thrown, expectation); + } + + private void assertGasProperty(BexCompiledProgram compiled, BexExecutionContext context, Map expectation) { + String property = string(expectation.get("property")); + if (!"gasUsedGreaterThanEquivalentTinyEvent".equals(property)) { + fail("Unsupported gas property: " + property); + } + long large = engine.execute(compiled, context).gasUsed(); + long tiny = engine.compileAndExecute(source(TINY_EVENT_PROGRAM), context).gasUsed(); + assertTrue(large > tiny, "Expected large output gas " + large + " to be greater than tiny output gas " + tiny); + } + + private void assertSuccessExpectations(BexExecutionResult result, Map expectation) { + if (expectation.containsKey("resultSimple")) { + assertEquals(normalize(expectation.get("resultSimple")), normalize(result.value().toSimple())); + } + if (expectation.containsKey("changeset")) { + assertEquals(normalize(expectation.get("changeset")), normalize(result.changeset().asValue().toSimple())); + } + if (expectation.containsKey("events")) { + assertEquals(normalize(expectation.get("events")), normalize(result.events().asValue().toSimple())); + } + } + + private BexExecutionContext context(Map fixture) { + Map context = map(fixture.get("context")); + String scope = string(context.get("documentScope")); + if (scope == null) { + scope = "/"; + } + Node root = parseNodeSource(string(context.get("rootDocumentSource"))); + Node event = parseNodeSource(string(context.get("eventSource"))); + Node currentContract = parseNodeSource(string(context.get("currentContractSource"))); + + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView(FrozenNode.fromResolvedNode(root), FrozenNode.fromResolvedNode(root), scope)) + .event(BexValues.nodeSnapshot(event)) + .currentContract(BexValues.nodeSnapshot(currentContract)) + .steps(steps(context.get("stepsBinding"))) + .gasLimit(1_000_000) + .build(); + } + + private BexStepResults steps(Object stepsObject) { + Map stepsMap = map(stepsObject); + BexStepResults.Builder builder = BexStepResults.builder(); + for (Map.Entry entry : stepsMap.entrySet()) { + builder.put(entry.getKey(), BexValues.fromSimple(normalize(entry.getValue()))); + } + return builder.build(); + } + + private Node parseProgram(Map fixture) { + return blue.yamlToNode(requiredString(fixture.get("programSource"), "programSource")); + } + + private Node parseNodeSource(String source) { + if (source == null || source.trim().isEmpty()) { + return blue.yamlToNode("{}"); + } + return blue.yamlToNode(source); + } + + private BexProgramSource source(String source) { + return BexProgramSource.inline(FrozenNode.fromResolvedNode(blue.yamlToNode(source))); + } + + @SuppressWarnings("unchecked") + private Map readFixture(Path path) throws IOException { + try (InputStream in = Files.newInputStream(path)) { + Object loaded = yaml.load(in); + if (!(loaded instanceof Map)) { + throw new IllegalArgumentException("Fixture must be a map: " + path); + } + return (Map) loaded; + } + } + + private List fixturePaths() throws URISyntaxException, IOException { + URL url = Thread.currentThread().getContextClassLoader().getResource(FIXTURE_ROOT); + assertNotNull(url, "Missing fixture resource root: " + FIXTURE_ROOT); + final Path root = Paths.get(url.toURI()); + List paths = new ArrayList<>(); + try (Stream stream = Files.walk(root)) { + stream.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".yaml")) + .forEach(paths::add); + } + Collections.sort(paths); + return paths; + } + + private String displayName(Path path) { + Path fileName = path.getFileName(); + return fileName != null ? fileName.toString() : path.toString(); + } + + private void assertErrorContains(Throwable ex, Map expectation) { + String expected = string(expectation.get("errorContains")); + if (expected == null || expected.isEmpty()) { + return; + } + String message = ex.getMessage(); + assertTrue(message != null && message.contains(expected), + "Expected error to contain <" + expected + "> but was <" + message + ">"); + } + + @SuppressWarnings("unchecked") + private Map map(Object value) { + if (value == null) { + return Collections.emptyMap(); + } + if (!(value instanceof Map)) { + throw new IllegalArgumentException("Expected map but found: " + value); + } + Map out = new LinkedHashMap<>(); + for (Map.Entry entry : ((Map) value).entrySet()) { + out.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return out; + } + + private String requiredString(Object value, String label) { + String text = string(value); + if (text == null) { + throw new IllegalArgumentException("Missing fixture field: " + label); + } + return text; + } + + private String string(Object value) { + return value != null ? String.valueOf(value) : null; + } + + @SuppressWarnings("unchecked") + private Object normalize(Object value) { + if (value instanceof Map) { + Map out = new LinkedHashMap<>(); + for (Map.Entry entry : ((Map) value).entrySet()) { + out.put(String.valueOf(entry.getKey()), normalize(entry.getValue())); + } + return out; + } + if (value instanceof List) { + List out = new ArrayList<>(); + for (Object item : (List) value) { + out.add(normalize(item)); + } + return out; + } + if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) { + return BigInteger.valueOf(((Number) value).longValue()); + } + if (value instanceof BigDecimal || value instanceof BigInteger || value instanceof String || value instanceof Boolean || value == null) { + return value; + } + if (value instanceof Float || value instanceof Double) { + return BigDecimal.valueOf(((Number) value).doubleValue()); + } + return value; + } +} diff --git a/src/test/java/blue/bex/BexUseCaseConformanceTest.java b/src/test/java/blue/bex/BexUseCaseConformanceTest.java new file mode 100644 index 0000000..f701bab --- /dev/null +++ b/src/test/java/blue/bex/BexUseCaseConformanceTest.java @@ -0,0 +1,357 @@ +package blue.bex; + +import blue.bex.api.BexEngine; +import blue.bex.api.BexExecutionContext; +import blue.bex.api.BexProgramSource; +import blue.bex.api.FrozenBexDocumentView; +import blue.bex.result.BexExecutionResult; +import blue.bex.value.BexValues; +import blue.language.Blue; +import blue.language.model.Node; +import blue.language.snapshot.FrozenNode; +import org.junit.jupiter.api.Test; + +import static blue.bex.test.BexTestFixtures.bi; +import static blue.bex.test.BexTestFixtures.defaultContext; +import static blue.bex.test.BexTestFixtures.frozen; +import static blue.bex.test.BexTestFixtures.l; +import static blue.bex.test.BexTestFixtures.list; +import static blue.bex.test.BexTestFixtures.m; +import static blue.bex.test.BexTestFixtures.obj; +import static blue.bex.test.BexTestFixtures.op; +import static blue.bex.test.BexTestFixtures.simple; +import static blue.bex.test.BexTestFixtures.stepDo; +import static blue.bex.test.BexTestFixtures.stepExpr; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BexUseCaseConformanceTest { + private final Blue blue = new Blue(); + private final BexEngine engine = BexEngine.builder().blue(blue).build(); + + @Test + void existsDistinguishesMissingFromPresentFalsyValues() { + assertEquals(false, simple(run(stepExpr(op("$exists", op("$document", "/missing"))), defaultContext()).value())); + assertEquals(true, simple(run(stepExpr(op("$exists", op("$literal", null))), defaultContext()).value())); + assertEquals(true, simple(run(stepExpr(op("$exists", "")), defaultContext()).value())); + assertEquals(true, simple(run(stepExpr(op("$exists", false)), defaultContext()).value())); + assertEquals(true, simple(run(stepExpr(op("$exists", list())), defaultContext()).value())); + assertEquals(true, simple(run(stepExpr(op("$exists", obj())), defaultContext()).value())); + } + + @Test + void optionalValidationUsesExistsAndBluePatternMatching() { + String program = yaml( + "type: Blue/BEX Program", + "expr:", + " $or:", + " - $not:", + " $exists:", + " $event: /message/request/note", + " - $is:", + " node:", + " $event: /message/request/note", + " pattern:", + " type: Text"); + + assertEquals(true, simple(runYaml(program, eventRequest(obj())).value())); + assertEquals(true, simple(runYaml(program, eventRequest(obj("note", "hello"))).value())); + assertEquals(false, simple(runYaml(program, eventRequest(obj("note", 7))).value())); + } + + @Test + void forEachListCanExposeIndexForPatchPaths() { + BexExecutionResult result = run(stepDo(list( + op("$forEach", obj( + "in", op("$event", "message/request/orders"), + "item", "order", + "index", "i", + "do", list(op("$appendChange", obj( + "op", "replace", + "path", op("$pointerJoin", list("orders", op("$var", "i"), "status")), + "val", "received"))))), + op("$return", op("$changeset", true)) + )), eventRequest(obj("orders", list(obj("id", "a"), obj("id", "b"))))); + + assertEquals(l( + m("op", "replace", "path", "/orders/0/status", "val", "received"), + m("op", "replace", "path", "/orders/1/status", "val", "received")), + simple(result.value())); + } + + @Test + void forEachObjectCanExposeKeyAndItemValueSeparately() { + BexExecutionResult result = run(stepDo(list( + op("$forEach", obj( + "in", op("$event", "message/request/ordersById"), + "key", "orderId", + "item", "order", + "do", list(op("$appendChange", obj( + "op", "replace", + "path", op("$pointerJoin", list("orders", op("$var", "orderId"), "status")), + "val", op("$pointerGet", obj("object", op("$var", "order"), "path", "status"))))))), + op("$return", op("$changeset", true)) + )), eventRequest(obj("ordersById", obj("abc/def~ghi", obj("status", "new"))))); + + assertEquals(l(m("op", "replace", "path", "/orders/abc~1def~0ghi/status", "val", "new")), + simple(result.value())); + } + + @Test + void forEachObjectKeepsOldEntryShapeWhenOnlyItemIsDeclared() { + BexExecutionResult result = run(stepDo(list( + op("$forEach", obj( + "in", op("$event", "message/request/ordersById"), + "item", "entry", + "do", list(op("$appendEvent", obj( + "orderId", op("$pointerGet", obj("object", op("$var", "entry"), "path", "key")), + "status", op("$pointerGet", obj( + "object", op("$pointerGet", obj("object", op("$var", "entry"), "path", "val")), + "path", "status"))))))), + op("$return", op("$events", true)) + )), eventRequest(obj("ordersById", obj("a", obj("status", "new"))))); + + assertEquals(l(m("orderId", "a", "status", "new")), simple(result.value())); + } + + @Test + void dynamicTextOperandsRejectMissingValuesButStaticEmptyKeyStillWorks() { + assertThrows(BexException.class, () -> run(stepExpr(op("$get", obj( + "object", obj("a", 1), + "key", op("$document", "/missing") + ))), defaultContext())); + assertThrows(BexException.class, () -> run(stepExpr(op("$objectSet", obj( + "object", obj(), + "key", op("$document", "/missing"), + "val", 1 + ))), defaultContext())); + assertThrows(BexException.class, () -> run(stepExpr(op("$binding", obj( + "name", op("$document", "/missing"), + "path", "/" + ))), defaultContext())); + assertThrows(BexException.class, () -> run(stepExpr(op("$steps", obj( + "step", op("$document", "/missing"), + "path", "/" + ))), defaultContext())); + assertThrows(BexException.class, () -> run(stepDo(list( + op("$appendChange", obj( + "op", op("$document", "/missing"), + "path", "/x", + "val", 1)) + )), defaultContext())); + assertThrows(BexException.class, () -> run(stepExpr(op("$pointerSet", obj( + "object", obj("a", 1), + "op", op("$document", "/missing"), + "path", "/a", + "val", 2 + ))), defaultContext())); + + assertEquals(m("", bi(1)), simple(run(stepExpr(op("$objectSet", obj( + "object", obj(), + "key", "", + "val", 1 + ))), defaultContext()).value())); + } + + @Test + void resultValueMaterializesDescendantObjectPatchWhenReadingParent() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/hotelOrder/status", "val", "confirmed")), + op("$return", obj("hotelOrder", op("$resultValue", "/hotelOrder"))) + )), documentContext(obj("hotelOrder", obj("status", "pending", "amount", 400)))); + + assertEquals(m("hotelOrder", m("amount", bi(400), "status", "confirmed")), simple(result.value())); + } + + @Test + void resultValueAppliesParentAndChildPatchesInOrder() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/hotelOrder", "val", obj("status", "pending", "amount", 400))), + op("$appendChange", obj("op", "replace", "path", "/hotelOrder/status", "val", "confirmed")), + op("$return", obj("hotelOrder", op("$resultValue", "/hotelOrder"))) + )), documentContext(obj())); + + assertEquals(m("hotelOrder", m("amount", bi(400), "status", "confirmed")), simple(result.value())); + } + + @Test + void resultValueMaterializesChildRemoveWhenReadingParent() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "remove", "path", "/hotelOrder/status")), + op("$return", obj("hotelOrder", op("$resultValue", "/hotelOrder"))) + )), documentContext(obj("hotelOrder", obj("status", "pending", "amount", 400)))); + + assertEquals(m("hotelOrder", m("amount", bi(400))), simple(result.value())); + } + + @Test + void resultValueExactPathAndListIndexReplacementStillWork() { + BexExecutionResult exact = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/hotelOrder/status", "val", "confirmed")), + op("$return", obj("status", op("$resultValue", "/hotelOrder/status"))) + )), documentContext(obj("hotelOrder", obj("status", "pending")))); + BexExecutionResult listReplacement = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/orders/1/status", "val", "confirmed")), + op("$return", obj("orders", op("$resultValue", "/orders"))) + )), documentContext(obj("orders", list(obj("status", "pending"), obj("status", "pending"))))); + + assertEquals(m("status", "confirmed"), simple(exact.value())); + assertEquals(m("orders", l(m("status", "pending"), m("status", "confirmed"))), simple(listReplacement.value())); + } + + @Test + void resultValueMaterializesScopedRelativePatchWhenReadingParent() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "status", "val", "confirmed")), + op("$return", obj("current", op("$resultValue", "/contracts/current"))) + )), documentContext(obj("contracts", obj("current", obj("status", "pending", "amount", 400))), + "/contracts/current")); + + assertEquals(m("current", m("amount", bi(400), "status", "confirmed")), simple(result.value())); + } + + @Test + void resultValueHandlesRootReplaceAndRootRemove() { + BexExecutionResult replaced = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/", "val", obj("status", "confirmed"))), + op("$return", obj("root", op("$resultValue", "/"))) + )), documentContext(obj("status", "pending"))); + BexExecutionResult removed = run(stepDo(list( + op("$appendChange", obj("op", "remove", "path", "/")), + op("$return", obj("root", op("$resultValue", "/"))) + )), documentContext(obj("status", "pending"))); + + assertEquals(m("root", m("status", "confirmed")), simple(replaced.value())); + assertEquals(m(), simple(removed.value())); + } + + @Test + void resultValueAppliesRemoveThenAddAtSamePath() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "remove", "path", "/hotelOrder/status")), + op("$appendChange", obj("op", "add", "path", "/hotelOrder/status", "val", "confirmed")), + op("$return", obj("hotelOrder", op("$resultValue", "/hotelOrder"))) + )), documentContext(obj("hotelOrder", obj("status", "pending", "amount", 400)))); + + assertEquals(m("hotelOrder", m("amount", bi(400), "status", "confirmed")), simple(result.value())); + } + + @Test + void resultValueCreatesMissingParentForChildAdd() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "add", "path", "/hotelOrder/status", "val", "confirmed")), + op("$return", obj("hotelOrder", op("$resultValue", "/hotelOrder"))) + )), documentContext(obj())); + + assertEquals(m("hotelOrder", m("status", "confirmed")), simple(result.value())); + } + + @Test + void resultValueListIndexRemoveIsNonShiftingOverlayBehavior() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "remove", "path", "/orders/1")), + op("$return", obj("orders", op("$resultValue", "/orders"))) + )), documentContext(obj("orders", list("a", "b", "c")))); + + assertEquals(m("orders", l("a", null, "c")), simple(result.value())); + } + + @Test + void forEachRejectsDuplicateBindingNames() { + assertThrows(BexException.class, () -> run(stepDo(list( + op("$forEach", obj( + "in", list("a"), + "item", "x", + "index", "x", + "do", list())) + )), defaultContext())); + assertThrows(BexException.class, () -> run(stepDo(list( + op("$forEach", obj( + "in", obj("a", 1), + "item", "x", + "key", "x", + "do", list())) + )), defaultContext())); + } + + @Test + void hotelOrderConfirmationBuildsPatchAndEvent() { + BexExecutionResult result = run(stepDo(list( + op("$appendChange", obj("op", "replace", "path", "/status", "val", "confirmed")), + op("$appendEvent", obj( + "type", "General/Event", + "kind", "Order Confirmed", + "partner", op("$document", "/hotelName"), + "amount", op("$document", "/amount"))), + op("$return", obj( + "status", op("$resultValue", "/status"), + "events", op("$events", true))) + )), documentContext(obj("status", "pending", "hotelName", "Blue Hotel", "amount", 120))); + + assertEquals(m( + "events", l(m("amount", bi(120), "kind", "Order Confirmed", "partner", "Blue Hotel", "type", "General/Event")), + "status", "confirmed"), simple(result.value())); + } + + @Test + void payNoteRequestsCaptureOnlyWhenBothPartnerOrdersAreConfirmed() { + assertEquals(l(m( + "amount", bi(500), + "reason", "Both partner orders confirmed", + "type", "PayNote/Complete Payment Requested")), + simple(runPayNote("confirmed", "confirmed").value())); + assertEquals(l(), simple(runPayNote("confirmed", "pending").value())); + } + + private BexExecutionResult runPayNote(String hotelStatus, String restaurantStatus) { + return run(stepDo(list( + op("$if", obj( + "cond", op("$and", list( + op("$eq", list(op("$document", "/hotelOrder/status"), "confirmed")), + op("$eq", list(op("$document", "/restaurantOrder/status"), "confirmed")))), + "then", list(op("$appendEvent", obj( + "type", "PayNote/Complete Payment Requested", + "amount", op("$document", "/amount/expectedTotal"), + "reason", "Both partner orders confirmed"))))), + op("$return", op("$events", true)) + )), documentContext(obj( + "hotelOrder", obj("status", hotelStatus), + "restaurantOrder", obj("status", restaurantStatus), + "amount", obj("expectedTotal", 500)))); + } + + private BexExecutionResult run(Node step, BexExecutionContext context) { + return engine.compileAndExecute(BexProgramSource.inline(FrozenNode.fromResolvedNode(step)), context); + } + + private BexExecutionResult runYaml(String yaml, BexExecutionContext context) { + Node node = blue.yamlToNode(yaml); + return engine.compileAndExecute(BexProgramSource.inline(FrozenNode.fromResolvedNode(node)), context); + } + + private BexExecutionContext documentContext(Node document) { + return documentContext(document, "/"); + } + + private BexExecutionContext documentContext(Node document, String scope) { + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView(frozen(document), frozen(document), scope)) + .event(BexValues.nodeSnapshot(obj())) + .currentContract(BexValues.nodeSnapshot(obj())) + .gasLimit(1_000_000) + .build(); + } + + private BexExecutionContext eventRequest(Node request) { + return BexExecutionContext.builder() + .document(new FrozenBexDocumentView(frozen(obj()), frozen(obj()), "/")) + .event(BexValues.nodeSnapshot(obj("message", obj("request", request)))) + .currentContract(BexValues.nodeSnapshot(obj())) + .gasLimit(1_000_000) + .build(); + } + + private static String yaml(String... lines) { + return String.join("\n", lines); + } +} diff --git a/src/test/resources/rich-fixtures/current/01-is-node-primitive-true.yaml b/src/test/resources/rich-fixtures/current/01-is-node-primitive-true.yaml new file mode 100644 index 0000000..69dc2e2 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/01-is-node-primitive-true.yaml @@ -0,0 +1,71 @@ +fixtureId: BEX-CURRENT-001 +title: Valid $is.node primitive match +targetStatus: current-pass +tags: +- syntax +- types +- $is +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $is: + node: 400 + pattern: + type: Integer +expectation: + outcome: success + resultSimple: true diff --git a/src/test/resources/rich-fixtures/current/02-join-list-success.yaml b/src/test/resources/rich-fixtures/current/02-join-list-success.yaml new file mode 100644 index 0000000..daf9235 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/02-join-list-success.yaml @@ -0,0 +1,73 @@ +fixtureId: BEX-CURRENT-002 +title: Valid $join.list syntax +targetStatus: current-pass +tags: +- syntax +- strings +- $join +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $join: + list: + - a + - b + - c + separator: ':' +expectation: + outcome: success + resultSimple: a:b:c diff --git a/src/test/resources/rich-fixtures/current/03-const-known.yaml b/src/test/resources/rich-fixtures/current/03-const-known.yaml new file mode 100644 index 0000000..a10cf89 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/03-const-known.yaml @@ -0,0 +1,68 @@ +fixtureId: BEX-CURRENT-003 +title: Known constant resolves +targetStatus: current-pass +tags: +- constants +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + constants: + captureAmount: 49900 + expr: + $const: captureAmount +expectation: + outcome: success + resultSimple: 49900 diff --git a/src/test/resources/rich-fixtures/current/04-const-unknown-compile-error.yaml b/src/test/resources/rich-fixtures/current/04-const-unknown-compile-error.yaml new file mode 100644 index 0000000..02cd1f9 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/04-const-unknown-compile-error.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-004 +title: Unknown constant is compile error +targetStatus: current-compile-error +tags: +- constants +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + constants: + captureAmount: 49900 + expr: + $const: captuerAmount +expectation: + outcome: compile-error + errorContains: Unknown constant diff --git a/src/test/resources/rich-fixtures/current/05-literal-preserves-unknown-const.yaml b/src/test/resources/rich-fixtures/current/05-literal-preserves-unknown-const.yaml new file mode 100644 index 0000000..dbfb8ab --- /dev/null +++ b/src/test/resources/rich-fixtures/current/05-literal-preserves-unknown-const.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-005 +title: $literal preserves nested unknown constant shape +targetStatus: current-pass +tags: +- literal +- constants +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $literal: + $const: captuerAmount +expectation: + outcome: success + resultSimple: + $const: captuerAmount diff --git a/src/test/resources/rich-fixtures/current/06-literal-rejects-bex-in-type-field.yaml b/src/test/resources/rich-fixtures/current/06-literal-rejects-bex-in-type-field.yaml new file mode 100644 index 0000000..d2af8c6 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/06-literal-rejects-bex-in-type-field.yaml @@ -0,0 +1,73 @@ +fixtureId: BEX-CURRENT-006 +title: $literal still rejects BEX inside Blue type-definition field +targetStatus: current-compile-error +tags: +- literal +- types +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $literal: + type: + $choose: + cond: true + then: Integer + else: Text +expectation: + outcome: compile-error + errorContains: BEX expressions inside Blue type diff --git a/src/test/resources/rich-fixtures/current/07-function-typed-integer-pass.yaml b/src/test/resources/rich-fixtures/current/07-function-typed-integer-pass.yaml new file mode 100644 index 0000000..8bc5ee0 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/07-function-typed-integer-pass.yaml @@ -0,0 +1,77 @@ +fixtureId: BEX-CURRENT-007 +title: Typed function arg accepts Integer +targetStatus: current-pass +tags: +- functions +- types +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + capture: + args: + amount: + type: Integer + expr: + $var: amount + expr: + $call: + function: capture + args: + amount: 400 +expectation: + outcome: success + resultSimple: 400 diff --git a/src/test/resources/rich-fixtures/current/08-function-typed-integer-rejects-text.yaml b/src/test/resources/rich-fixtures/current/08-function-typed-integer-rejects-text.yaml new file mode 100644 index 0000000..380dc88 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/08-function-typed-integer-rejects-text.yaml @@ -0,0 +1,78 @@ +fixtureId: BEX-CURRENT-008 +title: Typed function arg rejects Text for Integer +targetStatus: current-runtime-error +tags: +- functions +- types +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + capture: + args: + amount: + type: Integer + expr: + $var: amount + expr: + $call: + function: capture + args: + amount: '400' +expectation: + outcome: runtime-error + errorContains: does not match declared Blue pattern diff --git a/src/test/resources/rich-fixtures/current/09-function-structural-pass.yaml b/src/test/resources/rich-fixtures/current/09-function-structural-pass.yaml new file mode 100644 index 0000000..b32d0b0 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/09-function-structural-pass.yaml @@ -0,0 +1,97 @@ +fixtureId: BEX-CURRENT-009 +title: Structural function arg pattern passes +targetStatus: current-pass +tags: +- functions +- types +- structural +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + validateRequest: + args: + request: + customerName: + type: Text + schema: + required: true + nights: + type: Integer + schema: + required: true + expr: + customerName: + $get: + object: + $var: request + key: customerName + nights: + $get: + object: + $var: request + key: nights + expr: + $call: + function: validateRequest + args: + request: + $event: /message/request +expectation: + outcome: success + resultSimple: + customerName: Jan Kowalski + nights: 2 diff --git a/src/test/resources/rich-fixtures/current/10-function-structural-missing-required.yaml b/src/test/resources/rich-fixtures/current/10-function-structural-missing-required.yaml new file mode 100644 index 0000000..9563d6f --- /dev/null +++ b/src/test/resources/rich-fixtures/current/10-function-structural-missing-required.yaml @@ -0,0 +1,80 @@ +fixtureId: BEX-CURRENT-010 +title: Structural function arg rejects missing required field +targetStatus: current-runtime-error +tags: +- functions +- types +- structural +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + customerName: Jan Kowalski + createdAt: '2025-06-20T10:00:00Z' + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + validateRequest: + args: + request: + customerName: + type: Text + schema: + required: true + nights: + type: Integer + schema: + required: true + expr: true + expr: + $call: + function: validateRequest + args: + request: + $event: /message/request +expectation: + outcome: runtime-error + errorContains: does not match declared Blue pattern diff --git a/src/test/resources/rich-fixtures/current/11-function-extra-arg-compile-error.yaml b/src/test/resources/rich-fixtures/current/11-function-extra-arg-compile-error.yaml new file mode 100644 index 0000000..ac2a5f7 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/11-function-extra-arg-compile-error.yaml @@ -0,0 +1,78 @@ +fixtureId: BEX-CURRENT-011 +title: Extra function arg is compile error +targetStatus: current-compile-error +tags: +- functions +- abi +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + f: + args: + input: {} + expr: + $var: input + expr: + $call: + function: f + args: + input: 1 + extra: 2 +expectation: + outcome: compile-error + errorContains: Unknown argument diff --git a/src/test/resources/rich-fixtures/current/12-function-missing-arg-compile-error.yaml b/src/test/resources/rich-fixtures/current/12-function-missing-arg-compile-error.yaml new file mode 100644 index 0000000..2ca39c4 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/12-function-missing-arg-compile-error.yaml @@ -0,0 +1,76 @@ +fixtureId: BEX-CURRENT-012 +title: Missing function arg is compile error +targetStatus: current-compile-error +tags: +- functions +- abi +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + f: + args: + input: {} + expr: + $var: input + expr: + $call: + function: f + args: {} +expectation: + outcome: compile-error + errorContains: Missing argument diff --git a/src/test/resources/rich-fixtures/current/13-function-reserved-arg-name-rejected.yaml b/src/test/resources/rich-fixtures/current/13-function-reserved-arg-name-rejected.yaml new file mode 100644 index 0000000..74c21a4 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/13-function-reserved-arg-name-rejected.yaml @@ -0,0 +1,76 @@ +fixtureId: BEX-CURRENT-013 +title: Reserved Blue key cannot be arg name +targetStatus: current-compile-error +tags: +- functions +- blue-syntax +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + functions: + f: + args: + contracts: {} + expr: true + expr: + $call: + function: f + args: + contracts: anything +expectation: + outcome: compile-error + errorContains: reserved Blue key diff --git a/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml b/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml new file mode 100644 index 0000000..c078348 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/14-is-blueid-typed-node-true.yaml @@ -0,0 +1,74 @@ +fixtureId: BEX-CURRENT-014 +title: $is matches typed node to blueId pattern +targetStatus: current-pass +tags: +- types +- $is +- blueId +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $is: + node: + type: + blueId: HotelOrderType + status: confirmed + pattern: + blueId: HotelOrderType +expectation: + outcome: success + resultSimple: true diff --git a/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml b/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml new file mode 100644 index 0000000..7408ad0 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/15-is-blueid-wrong-type-false.yaml @@ -0,0 +1,74 @@ +fixtureId: BEX-CURRENT-015 +title: $is returns false for wrong blueId +targetStatus: current-pass +tags: +- types +- $is +- blueId +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $is: + node: + type: + blueId: RestaurantOrderType + status: confirmed + pattern: + blueId: HotelOrderType +expectation: + outcome: success + resultSimple: false diff --git a/src/test/resources/rich-fixtures/current/16-is-pattern-with-bex-rejected.yaml b/src/test/resources/rich-fixtures/current/16-is-pattern-with-bex-rejected.yaml new file mode 100644 index 0000000..46c5f39 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/16-is-pattern-with-bex-rejected.yaml @@ -0,0 +1,75 @@ +fixtureId: BEX-CURRENT-016 +title: $is.pattern is static and rejects BEX +targetStatus: current-compile-error +tags: +- types +- $is +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $is: + node: 10 + pattern: + type: + $choose: + cond: true + then: Integer + else: Text +expectation: + outcome: compile-error + errorContains: BEX expressions inside Blue type diff --git a/src/test/resources/rich-fixtures/current/17-document-scope-relative.yaml b/src/test/resources/rich-fixtures/current/17-document-scope-relative.yaml new file mode 100644 index 0000000..f7bab17 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/17-document-scope-relative.yaml @@ -0,0 +1,72 @@ +fixtureId: BEX-CURRENT-017 +title: $document uses current document scope +targetStatus: current-pass +tags: +- pointers +- $document +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + status: + $document: status + absoluteStatus: + $document: /status +expectation: + outcome: success + resultSimple: + status: scoped-status + absoluteStatus: pending diff --git a/src/test/resources/rich-fixtures/current/18-event-value-local.yaml b/src/test/resources/rich-fixtures/current/18-event-value-local.yaml new file mode 100644 index 0000000..bb239d5 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/18-event-value-local.yaml @@ -0,0 +1,72 @@ +fixtureId: BEX-CURRENT-018 +title: $event path is value-local, not document-scope-relative +targetStatus: current-pass +tags: +- pointers +- $event +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + amount: + $event: message/request/amount + actor: + $event: actor/name +expectation: + outcome: success + resultSimple: + amount: 7 + actor: Hotel Badura diff --git a/src/test/resources/rich-fixtures/current/19-current-contract-value-local.yaml b/src/test/resources/rich-fixtures/current/19-current-contract-value-local.yaml new file mode 100644 index 0000000..c64daf4 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/19-current-contract-value-local.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-019 +title: $currentContract path is value-local +targetStatus: current-pass +tags: +- pointers +- $currentContract +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + channelName: + $currentContract: channel/name +expectation: + outcome: success + resultSimple: + channelName: hotel-channel diff --git a/src/test/resources/rich-fixtures/current/20-steps-object-value-local.yaml b/src/test/resources/rich-fixtures/current/20-steps-object-value-local.yaml new file mode 100644 index 0000000..ae81a3b --- /dev/null +++ b/src/test/resources/rich-fixtures/current/20-steps-object-value-local.yaml @@ -0,0 +1,71 @@ +fixtureId: BEX-CURRENT-020 +title: $steps object path is value-local +targetStatus: current-pass +tags: +- pointers +- $steps +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + firstPatchPath: + $steps: + step: Build + path: changeset/0/path +expectation: + outcome: success + resultSimple: + firstPatchPath: /status diff --git a/src/test/resources/rich-fixtures/current/21-pointer-get-value-local.yaml b/src/test/resources/rich-fixtures/current/21-pointer-get-value-local.yaml new file mode 100644 index 0000000..a950ef8 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/21-pointer-get-value-local.yaml @@ -0,0 +1,71 @@ +fixtureId: BEX-CURRENT-021 +title: $pointerGet path is value-local +targetStatus: current-pass +tags: +- pointers +- $pointerGet +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $pointerGet: + object: + a: + b: 1 + path: a/b +expectation: + outcome: success + resultSimple: 1 diff --git a/src/test/resources/rich-fixtures/current/22-pointer-set-value-local.yaml b/src/test/resources/rich-fixtures/current/22-pointer-set-value-local.yaml new file mode 100644 index 0000000..3107db4 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/22-pointer-set-value-local.yaml @@ -0,0 +1,75 @@ +fixtureId: BEX-CURRENT-022 +title: $pointerSet path is value-local +targetStatus: current-pass +tags: +- pointers +- $pointerSet +context: + documentScope: /contracts/current + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $pointerSet: + object: + a: + b: 1 + path: a/c + val: 2 +expectation: + outcome: success + resultSimple: + a: + b: 1 + c: 2 diff --git a/src/test/resources/rich-fixtures/current/23-dynamic-pointer-missing-error.yaml b/src/test/resources/rich-fixtures/current/23-dynamic-pointer-missing-error.yaml new file mode 100644 index 0000000..eef83a1 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/23-dynamic-pointer-missing-error.yaml @@ -0,0 +1,71 @@ +fixtureId: BEX-CURRENT-023 +title: Dynamic pointer null/undefined rejects instead of resolving root/current scope +targetStatus: current-runtime-error +tags: +- pointers +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: + $document: /missingPath + val: done +expectation: + outcome: runtime-error + errorContains: Pointer operand cannot be null or undefined diff --git a/src/test/resources/rich-fixtures/current/24-pointer-join-escape.yaml b/src/test/resources/rich-fixtures/current/24-pointer-join-escape.yaml new file mode 100644 index 0000000..ca4acb7 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/24-pointer-join-escape.yaml @@ -0,0 +1,70 @@ +fixtureId: BEX-CURRENT-024 +title: $pointerJoin escapes slash and tilde +targetStatus: current-pass +tags: +- pointers +- $pointerJoin +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $pointerJoin: + - orders + - abc/def~ghi + - status +expectation: + outcome: success + resultSimple: /orders/abc~1def~0ghi/status diff --git a/src/test/resources/rich-fixtures/current/25-append-change-valid.yaml b/src/test/resources/rich-fixtures/current/25-append-change-valid.yaml new file mode 100644 index 0000000..7bd52bf --- /dev/null +++ b/src/test/resources/rich-fixtures/current/25-append-change-valid.yaml @@ -0,0 +1,75 @@ +fixtureId: BEX-CURRENT-025 +title: $appendChange creates validated patch +targetStatus: current-pass +tags: +- patches +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /status + val: confirmed + - $return: + changes: + $changeset: true +expectation: + outcome: success + changeset: + - op: replace + path: /status + val: confirmed diff --git a/src/test/resources/rich-fixtures/current/26-append-changes-bad-op-error.yaml b/src/test/resources/rich-fixtures/current/26-append-changes-bad-op-error.yaml new file mode 100644 index 0000000..d6cb873 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/26-append-changes-bad-op-error.yaml @@ -0,0 +1,70 @@ +fixtureId: BEX-CURRENT-026 +title: $appendChanges rejects unsupported op +targetStatus: current-runtime-error +tags: +- patches +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChanges: + - op: move + path: /status + val: confirmed +expectation: + outcome: runtime-error + errorContains: Unsupported patch op diff --git a/src/test/resources/rich-fixtures/current/27-append-change-missing-val-error.yaml b/src/test/resources/rich-fixtures/current/27-append-change-missing-val-error.yaml new file mode 100644 index 0000000..70ce4df --- /dev/null +++ b/src/test/resources/rich-fixtures/current/27-append-change-missing-val-error.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-027 +title: replace patch requires val +targetStatus: current-runtime-error +tags: +- patches +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /status +expectation: + outcome: runtime-error + errorContains: requires val diff --git a/src/test/resources/rich-fixtures/current/28-append-change-remove-no-val.yaml b/src/test/resources/rich-fixtures/current/28-append-change-remove-no-val.yaml new file mode 100644 index 0000000..14ca367 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/28-append-change-remove-no-val.yaml @@ -0,0 +1,73 @@ +fixtureId: BEX-CURRENT-028 +title: remove patch works without val +targetStatus: current-pass +tags: +- patches +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: remove + path: /hotelOrder/status + - $return: + changes: + $changeset: true +expectation: + outcome: success + changeset: + - op: remove + path: /hotelOrder/status diff --git a/src/test/resources/rich-fixtures/current/29-append-events-batch-success.yaml b/src/test/resources/rich-fixtures/current/29-append-events-batch-success.yaml new file mode 100644 index 0000000..950fb2b --- /dev/null +++ b/src/test/resources/rich-fixtures/current/29-append-events-batch-success.yaml @@ -0,0 +1,73 @@ +fixtureId: BEX-CURRENT-029 +title: $appendEvents appends in order +targetStatus: current-pass +tags: +- events +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendEvents: + - eventKind: A + - eventKind: B + - $return: + events: + $events: true +expectation: + outcome: success + events: + - eventKind: A + - eventKind: B diff --git a/src/test/resources/rich-fixtures/current/30-append-events-undefined-error.yaml b/src/test/resources/rich-fixtures/current/30-append-events-undefined-error.yaml new file mode 100644 index 0000000..0ff6db4 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/30-append-events-undefined-error.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-030 +title: $appendEvents rejects undefined item +targetStatus: current-runtime-error +tags: +- events +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendEvents: + - eventKind: A + - $document: /missingEvent +expectation: + outcome: runtime-error + errorContains: Undefined cannot diff --git a/src/test/resources/rich-fixtures/current/31-result-value-exact-latest.yaml b/src/test/resources/rich-fixtures/current/31-result-value-exact-latest.yaml new file mode 100644 index 0000000..e3bb7f1 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/31-result-value-exact-latest.yaml @@ -0,0 +1,80 @@ +fixtureId: BEX-CURRENT-031 +title: $resultValue exact path uses latest patch +targetStatus: current-pass +tags: +- resultValue +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /status + val: first + - $appendChange: + op: replace + path: /status + val: second + - $return: + status: + $resultValue: /status + count: + $resultValue: /count +expectation: + outcome: success + resultSimple: + status: second + count: 5 diff --git a/src/test/resources/rich-fixtures/current/32-string-operators.yaml b/src/test/resources/rich-fixtures/current/32-string-operators.yaml new file mode 100644 index 0000000..8ee68af --- /dev/null +++ b/src/test/resources/rich-fixtures/current/32-string-operators.yaml @@ -0,0 +1,91 @@ +fixtureId: BEX-CURRENT-032 +title: String operators compose correctly +targetStatus: current-pass +tags: +- strings +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + joined: + $join: + list: + - hotel + - restaurant + - paynote + separator: '|' + splitLimited: + $split: + text: a:b:c + separator: ':' + limit: 2 + after: + $sliceAfter: + - PayNote:CAPTURED + - 'PayNote:' + starts: + $startsWith: + - PayNote:CAPTURED + - 'PayNote:' +expectation: + outcome: success + resultSimple: + joined: hotel|restaurant|paynote + splitLimited: + - a + - b:c + after: CAPTURED + starts: true diff --git a/src/test/resources/rich-fixtures/current/33-logic-truthy-empty-coalesce.yaml b/src/test/resources/rich-fixtures/current/33-logic-truthy-empty-coalesce.yaml new file mode 100644 index 0000000..cf5fdca --- /dev/null +++ b/src/test/resources/rich-fixtures/current/33-logic-truthy-empty-coalesce.yaml @@ -0,0 +1,80 @@ +fixtureId: BEX-CURRENT-033 +title: Logic, truthiness, empty, and coalesce +targetStatus: current-pass +tags: +- logic +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + zeroTruthy: + $truthy: 0 + emptyObjectTruthy: + $truthy: {} + emptyText: + $empty: '' + firstUseful: + $coalesce: + - {} + - '' + - fallback +expectation: + outcome: success + resultSimple: + zeroTruthy: true + emptyObjectTruthy: false + emptyText: true + firstUseful: fallback diff --git a/src/test/resources/rich-fixtures/current/34-numeric-operators.yaml b/src/test/resources/rich-fixtures/current/34-numeric-operators.yaml new file mode 100644 index 0000000..b88ed01 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/34-numeric-operators.yaml @@ -0,0 +1,91 @@ +fixtureId: BEX-CURRENT-034 +title: Exact integer arithmetic and numeric comparison +targetStatus: current-pass +tags: +- numeric +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + total: + $add: + - 40000 + - 9900 + margin: + $subtract: + - 49900 + - 40000 + - 2500 + doubled: + $multiply: + - 25000 + - 2 + exactHalf: + $divide: + - 100 + - 2 + enough: + $gte: + - 49900 + - 40000 +expectation: + outcome: success + resultSimple: + total: 49900 + margin: 7400 + doubled: 50000 + exactHalf: 50 + enough: true diff --git a/src/test/resources/rich-fixtures/current/35-numeric-nonexact-divide-error.yaml b/src/test/resources/rich-fixtures/current/35-numeric-nonexact-divide-error.yaml new file mode 100644 index 0000000..deeef83 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/35-numeric-nonexact-divide-error.yaml @@ -0,0 +1,69 @@ +fixtureId: BEX-CURRENT-035 +title: Non-exact integer division fails +targetStatus: current-runtime-error +tags: +- numeric +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $divide: + - 5 + - 2 +expectation: + outcome: runtime-error + errorContains: Non-exact diff --git a/src/test/resources/rich-fixtures/current/36-object-list-operators.yaml b/src/test/resources/rich-fixtures/current/36-object-list-operators.yaml new file mode 100644 index 0000000..92a3d0c --- /dev/null +++ b/src/test/resources/rich-fixtures/current/36-object-list-operators.yaml @@ -0,0 +1,93 @@ +fixtureId: BEX-CURRENT-036 +title: Object and list operators +targetStatus: current-pass +tags: +- objects +- lists +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + keys: + $keys: + b: 2 + a: 1 + entries: + $entries: + b: 2 + a: 1 + second: + $listGet: + list: + - a + - b + index: 1 + merged: + $merge: + - a: 1 + - b: 2 + combined: + $listConcat: + - - a + - - b + objectSet: + $objectSet: + object: + a: 1 + key: b + val: 2 +expectation: + outcome: success diff --git a/src/test/resources/rich-fixtures/current/37-blueid-sibling-output-conversion-error.yaml b/src/test/resources/rich-fixtures/current/37-blueid-sibling-output-conversion-error.yaml new file mode 100644 index 0000000..6e5b1f7 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/37-blueid-sibling-output-conversion-error.yaml @@ -0,0 +1,68 @@ +fixtureId: BEX-CURRENT-037 +title: BexBlueNodeWriter rejects blueId with sibling fields +targetStatus: current-output-conversion-error +tags: +- blue-output +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + blueId: HotelOrderType + status: confirmed +expectation: + outcome: parse-error-or-output-conversion-error + errorContains: blueId diff --git a/src/test/resources/rich-fixtures/current/38-hotel-confirmation-use-case.yaml b/src/test/resources/rich-fixtures/current/38-hotel-confirmation-use-case.yaml new file mode 100644 index 0000000..9edfe40 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/38-hotel-confirmation-use-case.yaml @@ -0,0 +1,92 @@ +fixtureId: BEX-CURRENT-038 +title: Hotel confirmation builds patch and event +targetStatus: current-pass +tags: +- use-case +- hotel +- events +- patches +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /hotelOrder/status + val: confirmed + - $appendEvent: + eventKind: Order Confirmed + partner: + $document: /hotelName + amount: + $document: /hotelOrder/amount + - $return: + newStatus: + $resultValue: /hotelOrder/status + events: + $events: true +expectation: + outcome: success + resultSimple: + newStatus: confirmed + events: + - eventKind: Order Confirmed + partner: Hotel Badura + amount: 40000 + changeset: + - op: replace + path: /hotelOrder/status + val: confirmed diff --git a/src/test/resources/rich-fixtures/current/39-paynote-capture-condition-true.yaml b/src/test/resources/rich-fixtures/current/39-paynote-capture-condition-true.yaml new file mode 100644 index 0000000..ed00d17 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/39-paynote-capture-condition-true.yaml @@ -0,0 +1,88 @@ +fixtureId: BEX-CURRENT-039 +title: PayNote capture requested when both partner orders confirmed +targetStatus: current-pass +tags: +- use-case +- paynote +- events +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: confirmed + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: confirmed + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $if: + cond: + $and: + - $eq: + - $document: /hotelOrder/status + - confirmed + - $eq: + - $document: /restaurantOrder/status + - confirmed + then: + - $appendEvent: + eventKind: PayNote Complete Payment Requested + amount: + $document: /amount/expectedTotal + reason: Both partner orders confirmed + - $return: + events: + $events: true +expectation: + outcome: success + events: + - eventKind: PayNote Complete Payment Requested + amount: 49900 + reason: Both partner orders confirmed diff --git a/src/test/resources/rich-fixtures/current/40-paynote-capture-condition-false.yaml b/src/test/resources/rich-fixtures/current/40-paynote-capture-condition-false.yaml new file mode 100644 index 0000000..bfa813b --- /dev/null +++ b/src/test/resources/rich-fixtures/current/40-paynote-capture-condition-false.yaml @@ -0,0 +1,84 @@ +fixtureId: BEX-CURRENT-040 +title: PayNote capture not requested when a partner is pending +targetStatus: current-pass +tags: +- use-case +- paynote +- events +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $if: + cond: + $and: + - $eq: + - $document: /hotelOrder/status + - confirmed + - $eq: + - $document: /restaurantOrder/status + - confirmed + then: + - $appendEvent: + eventKind: PayNote Complete Payment Requested + amount: + $document: /amount/expectedTotal + - $return: + events: + $events: true +expectation: + outcome: success + events: [] diff --git a/src/test/resources/rich-fixtures/current/41-exists-missing-false.yaml b/src/test/resources/rich-fixtures/current/41-exists-missing-false.yaml new file mode 100644 index 0000000..7096067 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/41-exists-missing-false.yaml @@ -0,0 +1,83 @@ +fixtureId: BEX-NEXT-001 +title: $exists returns false only for undefined +targetStatus: current-pass +tags: +- $exists +- validation +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + missing: + $exists: + $document: /missing + nullPresent: + $exists: + $literal: null + falsePresent: + $exists: false + emptyTextPresent: + $exists: '' + emptyObjectPresent: + $exists: {} +expectation: + outcome: success + resultSimple: + missing: false + nullPresent: true + falsePresent: true + emptyTextPresent: true + emptyObjectPresent: true diff --git a/src/test/resources/rich-fixtures/current/42-optional-note-validation.yaml b/src/test/resources/rich-fixtures/current/42-optional-note-validation.yaml new file mode 100644 index 0000000..33aa5d8 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/42-optional-note-validation.yaml @@ -0,0 +1,72 @@ +fixtureId: BEX-NEXT-002 +title: Optional note validation with $exists + $is +targetStatus: current-pass +tags: +- $exists +- validation +- types +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $or: + - $not: + $exists: + $event: /message/request/note + - $is: + node: + $event: /message/request/note + pattern: + type: Text +expectation: + outcome: success + resultSimple: true diff --git a/src/test/resources/rich-fixtures/current/43-foreach-list-index-patches.yaml b/src/test/resources/rich-fixtures/current/43-foreach-list-index-patches.yaml new file mode 100644 index 0000000..6f68df5 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/43-foreach-list-index-patches.yaml @@ -0,0 +1,84 @@ +fixtureId: BEX-NEXT-003 +title: $forEach exposes list index for patch generation +targetStatus: current-pass +tags: +- $forEach +- patches +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + orders: + - id: ORD-1 + - id: ORD-2 + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $forEach: + in: + $event: /message/request/orders + item: order + index: i + do: + - $appendChange: + op: replace + path: + $pointerJoin: + - ordersByIndex + - $var: i + - status + val: received + - $return: + changes: + $changeset: true +expectation: + outcome: success + changeset: + - op: replace + path: /ordersByIndex/0/status + val: received + - op: replace + path: /ordersByIndex/1/status + val: received diff --git a/src/test/resources/rich-fixtures/current/44-foreach-object-key-value-patches.yaml b/src/test/resources/rich-fixtures/current/44-foreach-object-key-value-patches.yaml new file mode 100644 index 0000000..e39ffda --- /dev/null +++ b/src/test/resources/rich-fixtures/current/44-foreach-object-key-value-patches.yaml @@ -0,0 +1,90 @@ +fixtureId: BEX-NEXT-004 +title: $forEach exposes object key and item value separately +targetStatus: current-pass +tags: +- $forEach +- patches +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + ordersById: + ORD-1: + amount: 100 + ORD-2: + amount: 200 + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $forEach: + in: + $event: /message/request/ordersById + key: orderId + item: order + do: + - $appendChange: + op: replace + path: + $pointerJoin: + - orders + - $var: orderId + - amount + val: + $get: + object: + $var: order + key: amount + - $return: + changes: + $changeset: true +expectation: + outcome: success + changeset: + - op: replace + path: /orders/ORD-1/amount + val: 100 + - op: replace + path: /orders/ORD-2/amount + val: 200 diff --git a/src/test/resources/rich-fixtures/current/45-dynamic-text-object-key-missing-error.yaml b/src/test/resources/rich-fixtures/current/45-dynamic-text-object-key-missing-error.yaml new file mode 100644 index 0000000..7e853bd --- /dev/null +++ b/src/test/resources/rich-fixtures/current/45-dynamic-text-object-key-missing-error.yaml @@ -0,0 +1,71 @@ +fixtureId: BEX-NEXT-005 +title: Dynamic text operands reject null/undefined for object keys +targetStatus: current-runtime-error +tags: +- text-operands +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $objectSet: + object: {} + key: + $document: /missingKey + val: 1 +expectation: + outcome: runtime-error + errorContains: cannot be null or undefined diff --git a/src/test/resources/rich-fixtures/current/46-dynamic-text-step-name-missing-error.yaml b/src/test/resources/rich-fixtures/current/46-dynamic-text-step-name-missing-error.yaml new file mode 100644 index 0000000..cfab715 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/46-dynamic-text-step-name-missing-error.yaml @@ -0,0 +1,70 @@ +fixtureId: BEX-NEXT-006 +title: Dynamic step name rejects null/undefined +targetStatus: current-runtime-error +tags: +- text-operands +- errors +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $steps: + step: + $document: /missingStepName + path: / +expectation: + outcome: runtime-error + errorContains: cannot be null or undefined diff --git a/src/test/resources/rich-fixtures/current/47-resultvalue-parent-child-replace.yaml b/src/test/resources/rich-fixtures/current/47-resultvalue-parent-child-replace.yaml new file mode 100644 index 0000000..58d3fcd --- /dev/null +++ b/src/test/resources/rich-fixtures/current/47-resultvalue-parent-child-replace.yaml @@ -0,0 +1,76 @@ +fixtureId: BEX-NEXT-007 +title: $resultValue materializes parent object after child replace +targetStatus: current-pass +tags: +- resultValue +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /hotelOrder/status + val: confirmed + - $return: + hotelOrder: + $resultValue: /hotelOrder +expectation: + outcome: success + resultSimple: + hotelOrder: + status: confirmed + amount: 40000 + hotelName: Hotel Badura diff --git a/src/test/resources/rich-fixtures/current/48-resultvalue-parent-replace-then-child-replace.yaml b/src/test/resources/rich-fixtures/current/48-resultvalue-parent-replace-then-child-replace.yaml new file mode 100644 index 0000000..515e2fc --- /dev/null +++ b/src/test/resources/rich-fixtures/current/48-resultvalue-parent-replace-then-child-replace.yaml @@ -0,0 +1,81 @@ +fixtureId: BEX-NEXT-008 +title: $resultValue applies patches in order when parent then child are replaced +targetStatus: current-pass +tags: +- resultValue +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /hotelOrder + val: + status: pending + amount: 40000 + - $appendChange: + op: replace + path: /hotelOrder/status + val: confirmed + - $return: + hotelOrder: + $resultValue: /hotelOrder +expectation: + outcome: success + resultSimple: + hotelOrder: + status: confirmed + amount: 40000 diff --git a/src/test/resources/rich-fixtures/current/49-resultvalue-parent-child-remove.yaml b/src/test/resources/rich-fixtures/current/49-resultvalue-parent-child-remove.yaml new file mode 100644 index 0000000..4d08cf5 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/49-resultvalue-parent-child-remove.yaml @@ -0,0 +1,74 @@ +fixtureId: BEX-NEXT-009 +title: $resultValue materializes parent object after child remove +targetStatus: current-pass +tags: +- resultValue +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: remove + path: /hotelOrder/status + - $return: + hotelOrder: + $resultValue: /hotelOrder +expectation: + outcome: success + resultSimple: + hotelOrder: + amount: 40000 + hotelName: Hotel Badura diff --git a/src/test/resources/rich-fixtures/current/50-resultvalue-list-index-replace.yaml b/src/test/resources/rich-fixtures/current/50-resultvalue-list-index-replace.yaml new file mode 100644 index 0000000..6c79905 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/50-resultvalue-list-index-replace.yaml @@ -0,0 +1,79 @@ +fixtureId: BEX-NEXT-010 +title: $resultValue supports list index replacement materialization +targetStatus: current-pass +tags: +- resultValue +- lists +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + guestNames: + - Jan + - Ola + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendChange: + op: replace + path: /guestNames/1 + val: Anna + - $return: + guestNames: + $resultValue: /guestNames +expectation: + outcome: success + resultSimple: + guestNames: + - Jan + - Anna diff --git a/src/test/resources/rich-fixtures/current/51-gas-large-output-costs-more.yaml b/src/test/resources/rich-fixtures/current/51-gas-large-output-costs-more.yaml new file mode 100644 index 0000000..d55d6e6 --- /dev/null +++ b/src/test/resources/rich-fixtures/current/51-gas-large-output-costs-more.yaml @@ -0,0 +1,74 @@ +fixtureId: BEX-NEXT-011 +title: Large appended output consumes more gas than tiny output +targetStatus: current-gas-property +tags: +- gas +context: + documentScope: / + rootDocumentSource: | + status: pending + count: 5 + amount: + expectedTotal: 49900 + secured: 49900 + completed: 0 + hotelName: Hotel Badura + restaurantName: Restauracja Cud Malina + hotelOrder: + status: pending + amount: 40000 + hotelName: Hotel Badura + restaurantOrder: + status: pending + amount: 25000 + orders: + abc/def~ghi: + status: pending + ORD-1: + status: pending + ORD-2: + status: pending + contracts: + current: + status: scoped-status + channel: + name: scoped-channel + eventSource: | + message: + request: + amount: 7 + orderId: abc/def~ghi + customerName: Jan Kowalski + nights: 2 + note: hello + createdAt: '2025-06-20T10:00:00Z' + actor: + name: Hotel Badura + currentContractSource: | + channel: + name: hotel-channel + limits: + maxAmount: 50000 + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + do: + - $appendEvent: + eventKind: Large + payload: + $join: + list: + - 00000000000000000000000000000000000000000000000000 + - 11111111111111111111111111111111111111111111111111 + - 22222222222222222222222222222222222222222222222222 + separator: '' +expectation: + outcome: gas-property + property: gasUsedGreaterThanEquivalentTinyEvent diff --git a/src/test/resources/rich-fixtures/parse-errors/01-invalid-is-value-shape.yaml b/src/test/resources/rich-fixtures/parse-errors/01-invalid-is-value-shape.yaml new file mode 100644 index 0000000..6f2bd02 --- /dev/null +++ b/src/test/resources/rich-fixtures/parse-errors/01-invalid-is-value-shape.yaml @@ -0,0 +1,26 @@ +fixtureId: BEX-PARSE-001 +title: Old $is.value shape is invalid Blue YAML +targetStatus: current-parse-error +tags: +- blue-syntax +- parse-errors +context: + documentScope: / + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $is: + value: 400 + pattern: + type: Integer +expectation: + outcome: parse-error + errorContains: payload kind diff --git a/src/test/resources/rich-fixtures/parse-errors/02-invalid-join-items-shape.yaml b/src/test/resources/rich-fixtures/parse-errors/02-invalid-join-items-shape.yaml new file mode 100644 index 0000000..6f3a1b4 --- /dev/null +++ b/src/test/resources/rich-fixtures/parse-errors/02-invalid-join-items-shape.yaml @@ -0,0 +1,27 @@ +fixtureId: BEX-PARSE-002 +title: Old $join.items shape is invalid Blue YAML +targetStatus: current-parse-error +tags: +- blue-syntax +- parse-errors +context: + documentScope: / + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + $join: + items: + - a + - b + separator: ':' +expectation: + outcome: parse-error + errorContains: payload kind diff --git a/src/test/resources/rich-fixtures/parse-errors/03-invalid-blueid-with-siblings.yaml b/src/test/resources/rich-fixtures/parse-errors/03-invalid-blueid-with-siblings.yaml new file mode 100644 index 0000000..05759f3 --- /dev/null +++ b/src/test/resources/rich-fixtures/parse-errors/03-invalid-blueid-with-siblings.yaml @@ -0,0 +1,24 @@ +fixtureId: BEX-PARSE-003 +title: blueId with sibling fields is invalid Blue authoring +targetStatus: current-parse-error +tags: +- blue-syntax +- parse-errors +context: + documentScope: / + stepsBinding: + Build: + changeset: + - op: replace + path: /status + val: built + events: + - eventKind: Built +programSource: | + type: Blue/BEX Program + expr: + blueId: HotelOrderType + status: confirmed +expectation: + outcome: parse-error-or-output-conversion-error + errorContains: blueId