Skip to content

Commit d5ab77a

Browse files
George-iamgeobon
andauthored
feat: add users-family beta kickoff for Java SDK (#2)
Bootstrap the Java SDK with users contract methods, unit tests, and CI so Beta parity can expand from a tested baseline. Made-with: Cursor Co-authored-by: George-iam <georgeb@gmail.com>
1 parent 2185dcc commit d5ab77a

8 files changed

Lines changed: 447 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: test
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-java@v4
15+
with:
16+
distribution: temurin
17+
java-version: "17"
18+
- name: Run Maven tests
19+
run: mvn -B test

README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,49 @@ Official Java SDK for Axme APIs and workflows.
44

55
## Status
66

7-
Repository bootstrap in progress.
7+
Beta parity kickoff in progress.
8+
9+
## Quickstart
10+
11+
```java
12+
import dev.axme.sdk.AxmeClient;
13+
import dev.axme.sdk.AxmeClientConfig;
14+
import dev.axme.sdk.RequestOptions;
15+
import java.util.Map;
16+
17+
public class Quickstart {
18+
public static void main(String[] args) throws Exception {
19+
AxmeClient client = new AxmeClient(new AxmeClientConfig("https://gateway.example.com", "YOUR_API_KEY"));
20+
21+
Map<String, Object> registered =
22+
client.registerNick(
23+
Map.of("nick", "@partner.user", "display_name", "Partner User"),
24+
new RequestOptions("nick-register-001", null));
25+
26+
Map<String, Object> check = client.checkNick("@partner.user", RequestOptions.none());
27+
28+
Map<String, Object> renamed =
29+
client.renameNick(
30+
Map.of("owner_agent", registered.get("owner_agent"), "nick", "@partner.new"),
31+
new RequestOptions("nick-rename-001", null));
32+
33+
Map<String, Object> profile =
34+
client.getUserProfile((String) registered.get("owner_agent"), RequestOptions.none());
35+
36+
Map<String, Object> updated =
37+
client.updateUserProfile(
38+
Map.of("owner_agent", profile.get("owner_agent"), "display_name", "Partner User Updated"),
39+
new RequestOptions("profile-update-001", null));
40+
41+
System.out.println(check);
42+
System.out.println(renamed);
43+
System.out.println(updated);
44+
}
45+
}
46+
```
47+
48+
## Development
49+
50+
```bash
51+
mvn test
52+
```

pom.xml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>dev.axme</groupId>
8+
<artifactId>axme-sdk-java</artifactId>
9+
<version>0.1.0</version>
10+
<name>axme-sdk-java</name>
11+
<description>Official Java SDK for Axme APIs and workflows.</description>
12+
13+
<properties>
14+
<maven.compiler.source>11</maven.compiler.source>
15+
<maven.compiler.target>11</maven.compiler.target>
16+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
17+
<junit.version>5.10.2</junit.version>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.fasterxml.jackson.core</groupId>
23+
<artifactId>jackson-databind</artifactId>
24+
<version>2.17.1</version>
25+
</dependency>
26+
27+
<dependency>
28+
<groupId>org.junit.jupiter</groupId>
29+
<artifactId>junit-jupiter</artifactId>
30+
<version>${junit.version}</version>
31+
<scope>test</scope>
32+
</dependency>
33+
<dependency>
34+
<groupId>com.squareup.okhttp3</groupId>
35+
<artifactId>mockwebserver</artifactId>
36+
<version>4.12.0</version>
37+
<scope>test</scope>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<groupId>org.apache.maven.plugins</groupId>
45+
<artifactId>maven-surefire-plugin</artifactId>
46+
<version>3.2.5</version>
47+
<configuration>
48+
<useModulePath>false</useModulePath>
49+
</configuration>
50+
</plugin>
51+
</plugins>
52+
</build>
53+
</project>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package dev.axme.sdk;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import java.io.IOException;
6+
import java.net.URI;
7+
import java.net.URLEncoder;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.LinkedHashMap;
13+
import java.util.Map;
14+
15+
public final class AxmeClient {
16+
private final String baseUrl;
17+
private final String apiKey;
18+
private final HttpClient httpClient;
19+
private final ObjectMapper objectMapper = new ObjectMapper();
20+
21+
public AxmeClient(AxmeClientConfig config) {
22+
this(config, HttpClient.newHttpClient());
23+
}
24+
25+
public AxmeClient(AxmeClientConfig config, HttpClient httpClient) {
26+
this.baseUrl = config.getBaseUrl();
27+
this.apiKey = config.getApiKey();
28+
this.httpClient = httpClient;
29+
}
30+
31+
public Map<String, Object> registerNick(Map<String, Object> payload, RequestOptions options)
32+
throws IOException, InterruptedException {
33+
return requestJson("POST", "/v1/users/register-nick", Map.of(), payload, normalizeOptions(options));
34+
}
35+
36+
public Map<String, Object> checkNick(String nick, RequestOptions options)
37+
throws IOException, InterruptedException {
38+
return requestJson("GET", "/v1/users/check-nick", Map.of("nick", nick), null, normalizeOptions(options));
39+
}
40+
41+
public Map<String, Object> renameNick(Map<String, Object> payload, RequestOptions options)
42+
throws IOException, InterruptedException {
43+
return requestJson("POST", "/v1/users/rename-nick", Map.of(), payload, normalizeOptions(options));
44+
}
45+
46+
public Map<String, Object> getUserProfile(String ownerAgent, RequestOptions options)
47+
throws IOException, InterruptedException {
48+
return requestJson(
49+
"GET",
50+
"/v1/users/profile",
51+
Map.of("owner_agent", ownerAgent),
52+
null,
53+
normalizeOptions(options));
54+
}
55+
56+
public Map<String, Object> updateUserProfile(Map<String, Object> payload, RequestOptions options)
57+
throws IOException, InterruptedException {
58+
return requestJson("POST", "/v1/users/profile/update", Map.of(), payload, normalizeOptions(options));
59+
}
60+
61+
private Map<String, Object> requestJson(
62+
String method,
63+
String path,
64+
Map<String, String> query,
65+
Map<String, Object> payload,
66+
RequestOptions options)
67+
throws IOException, InterruptedException {
68+
HttpRequest.Builder builder =
69+
HttpRequest.newBuilder()
70+
.uri(URI.create(buildUrl(path, query)))
71+
.method(method, payload == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
72+
.header("Authorization", "Bearer " + apiKey)
73+
.header("Accept", "application/json");
74+
75+
if (payload != null) {
76+
builder.header("Content-Type", "application/json");
77+
}
78+
if (!isBlank(options.getIdempotencyKey())) {
79+
builder.header("Idempotency-Key", options.getIdempotencyKey());
80+
}
81+
if (!isBlank(options.getTraceId())) {
82+
builder.header("X-Trace-Id", options.getTraceId());
83+
}
84+
85+
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
86+
if (response.statusCode() < 200 || response.statusCode() >= 300) {
87+
throw new AxmeHttpException(response.statusCode(), response.body());
88+
}
89+
if (response.body() == null || response.body().trim().isEmpty()) {
90+
return Map.of();
91+
}
92+
93+
return objectMapper.readValue(response.body(), new TypeReference<Map<String, Object>>() {});
94+
}
95+
96+
private String buildUrl(String path, Map<String, String> query) {
97+
if (query == null || query.isEmpty()) {
98+
return baseUrl + path;
99+
}
100+
101+
Map<String, String> filtered = new LinkedHashMap<>();
102+
for (Map.Entry<String, String> entry : query.entrySet()) {
103+
if (!isBlank(entry.getValue())) {
104+
filtered.put(entry.getKey(), entry.getValue());
105+
}
106+
}
107+
if (filtered.isEmpty()) {
108+
return baseUrl + path;
109+
}
110+
111+
StringBuilder builder = new StringBuilder(baseUrl).append(path).append("?");
112+
boolean first = true;
113+
for (Map.Entry<String, String> entry : filtered.entrySet()) {
114+
if (!first) {
115+
builder.append("&");
116+
}
117+
first = false;
118+
builder
119+
.append(urlEncode(entry.getKey()))
120+
.append("=")
121+
.append(urlEncode(entry.getValue()));
122+
}
123+
return builder.toString();
124+
}
125+
126+
private static String urlEncode(String value) {
127+
return URLEncoder.encode(value, StandardCharsets.UTF_8);
128+
}
129+
130+
private static RequestOptions normalizeOptions(RequestOptions options) {
131+
return options == null ? RequestOptions.none() : options;
132+
}
133+
134+
private static boolean isBlank(String value) {
135+
return value == null || value.trim().isEmpty();
136+
}
137+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.axme.sdk;
2+
3+
public final class AxmeClientConfig {
4+
private final String baseUrl;
5+
private final String apiKey;
6+
7+
public AxmeClientConfig(String baseUrl, String apiKey) {
8+
if (baseUrl == null || baseUrl.trim().isEmpty()) {
9+
throw new IllegalArgumentException("baseUrl is required");
10+
}
11+
if (apiKey == null || apiKey.trim().isEmpty()) {
12+
throw new IllegalArgumentException("apiKey is required");
13+
}
14+
this.baseUrl = trimTrailingSlash(baseUrl.trim());
15+
this.apiKey = apiKey.trim();
16+
}
17+
18+
public String getBaseUrl() {
19+
return baseUrl;
20+
}
21+
22+
public String getApiKey() {
23+
return apiKey;
24+
}
25+
26+
private static String trimTrailingSlash(String value) {
27+
if (value.endsWith("/")) {
28+
return value.substring(0, value.length() - 1);
29+
}
30+
return value;
31+
}
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.axme.sdk;
2+
3+
public final class AxmeHttpException extends RuntimeException {
4+
private final int statusCode;
5+
private final String responseBody;
6+
7+
public AxmeHttpException(int statusCode, String responseBody) {
8+
super("axme request failed with status " + statusCode);
9+
this.statusCode = statusCode;
10+
this.responseBody = responseBody;
11+
}
12+
13+
public int getStatusCode() {
14+
return statusCode;
15+
}
16+
17+
public String getResponseBody() {
18+
return responseBody;
19+
}
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.axme.sdk;
2+
3+
public final class RequestOptions {
4+
private final String idempotencyKey;
5+
private final String traceId;
6+
7+
public RequestOptions() {
8+
this(null, null);
9+
}
10+
11+
public RequestOptions(String idempotencyKey, String traceId) {
12+
this.idempotencyKey = idempotencyKey;
13+
this.traceId = traceId;
14+
}
15+
16+
public String getIdempotencyKey() {
17+
return idempotencyKey;
18+
}
19+
20+
public String getTraceId() {
21+
return traceId;
22+
}
23+
24+
public static RequestOptions none() {
25+
return new RequestOptions();
26+
}
27+
}

0 commit comments

Comments
 (0)