Skip to content

Commit b9337f7

Browse files
authored
feat: APIExecutor for calling raw arbitrary endpoints (#273)
* feat: raw request builder * fix: fmt * feat: raw requests integration tests * fix: rawapi doc * feat: calling other endpoints section * fix: typo in readme * feat: refactor examples * fix: refactor example * fix: comment ' * feat: use list stores via raw req * feat: refactor add typed resp in example * fix: spotless fmt * feat: address copilot comments * feat: address coderabbit comments * fix: use gradle 8.2.1 for example * feat: use build buulder chain for consistency * feat: rename and refactor to APIExecutor for consistency * fix: rename consistent naming in docs * fix: changelog * fix: naming * fix: naming convention in example
1 parent 68211ed commit b9337f7

19 files changed

Lines changed: 2234 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
### Added
1010
- feat: support for streamed list objects (#252, #272)
1111

12+
### Added
13+
- Introduced `ApiExecutor` for executing custom HTTP requests to OpenFGA API endpoints
14+
1215
## v0.9.4
1316

1417
### [0.9.4](https://github.com/openfga/java-sdk/compare/v0.9.3...v0.9.4) (2025-12-05)

README.md

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the
4646
- [Assertions](#assertions)
4747
- [Read Assertions](#read-assertions)
4848
- [Write Assertions](#write-assertions)
49+
- [Calling Other Endpoints](#calling-other-endpoints)
4950
- [Retries](#retries)
5051
- [API Endpoints](#api-endpoints)
5152
- [Models](#models)
@@ -746,7 +747,7 @@ Similar to [check](#check), but instead of checking a single user-object relatio
746747
> Passing `ClientBatchCheckOptions` is optional. All fields of `ClientBatchCheckOptions` are optional.
747748
748749
```java
749-
var reequst = new ClientBatchCheckRequest().checks(
750+
var request = new ClientBatchCheckRequest().checks(
750751
List.of(
751752
new ClientBatchCheckItem()
752753
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
@@ -774,7 +775,7 @@ var reequst = new ClientBatchCheckRequest().checks(
774775
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
775776
.relation("creator")
776777
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
777-
.correlationId("cor-3), // optional, one will be generated for you if not provided
778+
.correlationId("cor-3"), // optional, one will be generated for you if not provided
778779
new ClientCheckRequest()
779780
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
780781
.relation("deleter")
@@ -1167,6 +1168,89 @@ try {
11671168
}
11681169
```
11691170

1171+
### Calling Other Endpoints
1172+
1173+
The API Executor provides direct HTTP access to OpenFGA endpoints not yet wrapped by the SDK. It maintains the SDK's client configuration including authentication, telemetry, retries, and error handling.
1174+
1175+
Use cases:
1176+
- Calling endpoints not yet supported by the SDK
1177+
- Using an SDK version that lacks support for a particular endpoint
1178+
- Accessing custom endpoints that extend the OpenFGA API
1179+
1180+
Initialize the SDK normally and access the API Executor via the `fgaClient` instance:
1181+
1182+
```java
1183+
// Initialize the client, same as above
1184+
ClientConfiguration config = new ClientConfiguration()
1185+
.apiUrl("http://localhost:8080")
1186+
.storeId("01YCP46JKYM8FJCQ37NMBYHE5X");
1187+
OpenFgaClient fgaClient = new OpenFgaClient(config);
1188+
1189+
// Custom new endpoint that doesn't exist in the SDK yet
1190+
Map<String, Object> requestBody = Map.of(
1191+
"user", "user:bob",
1192+
"action", "custom_action",
1193+
"resource", "resource:123"
1194+
);
1195+
1196+
// Build the request
1197+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/custom-endpoint")
1198+
.pathParam("store_id", storeId)
1199+
.queryParam("page_size", "20")
1200+
.queryParam("continuation_token", "eyJwayI6...")
1201+
.body(requestBody)
1202+
.header("X-Experimental-Feature", "enabled")
1203+
.build();
1204+
```
1205+
1206+
#### Example: Calling a new "Custom Endpoint" endpoint and handling raw response
1207+
1208+
```java
1209+
// Get raw response without automatic decoding
1210+
ApiResponse<String> rawResponse = fgaClient.apiExecutor().send(request).get();
1211+
1212+
String rawJson = rawResponse.getData();
1213+
System.out.println("Response: " + rawJson);
1214+
1215+
// You can access fields like headers, status code, etc. from rawResponse:
1216+
System.out.println("Status Code: " + rawResponse.getStatusCode());
1217+
System.out.println("Headers: " + rawResponse.getHeaders());
1218+
```
1219+
1220+
#### Example: Calling a new "Custom Endpoint" endpoint and decoding response into a struct
1221+
1222+
```java
1223+
// Define a class to hold the response
1224+
class CustomEndpointResponse {
1225+
private boolean allowed;
1226+
private String reason;
1227+
1228+
public boolean isAllowed() { return allowed; }
1229+
public void setAllowed(boolean allowed) { this.allowed = allowed; }
1230+
public String getReason() { return reason; }
1231+
public void setReason(String reason) { this.reason = reason; }
1232+
}
1233+
1234+
// Get response decoded into CustomEndpointResponse class
1235+
ApiResponse<CustomEndpointResponse> response = fgaClient.apiExecutor()
1236+
.send(request, CustomEndpointResponse.class)
1237+
.get();
1238+
1239+
CustomEndpointResponse customEndpointResponse = response.getData();
1240+
System.out.println("Allowed: " + customEndpointResponse.isAllowed());
1241+
System.out.println("Reason: " + customEndpointResponse.getReason());
1242+
1243+
// You can access fields like headers, status code, etc. from response:
1244+
System.out.println("Status Code: " + response.getStatusCode());
1245+
System.out.println("Headers: " + response.getHeaders());
1246+
```
1247+
1248+
For a complete working example, see [examples/api-executor](examples/api-executor).
1249+
1250+
#### Documentation
1251+
1252+
See [docs/ApiExecutor.md](docs/ApiExecutor.md) for complete API reference and examples.
1253+
11701254
### API Endpoints
11711255

11721256
| Method | HTTP request | Description |

docs/ApiExecutor.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# API Executor
2+
3+
Direct HTTP access to OpenFGA endpoints.
4+
5+
## Quick Start
6+
7+
```java
8+
OpenFgaClient client = new OpenFgaClient(config);
9+
10+
// Build request
11+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/check")
12+
.pathParam("store_id", storeId)
13+
.body(Map.of("tuple_key", Map.of("user", "user:jon", "relation", "reader", "object", "doc:1")))
14+
.build();
15+
16+
// Execute - typed response
17+
ApiResponse<CheckResponse> response = client.apiExecutor().send(request, CheckResponse.class).get();
18+
19+
// Execute - raw JSON
20+
ApiResponse<String> rawResponse = client.apiExecutor().send(request).get();
21+
```
22+
23+
## API Reference
24+
25+
### ApiExecutorRequestBuilder
26+
27+
**Factory:**
28+
```java
29+
ApiExecutorRequestBuilder.builder(String method, String path)
30+
```
31+
32+
**Methods:**
33+
```java
34+
.pathParam(String key, String value) // Replace {key} in path, URL-encoded
35+
.queryParam(String key, String value) // Add query parameter, URL-encoded
36+
.header(String key, String value) // Add HTTP header
37+
.body(Object body) // Set request body (auto-serialized to JSON)
38+
.build() // Complete the builder (required)
39+
```
40+
41+
**Example:**
42+
```java
43+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/write")
44+
.pathParam("store_id", "01ABC")
45+
.queryParam("dry_run", "true")
46+
.header("X-Request-ID", "uuid")
47+
.body(requestObject)
48+
.build();
49+
```
50+
51+
### ApiExecutor
52+
53+
**Access:**
54+
```java
55+
ApiExecutor apiExecutor = client.apiExecutor();
56+
```
57+
58+
**Methods:**
59+
```java
60+
CompletableFuture<ApiResponse<String>> send(ApiExecutorRequestBuilder request)
61+
CompletableFuture<ApiResponse<T>> send(ApiExecutorRequestBuilder request, Class<T> responseType)
62+
```
63+
64+
### ApiResponse<T>
65+
66+
```java
67+
int getStatusCode() // HTTP status
68+
Map<String, List<String>> getHeaders() // Response headers
69+
String getRawResponse() // Raw JSON body
70+
T getData() // Deserialized data
71+
```
72+
73+
## Examples
74+
75+
### GET Request
76+
```java
77+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("GET", "/stores/{store_id}/feature")
78+
.pathParam("store_id", storeId)
79+
.build();
80+
81+
client.apiExecutor().send(request, FeatureResponse.class)
82+
.thenAccept(r -> System.out.println("Status: " + r.getStatusCode()));
83+
```
84+
85+
### POST with Body
86+
```java
87+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/bulk-delete")
88+
.pathParam("store_id", storeId)
89+
.queryParam("force", "true")
90+
.body(new BulkDeleteRequest("2023-01-01", "user", 1000))
91+
.build();
92+
93+
client.apiExecutor().send(request, BulkDeleteResponse.class).get();
94+
```
95+
96+
### Raw JSON Response
97+
```java
98+
ApiResponse<String> response = client.apiExecutor().send(request).get();
99+
String json = response.getRawResponse(); // Raw JSON
100+
```
101+
102+
### Query Parameters
103+
```java
104+
ApiExecutorRequestBuilder.builder("GET", "/stores/{store_id}/items")
105+
.pathParam("store_id", storeId)
106+
.queryParam("page", "1")
107+
.queryParam("limit", "50")
108+
.queryParam("sort", "created_at")
109+
.build();
110+
```
111+
112+
### Custom Headers
113+
```java
114+
ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/action")
115+
.header("X-Request-ID", UUID.randomUUID().toString())
116+
.header("X-Idempotency-Key", "key-123")
117+
.body(data)
118+
.build();
119+
```
120+
121+
### Error Handling
122+
```java
123+
client.apiExecutor().send(request, ResponseType.class)
124+
.exceptionally(e -> {
125+
if (e.getCause() instanceof FgaError) {
126+
FgaError error = (FgaError) e.getCause();
127+
System.err.println("API Error: " + error.getStatusCode());
128+
}
129+
return null;
130+
});
131+
```
132+
133+
### Map as Request Body
134+
```java
135+
ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/settings")
136+
.pathParam("store_id", storeId)
137+
.body(Map.of(
138+
"setting", "value",
139+
"enabled", true,
140+
"threshold", 100,
141+
"options", List.of("opt1", "opt2")
142+
))
143+
.build();
144+
```
145+
146+
## Notes
147+
148+
- Path/query parameters are URL-encoded automatically
149+
- Authentication tokens injected from client config
150+
- `{store_id}` auto-replaced if not provided via `.pathParam()`
151+
152+
## Migration to Typed Methods
153+
154+
When SDK adds typed methods for an endpoint, you can migrate from API Executor:
155+
156+
```java
157+
// API Executor
158+
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/check")
159+
.body(req)
160+
.build();
161+
162+
client.apiExecutor().send(request, CheckResponse.class).get();
163+
164+
// Typed SDK (when available)
165+
client.check(req).get();
166+
```
167+

examples/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ A simple example that creates a store, runs a set of calls against it including
99

1010
#### OpenTelemetry Examples
1111
- `opentelemetry/` - Demonstrates OpenTelemetry integration both via manual code configuration, as well as no-code instrumentation using the OpenTelemetry java agent
12+
13+
#### Streaming Examples
14+
- `streamed-list-objects/` - Demonstrates using the StreamedListObjects API to retrieve large result sets without pagination limits
15+
16+
#### API Executor Examples
17+
- `api-executor/` - Demonstrates direct HTTP access to OpenFGA endpoints not yet wrapped by the SDK, maintaining SDK configuration (authentication, retries, error handling)
18+

examples/api-executor/Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.PHONY: build run run-openfga
2+
all: build
3+
4+
project_name=.
5+
openfga_version=latest
6+
language=java
7+
8+
build:
9+
../../gradlew -P language=$(language) build
10+
11+
run:
12+
../../gradlew -P language=$(language) run
13+
14+
run-openfga:
15+
docker pull docker.io/openfga/openfga:${openfga_version} && \
16+
docker run -p 8080:8080 docker.io/openfga/openfga:${openfga_version} run
17+

0 commit comments

Comments
 (0)