Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Implement rules engine ITE fn and S3 tree transform",
"pull_requests": [
"[#2903](https://github.com/smithy-lang/smithy/pull/2903)"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "feature",
"description": "Improve BDD sifting (2x speed, more reduction)",
"pull_requests": [
"[#2890](https://github.com/smithy-lang/smithy/pull/2890)"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,103 @@ The following example uses ``isValidHostLabel`` to check if the value of the
}


.. _rules-engine-standard-library-ite:

``ite`` function
================

Summary
An if-then-else function that returns one of two values based on a boolean condition.
Argument types
* condition: ``bool``
* trueValue: ``T`` or ``option<T>``
* falseValue: ``T`` or ``option<T>``
Return type
* ``ite(bool, T, T)`` → ``T`` (both non-optional, result is non-optional)
* ``ite(bool, T, option<T>)`` → ``option<T>`` (any optional makes result optional)
* ``ite(bool, option<T>, T)`` → ``option<T>`` (any optional makes result optional)
* ``ite(bool, option<T>, option<T>)`` → ``option<T>`` (both optional, result is optional)
Since
1.1

The ``ite`` (if-then-else) function evaluates a boolean condition and returns one of two values based on
the result. If the condition is ``true``, it returns ``trueValue``; if ``false``, it returns ``falseValue``.
This function is particularly useful for computing conditional values without branching in the rule tree, resulting
in fewer result nodes, and enabling better BDD optimizations as a result of reduced fragmentation.

.. important::
Both ``trueValue`` and ``falseValue`` must have the same base type ``T``. The result type follows
the "least upper bound" rule: if either branch is optional, the result is optional.

The following example uses ``ite`` to compute a URL suffix based on whether FIPS is enabled:

.. code-block:: json

{
"fn": "ite",
"argv": [
{"ref": "UseFIPS"},
"-fips",
""
],
"assign": "fipsSuffix"
}

The following example uses ``ite`` with ``coalesce`` to handle an optional boolean parameter:

.. code-block:: json

{
"fn": "ite",
"argv": [
{
"fn": "coalesce",
"argv": [
{"ref": "DisableFeature"},
false
]
},
"disabled",
"enabled"
],
"assign": "featureState"
}


.. _rules-engine-standard-library-ite-examples:

--------
Examples
--------

The following table shows various inputs and their corresponding outputs for the ``ite`` function:

.. list-table::
:header-rows: 1
:widths: 20 25 25 30

* - Condition
- True Value
- False Value
- Output
* - ``true``
- ``"-fips"``
- ``""``
- ``"-fips"``
* - ``false``
- ``"-fips"``
- ``""``
- ``""``
* - ``true``
- ``"sigv4"``
- ``"sigv4-s3express"``
- ``"sigv4"``
* - ``false``
- ``"sigv4"``
- ``"sigv4-s3express"``
- ``"sigv4-s3express"``


.. _rules-engine-standard-library-not:

``not`` function
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pluginManagement {
}
}



rootProject.name = "smithy"

include(":smithy-aws-iam-traits")
Expand Down
51 changes: 51 additions & 0 deletions smithy-aws-endpoints/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,61 @@ description = "AWS specific components for managing endpoints in Smithy"
extra["displayName"] = "Smithy :: AWS Endpoints Components"
extra["moduleName"] = "software.amazon.smithy.aws.endpoints"

// Custom configuration for S3 model - kept separate from test classpath to avoid
// polluting other tests with S3 model discovery
val s3Model: Configuration by configurations.creating

dependencies {
api(project(":smithy-aws-traits"))
api(project(":smithy-diff"))
api(project(":smithy-rules-engine"))
api(project(":smithy-model"))
api(project(":smithy-utils"))

s3Model("software.amazon.api.models:s3:1.0.11")
}

// Integration test source set for tests that require the S3 model
// These tests require JDK 17+ due to the S3 model dependency
sourceSets {
create("it") {
compileClasspath += sourceSets["main"].output + sourceSets["test"].output
runtimeClasspath += sourceSets["main"].output + sourceSets["test"].output
}
}

configurations["itImplementation"].extendsFrom(configurations["testImplementation"])
configurations["itRuntimeOnly"].extendsFrom(configurations["testRuntimeOnly"])
configurations["itImplementation"].extendsFrom(s3Model)

// Configure IT source set to compile with current JDK (17+)
tasks.named<JavaCompile>("compileItJava") {
// Use current Java version instead of hardcoding to allow flexibility in CI
sourceCompatibility = "17"
targetCompatibility = "17"
}

val integrationTest by tasks.registering(Test::class) {
description = "Runs integration tests that require external models like S3"
group = "verification"
testClassesDirs = sourceSets["it"].output.classesDirs
classpath = sourceSets["it"].runtimeClasspath
dependsOn(tasks.jar)
shouldRunAfter(tasks.test)

// Pass build directory to tests
systemProperty(
"buildDir",
layout.buildDirectory
.get()
.asFile.absolutePath,
)
}

tasks.test {
finalizedBy(integrationTest)
}

