Skip to content

Commit a46e7e8

Browse files
committed
sprintf builtin
1 parent c8f9fed commit a46e7e8

File tree

5 files changed

+163
-1
lines changed

5 files changed

+163
-1
lines changed

core/src/main/java/com/styra/opa/wasm/builtins/Provided.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ private static OpaBuiltin.Builtin[] merge(
2121
}
2222

2323
public static OpaBuiltin.Builtin[] all() {
24-
return all(Json.all(), Yaml.all());
24+
return all(String.all(), Json.all(), Yaml.all());
2525
}
2626
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.styra.opa.wasm.builtins;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.node.TextNode;
6+
import com.styra.opa.wasm.OpaBuiltin;
7+
import com.styra.opa.wasm.OpaWasm;
8+
import java.math.BigInteger;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
public class String {
13+
14+
private static JsonNode sprintfImpl(OpaWasm instance, JsonNode operand0, JsonNode operand1) {
15+
// Implementation from:
16+
// https://github.com/open-policy-agent/opa/blob/269a118ad9f1b5673c68e97803d722fd4e28c0a1/v1/topdown/strings.go#L525
17+
// Validate the first operand as a string
18+
if (!operand0.isTextual()) {
19+
throw new IllegalArgumentException("Operand 1 must be a string.");
20+
}
21+
var format = operand0.asText();
22+
23+
// Validate the second operand as an array
24+
if (!operand1.isArray()) {
25+
throw new IllegalArgumentException("Operand 2 must be an array.");
26+
}
27+
var arr = new ArrayList<JsonNode>();
28+
var iter = operand1.elements();
29+
while (iter.hasNext()) {
30+
arr.add(iter.next());
31+
}
32+
33+
// Optimized path for "to_string" for a single integer, e.g., sprintf("%d", [x])
34+
if ("%d".equals(format) && arr.size() == 1) {
35+
JsonNode firstElement = arr.get(0);
36+
if (firstElement.isNumber()) {
37+
try {
38+
return instance.jsonMapper().readTree(firstElement.numberValue().toString());
39+
} catch (JsonProcessingException e) {
40+
throw new RuntimeException(e);
41+
}
42+
}
43+
}
44+
45+
// Convert array elements into arguments
46+
List<Object> args = new ArrayList<>();
47+
for (JsonNode element : arr) {
48+
if (element.isInt()) {
49+
args.add(element.asInt());
50+
} else if (element.isBigInteger()) {
51+
args.add(new BigInteger(element.asText()));
52+
} else if (element.isDouble()) {
53+
args.add(element.asDouble());
54+
} else if (element.isTextual()) {
55+
args.add(element.asText());
56+
} else {
57+
args.add(element.toString());
58+
}
59+
}
60+
61+
// Apply formatting using String.format
62+
try {
63+
var result = java.lang.String.format(format, args.toArray());
64+
return TextNode.valueOf(result);
65+
} catch (IllegalArgumentException e) {
66+
throw new RuntimeException("Formatting error: " + e.getMessage());
67+
}
68+
}
69+
70+
public static final OpaBuiltin.Builtin sprintf =
71+
OpaBuiltin.from("sprintf", String::sprintfImpl);
72+
73+
public static OpaBuiltin.Builtin[] all() {
74+
return new OpaBuiltin.Builtin[] {sprintf};
75+
}
76+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.styra.opa.wasm;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.nio.file.Path;
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.Test;
8+
9+
public class OpaStringBuiltinsTest {
10+
static Path wasmFile;
11+
12+
@BeforeAll
13+
public static void beforeAll() throws Exception {
14+
wasmFile =
15+
OpaCli.compile(
16+
"string-builtins",
17+
"string_builtins/invoke_sprintf",
18+
"string_builtins/integer_fastpath",
19+
"string_builtins/string_example")
20+
.resolve("policy.wasm");
21+
}
22+
23+
@Test
24+
public void sprintf() {
25+
var opa = OpaPolicy.builder().withPolicy(wasmFile).build();
26+
27+
var result = Utils.getResult(opa.entrypoint("string_builtins/invoke_sprintf").evaluate());
28+
29+
assertEquals("hello user your number is 321!", result.get("printed").asText());
30+
}
31+
32+
@Test
33+
public void integerFastPath() {
34+
var opa = OpaPolicy.builder().withPolicy(wasmFile).build();
35+
36+
var result = Utils.getResult(opa.entrypoint("string_builtins/integer_fastpath").evaluate());
37+
38+
assertEquals(123, result.get("printed").asInt());
39+
}
40+
41+
@Test
42+
public void stringExample() {
43+
var opa = OpaPolicy.builder().withPolicy(wasmFile).build();
44+
45+
var result = Utils.getResult(opa.entrypoint("string_builtins/string_example").evaluate());
46+
47+
assertEquals("my string", result.get("printed").asText());
48+
}
49+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"builtins": [
3+
{
4+
"name": "sprintf",
5+
"decl": {
6+
"args": [
7+
{
8+
"type": "string"
9+
},
10+
{
11+
"dynamic": {
12+
"type": "any"
13+
},
14+
"type": "array"
15+
}
16+
],
17+
"result": {
18+
"type": "string"
19+
},
20+
"type": "function"
21+
}
22+
}
23+
]
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package string_builtins
2+
3+
invoke_sprintf = x {
4+
x = { "printed": sprintf("hello %s your number is %d!", ["user", 321]) }
5+
}
6+
7+
integer_fastpath = x {
8+
x = { "printed": sprintf("%d", [123]) }
9+
}
10+
11+
string_example = x {
12+
x = { "printed": sprintf("%s", ["my string"]) }
13+
}

0 commit comments

Comments
 (0)