Skip to content

Commit d32e674

Browse files
committed
Add endpoint rules engine
1 parent 38afeb7 commit d32e674

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5360
-7
lines changed

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ plugins {
77
alias(libs.plugins.jreleaser)
88
}
99

10+
repositories {
11+
mavenLocal()
12+
mavenCentral()
13+
}
14+
1015
task("addGitHooks") {
1116
onlyIf("unix") {
1217
!Os.isFamily(Os.FAMILY_WINDOWS)

buildSrc/src/main/kotlin/smithy-java.module-conventions.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ afterEvaluate {
4949
manifest {
5050
attributes(mapOf("Automatic-Module-Name" to moduleName))
5151
}
52-
duplicatesStrategy = DuplicatesStrategy.FAIL
52+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
5353
}
5454
}
5555

client/client-core/src/main/java/software/amazon/smithy/java/client/core/endpoint/Endpoint.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
import java.net.URI;
99
import java.net.URISyntaxException;
1010
import java.util.List;
11+
import java.util.Map;
1112
import java.util.Set;
1213
import software.amazon.smithy.java.context.Context;
1314

1415
/**
1516
* A resolved endpoint.
1617
*/
1718
public interface Endpoint {
19+
/**
20+
* Assigns headers to an endpoint. These are typically HTTP headers.
21+
*/
22+
Context.Key<Map<String, List<String>>> HEADERS = Context.key("Endpoint headers");
23+
1824
/**
1925
* The endpoint URI.
2026
*
@@ -51,10 +57,13 @@ public interface Endpoint {
5157
*
5258
* @return the builder.
5359
*/
60+
@SuppressWarnings("unchecked")
5461
default Builder toBuilder() {
5562
var builder = new EndpointImpl.Builder();
5663
builder.uri(uri());
57-
properties().forEach(k -> builder.properties.put(k, property(k)));
64+
for (var e : properties()) {
65+
builder.putProperty((Context.Key<Object>) e, property(e));
66+
}
5867
for (EndpointAuthScheme authScheme : authSchemes()) {
5968
builder.addAuthScheme(authScheme);
6069
}

client/client-core/src/main/java/software/amazon/smithy/java/client/core/endpoint/EndpointImpl.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ final class EndpointImpl implements Endpoint {
2323

2424
private EndpointImpl(Builder builder) {
2525
this.uri = Objects.requireNonNull(builder.uri);
26-
this.authSchemes = List.copyOf(builder.authSchemes);
27-
this.properties = Map.copyOf(builder.properties);
26+
this.authSchemes = builder.authSchemes == null ? List.of() : builder.authSchemes;
27+
this.properties = builder.properties == null ? Map.of() : builder.properties;
28+
// Clear out the builder, making this class immutable and the builder still reusable.
29+
builder.authSchemes = null;
30+
builder.properties = null;
2831
}
2932

3033
@Override
@@ -66,11 +69,16 @@ public int hashCode() {
6669
return Objects.hash(uri, authSchemes, properties);
6770
}
6871

72+
@Override
73+
public String toString() {
74+
return "Endpoint{uri=" + uri + ", authSchemes=" + authSchemes + ", properties=" + properties + '}';
75+
}
76+
6977
static final class Builder implements Endpoint.Builder {
7078

7179
private URI uri;
72-
private final List<EndpointAuthScheme> authSchemes = new ArrayList<>();
73-
final Map<Context.Key<?>, Object> properties = new HashMap<>();
80+
private List<EndpointAuthScheme> authSchemes;
81+
private Map<Context.Key<?>, Object> properties;
7482

7583
@Override
7684
public Builder uri(URI uri) {
@@ -89,12 +97,18 @@ public Builder uri(String uri) {
8997

9098
@Override
9199
public Builder addAuthScheme(EndpointAuthScheme authScheme) {
100+
if (this.authSchemes == null) {
101+
this.authSchemes = new ArrayList<>();
102+
}
92103
this.authSchemes.add(authScheme);
93104
return this;
94105
}
95106

96107
@Override
97108
public <T> Builder putProperty(Context.Key<T> property, T value) {
109+
if (this.properties == null) {
110+
this.properties = new HashMap<>();
111+
}
98112
properties.put(property, value);
99113
return this;
100114
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
plugins {
2+
id("smithy-java.module-conventions")
3+
id("me.champeau.jmh") version "0.7.3"
4+
}
5+
6+
description = "Implements the rules engine traits used to resolve endpoints"
7+
8+
extra["displayName"] = "Smithy :: Java :: Client :: Endpoint Rules"
9+
extra["moduleName"] = "software.amazon.smithy.java.client.endpointrules"
10+
11+
dependencies {
12+
api(project(":client:client-core"))
13+
implementation(libs.smithy.rules)
14+
implementation(project(":logging"))
15+
}
16+
17+
jmh {
18+
warmupIterations = 2
19+
iterations = 5
20+
fork = 1
21+
// profilers.add("async:output=flamegraph")
22+
// profilers.add("gc")
23+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.concurrent.TimeUnit;
11+
import org.openjdk.jmh.annotations.Benchmark;
12+
import org.openjdk.jmh.annotations.BenchmarkMode;
13+
import org.openjdk.jmh.annotations.Fork;
14+
import org.openjdk.jmh.annotations.Measurement;
15+
import org.openjdk.jmh.annotations.Mode;
16+
import org.openjdk.jmh.annotations.OutputTimeUnit;
17+
import org.openjdk.jmh.annotations.Param;
18+
import org.openjdk.jmh.annotations.Scope;
19+
import org.openjdk.jmh.annotations.Setup;
20+
import org.openjdk.jmh.annotations.State;
21+
import org.openjdk.jmh.annotations.Warmup;
22+
import software.amazon.smithy.java.context.Context;
23+
import software.amazon.smithy.model.node.Node;
24+
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
25+
import software.amazon.smithy.utils.IoUtils;
26+
27+
@State(Scope.Benchmark)
28+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
29+
@BenchmarkMode(Mode.AverageTime)
30+
@Warmup(
31+
iterations = 2,
32+
time = 3,
33+
timeUnit = TimeUnit.SECONDS)
34+
@Measurement(
35+
iterations = 3,
36+
time = 3,
37+
timeUnit = TimeUnit.SECONDS)
38+
@Fork(1)
39+
public class VmBench {
40+
41+
private static final Map<String, Map<String, Object>> CASES = Map.ofEntries(
42+
Map.entry("example-complex-ruleset.json-1",
43+
Map.of(
44+
"Endpoint",
45+
"https://example.com",
46+
"UseFIPS",
47+
false)),
48+
Map.entry("minimal-ruleset.json-1", Map.of("Region", "us-east-1")));
49+
50+
@Param({
51+
"yes",
52+
"no",
53+
})
54+
private String optimize;
55+
56+
@Param({
57+
"example-complex-ruleset.json-1",
58+
"minimal-ruleset.json-1"
59+
})
60+
private String testName;
61+
62+
private EndpointRuleSet ruleSet;
63+
private Map<String, Object> parameters;
64+
private RulesProgram program;
65+
private Context ctx;
66+
67+
@Setup
68+
public void setup() throws Exception {
69+
parameters = new HashMap<>(CASES.get(testName));
70+
var actualFile = testName.substring(0, testName.length() - 2);
71+
var url = VmBench.class.getResource(actualFile);
72+
if (url == null) {
73+
throw new RuntimeException("Test case not found: " + actualFile);
74+
}
75+
var data = Node.parse(IoUtils.readUtf8Url(url));
76+
ruleSet = EndpointRuleSet.fromNode(data);
77+
78+
var engine = new RulesEngine();
79+
if (optimize.equals("nop")) {
80+
engine.disableOptimizations();
81+
}
82+
program = engine.compile(ruleSet);
83+
ctx = Context.create();
84+
}
85+
86+
// @Benchmark
87+
// public Object compile() {
88+
// // TODO
89+
// return null;
90+
// }
91+
92+
@Benchmark
93+
public Object evaluate() {
94+
return program.resolveEndpoint(ctx, parameters);
95+
}
96+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import java.net.URI;
9+
import java.util.List;
10+
import java.util.Map;
11+
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.GetAttr;
12+
13+
sealed interface AttrExpression {
14+
15+
Object apply(Object o);
16+
17+
static AttrExpression from(GetAttr getAttr) {
18+
var path = getAttr.getPath();
19+
20+
// Set the toString value on the final result.
21+
String str = getAttr.toString(); // in the form of something#path
22+
int position = str.lastIndexOf('#');
23+
var tostringValue = str.substring(position + 1);
24+
25+
if (path.isEmpty()) {
26+
throw new UnsupportedOperationException("Invalid getAttr expression: requires at least one part");
27+
} else if (path.size() == 1) {
28+
return new ToString(tostringValue, from(getAttr.getPath().get(0)));
29+
}
30+
31+
// Parse the multi-level expression ("foo.bar.baz[9]").
32+
var result = new AndThen(from(path.get(0)), from(path.get(1)));
33+
for (var i = 2; i < path.size(); i++) {
34+
result = new AndThen(result, from(path.get(i)));
35+
}
36+
37+
return new ToString(tostringValue, result);
38+
}
39+
40+
private static AttrExpression from(GetAttr.Part part) {
41+
if (part instanceof GetAttr.Part.Key k) {
42+
return new GetKey(k.key().toString());
43+
} else if (part instanceof GetAttr.Part.Index i) {
44+
return new GetIndex(i.index());
45+
} else {
46+
throw new UnsupportedOperationException("Unexpected GetAttr part: " + part);
47+
}
48+
}
49+
50+
static AttrExpression parse(String value) {
51+
var values = value.split("\\.");
52+
53+
// Parse a single-level expression ("foo" or "bar[0]").
54+
if (values.length == 1) {
55+
return new ToString(value, parsePart(value));
56+
}
57+
58+
// Parse the multi-level expression ("foo.bar.baz[9]").
59+
var result = new AndThen(parsePart(values[0]), parsePart(values[1]));
60+
for (var i = 2; i < values.length; i++) {
61+
result = new AndThen(result, parsePart(values[i]));
62+
}
63+
64+
// Set the toString value on the final result.
65+
return new ToString(value, result);
66+
}
67+
68+
private static AttrExpression parsePart(String part) {
69+
int position = part.indexOf('[');
70+
if (position == -1) {
71+
return new GetKey(part);
72+
} else {
73+
String numberString = part.substring(position + 1, part.length() - 1);
74+
int index = Integer.parseInt(numberString);
75+
String key = part.substring(0, position);
76+
return new AndThen(new GetKey(key), new GetIndex(index));
77+
}
78+
}
79+
80+
record ToString(String original, AttrExpression delegate) implements AttrExpression {
81+
@Override
82+
public Object apply(Object o) {
83+
return delegate.apply(o);
84+
}
85+
86+
@Override
87+
public String toString() {
88+
return original;
89+
}
90+
}
91+
92+
record AndThen(AttrExpression left, AttrExpression right) implements AttrExpression {
93+
@Override
94+
public Object apply(Object o) {
95+
var result = left.apply(o);
96+
if (result != null) {
97+
result = right.apply(result);
98+
}
99+
return result;
100+
}
101+
}
102+
103+
record GetKey(String key) implements AttrExpression {
104+
@Override
105+
@SuppressWarnings("rawtypes")
106+
public Object apply(Object o) {
107+
if (o instanceof Map m) {
108+
return m.get(key);
109+
} else if (o instanceof URI u) {
110+
return EndpointUtils.getUriProperty(u, key);
111+
} else {
112+
return null;
113+
}
114+
}
115+
}
116+
117+
record GetIndex(int index) implements AttrExpression {
118+
@Override
119+
@SuppressWarnings("rawtypes")
120+
public Object apply(Object o) {
121+
if (o instanceof List l && l.size() > index) {
122+
return l.get(index);
123+
}
124+
return null;
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)