tasks.named("check") {
dependsOn(integrationTest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rulesengine.aws.language.functions;

import static org.junit.jupiter.api.Assertions.assertFalse;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ModelSerializer;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.rulesengine.aws.s3.S3TreeRewriter;
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.language.evaluation.TestEvaluator;
import software.amazon.smithy.rulesengine.logic.bdd.CostOptimization;
import software.amazon.smithy.rulesengine.logic.bdd.SiftingOptimization;
import software.amazon.smithy.rulesengine.logic.cfg.Cfg;
import software.amazon.smithy.rulesengine.traits.EndpointBddTrait;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
import software.amazon.smithy.rulesengine.traits.EndpointTestCase;
import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait;

class S3BddTest {
private static final ShapeId S3_SERVICE_ID = ShapeId.from("com.amazonaws.s3#AmazonS3");
private static Model model;
private static ServiceShape s3Service;
private static EndpointRuleSet originalRules;
private static EndpointRuleSet rules;
private static List<EndpointTestCase> testCases;

@BeforeAll
static void loadS3Model() {
model = Model.assembler()
.discoverModels()
.assemble()
.unwrap();

s3Service = model.expectShape(S3_SERVICE_ID, ServiceShape.class);
originalRules = s3Service.expectTrait(EndpointRuleSetTrait.class).getEndpointRuleSet();
rules = S3TreeRewriter.transform(originalRules);
testCases = s3Service.expectTrait(EndpointTestsTrait.class).getTestCases();
}

@Test
void compileToBddWithOptimizations() {
// Verify transforms preserve semantics by running all test cases
assertFalse(testCases.isEmpty(), "S3 model should have endpoint test cases");
for (EndpointTestCase testCase : testCases) {
TestEvaluator.evaluate(rules, testCase);
}

// Build CFG and compile to BDD
Cfg cfg = Cfg.from(rules);
EndpointBddTrait trait = EndpointBddTrait.from(cfg);

StringBuilder sb = new StringBuilder();
sb.append("\n=== BDD STATS ===\n");
sb.append("Conditions: ").append(trait.getConditions().size()).append("\n");
sb.append("Results: ").append(trait.getResults().size()).append("\n");
sb.append("Initial BDD nodes: ").append(trait.getBdd().getNodeCount()).append("\n");

// Apply sifting optimization
SiftingOptimization sifting = SiftingOptimization.builder().cfg(cfg).build();
EndpointBddTrait siftedTrait = sifting.apply(trait);
sb.append("After sifting - nodes: ").append(siftedTrait.getBdd().getNodeCount()).append("\n");

// Apply cost optimization
CostOptimization cost = CostOptimization.builder().cfg(cfg).build();
EndpointBddTrait optimizedTrait = cost.apply(siftedTrait);
sb.append("After cost opt - nodes: ").append(optimizedTrait.getBdd().getNodeCount()).append("\n");

// Print conditions for analysis
sb.append("\n=== CONDITIONS ===\n");
for (int i = 0; i < trait.getConditions().size(); i++) {
sb.append(i).append(": ").append(trait.getConditions().get(i)).append("\n");
}

// Print results (endpoints) for analysis
sb.append("\n=== RESULTS ===\n");
for (int i = 0; i < trait.getResults().size(); i++) {
sb.append(i).append(": ").append(trait.getResults().get(i)).append("\n");
}

System.out.println(sb);

// Write model with BDD trait to build directory
writeModelWithBddTrait(optimizedTrait);
}

private void writeModelWithBddTrait(EndpointBddTrait bddTrait) {
String buildDir = System.getProperty("buildDir");
if (buildDir == null) {
System.out.println("buildDir system property not set, skipping model output");
return;
}

// Create updated service with BDD trait instead of RuleSet trait
ServiceShape updatedService = s3Service.toBuilder()
.removeTrait(EndpointRuleSetTrait.ID)
.addTrait(bddTrait)
.build();

// Build updated model
Model updatedModel = model.toBuilder()
.removeShape(s3Service.getId())
.addShape(updatedService)
.build();

// Serialize to JSON
ModelSerializer serializer = ModelSerializer.builder().build();
String json = Node.prettyPrintJson(serializer.serialize(updatedModel));

// Write to build directory
Path outputPath = Paths.get(buildDir, "s3-bdd-model.json");
try {
Path parentDir = outputPath.getParent();
if (parentDir != null) {
Files.createDirectories(parentDir);
}
Files.writeString(outputPath, json);
System.out.println("Wrote S3 BDD model to: " + outputPath);
} catch (IOException e) {
throw new RuntimeException("Failed to write S3 BDD model", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rulesengine.aws.language.functions;

import static org.junit.jupiter.api.Assertions.assertFalse;

import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.rulesengine.aws.s3.S3TreeRewriter;
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.language.evaluation.TestEvaluator;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
import software.amazon.smithy.rulesengine.traits.EndpointTestCase;
import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait;

/**
* Runs the endpoint test cases against the transformed S3 model. We're fixed to a specific version for this test,
* but could periodically bump the version if needed.
*/
class S3TreeRewriterTest {
private static final ShapeId S3_SERVICE_ID = ShapeId.from("com.amazonaws.s3#AmazonS3");

private static EndpointRuleSet originalRules;
private static List<EndpointTestCase> testCases;

@BeforeAll
static void loadS3Model() {
Model model = Model.assembler()
.discoverModels()
.assemble()
.unwrap();

ServiceShape s3Service = model.expectShape(S3_SERVICE_ID, ServiceShape.class);
originalRules = s3Service.expectTrait(EndpointRuleSetTrait.class).getEndpointRuleSet();
testCases = s3Service.expectTrait(EndpointTestsTrait.class).getTestCases();
}

@Test
void transformPreservesEndpointTestSemantics() {
assertFalse(testCases.isEmpty(), "S3 model should have endpoint test cases");

EndpointRuleSet transformed = S3TreeRewriter.transform(originalRules);
for (EndpointTestCase testCase : testCases) {
TestEvaluator.evaluate(transformed, testCase);
}
}
}
Loading
Loading