From d2da88b2132a12477ddb2c9a3342200849c189ae Mon Sep 17 00:00:00 2001 From: Odunlami Zacchaeus Date: Tue, 15 Jul 2025 17:11:59 +0100 Subject: [PATCH 1/2] fix type erasure --- README.md | 76 +++++++++++++ .../io/github/odunlamizo/jsonbin/App.java | 5 +- .../jsonbin/okhttp/AuthInterceptor.java | 18 ++- .../jsonbin/okhttp/JsonBinOkHttp.java | 74 +++++++++++-- .../jsonbin/okhttp/JsonBinOkHttpTest.java | 103 ++++++++++++++++++ .../odunlamizo/jsonbin/util/JsonUtilTest.java | 69 ++++++++++++ 6 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java create mode 100644 src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java diff --git a/README.md b/README.md index 9ce4d62..73249d7 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,79 @@ [![codecov](https://codecov.io/gh/OdunlamiZO/java-jsonbin/graph/badge.svg?token=HULR9R4NAH)](https://codecov.io/gh/OdunlamiZO/java-jsonbin) +A minimal and type-safe Java SDK for interacting with JsonBin.io. This SDK allows developers to read and deserialize JSON documents from public or private bins using a clean, strongly-typed API. + +## Requirements + +- Java 17 or higher +- Maven (for dependency management) + +## Installation + +1. Add the GitHub Maven repository to your `pom.xml`: + +```xml + + + github + GitHub Packages - java-jsonbin + https://maven.pkg.github.com/OdunlamiZO/java-jsonbin + + +``` + +2. Add the dependency: + +```xml + + io.github.odunlamizo + java-jsonbin + 1.0-SNAPSHOT + +``` + +3. Configure GitHub credentials in your Maven settings.xml (usually located at ~/.m2/settings.xml): + +```xml + + + + github + YOUR_GITHUB_USERNAME + YOUR_PERSONAL_ACCESS_TOKEN + + + +``` + +> 📌    Step 1 & 3 is required for now, since we are only deploying to github packages. + +## Getting Started + +### Setup + +```java +JsonBin jsonBin = + new JsonBinOkHttp.Builder().withMasterKey("JSONBIN_MASTER_KEY").build(UserList.class); +``` + +### Example: Calling the API + +Here's a basic example of using the SDK to read a bin from JSONBin.io: + +```java +Bin bin = jsonBin.readBin("687644d36063391d31ae163f"); +System.out.println(bin); +// Bin(record={users=[{name=Morounfoluwa Mary, age=19}]}, metadata=Metadata(id=687644d36063391d31ae163f, _private=false, createdAt=2025-07-15T12:08:51.887Z, name=Java SDK Test)) +``` + +## Contributing + +We welcome contributions to improve this SDK! To contribute: + +1. **Fork** the repository. +2. **Create a new branch** for your feature or bugfix. +3. Make your changes and write appropriate tests. +4. **Open a Pull Request (PR)** to the `main` branch with a clear description of your changes. + +> 📌   Please ensure your code adheres to the project's style and passes all tests before submitting a PR. \ No newline at end of file diff --git a/src/main/java/io/github/odunlamizo/jsonbin/App.java b/src/main/java/io/github/odunlamizo/jsonbin/App.java index 06c1e4e..37fe419 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/App.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/App.java @@ -20,8 +20,9 @@ public static void main(String[] args) { System.exit(1); } - JsonBin jsonBin = new JsonBinOkHttp<>(masterKey); + JsonBin jsonBin = + new JsonBinOkHttp.Builder().withMasterKey(masterKey).build(UserList.class); Bin bin = jsonBin.readBin("687644d36063391d31ae163f"); - System.out.println(bin); + System.out.println(bin.getRecord().getUsers().size()); } } diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java index d887fa2..4172b67 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java @@ -20,15 +20,13 @@ public AuthInterceptor(String masterKey, String accessKey) { public Response intercept(Chain chain) throws IOException { Request original = chain.request(); - boolean hasAccessKey = accessKey != null && !accessKey.isBlank(); - - Request requestWithAuth = - original.newBuilder() - .header( - hasAccessKey ? "x-access-key" : "x-master-key", - hasAccessKey ? accessKey : masterKey) - .build(); - - return chain.proceed(requestWithAuth); + Request.Builder requestBuilder = original.newBuilder(); + if (accessKey != null && !accessKey.isEmpty()) { + requestBuilder.header("x-access-key", accessKey); + } else { + requestBuilder.header("x-master-key", masterKey); + } + + return chain.proceed(requestBuilder.build()); } } diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java index e541579..88b45cc 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java @@ -1,12 +1,15 @@ package io.github.odunlamizo.jsonbin.okhttp; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.type.TypeFactory; import io.github.odunlamizo.jsonbin.JsonBin; import io.github.odunlamizo.jsonbin.JsonBinException; import io.github.odunlamizo.jsonbin.model.Bin; import io.github.odunlamizo.jsonbin.model.Error; import io.github.odunlamizo.jsonbin.util.JsonUtil; import java.io.IOException; +import java.util.function.Function; import lombok.NonNull; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -15,24 +18,73 @@ public class JsonBinOkHttp implements JsonBin { private final OkHttpClient client; - private final String baseUrl; + private final Function> deserializer; - private final TypeReference> typeReference; - - public JsonBinOkHttp(@NonNull String masterKey) { - this(masterKey, "https://api.jsonbin.io/v3"); - } - - public JsonBinOkHttp(@NonNull String masterKey, @NonNull String baseUrl) { + private JsonBinOkHttp( + String masterKey, + String accessKey, + String baseUrl, + Function> deserializer) { this.baseUrl = baseUrl; - this.typeReference = new TypeReference>() {}; + this.deserializer = deserializer; this.client = new OkHttpClient.Builder() - .addInterceptor(new AuthInterceptor(masterKey, null)) + .addInterceptor(new AuthInterceptor(masterKey, accessKey)) .build(); } + public static class Builder { + private String masterKey; + + private String accessKey; + + private String baseUrl = "https://api.jsonbin.io/v3"; + + public Builder withMasterKey(String masterKey) { + this.masterKey = masterKey; + return this; + } + + public Builder withAccessKey(String accessKey) { + this.accessKey = accessKey; + return this; + } + + public Builder withBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public JsonBinOkHttp build(Class recordClass) { + if ((masterKey == null || masterKey.isBlank()) + && (accessKey == null || accessKey.isBlank())) { + throw new IllegalArgumentException( + "Either masterKey or accessKey must be provided."); + } + + return new JsonBinOkHttp<>( + masterKey, + accessKey, + baseUrl, + json -> { + TypeReference> ref = + new TypeReference<>() { + @Override + public java.lang.reflect.Type getType() { + return TypeFactory.defaultInstance() + .constructParametricType(Bin.class, recordClass); + } + }; + try { + return JsonUtil.toValue(json, ref); + } catch (JsonProcessingException e) { + throw new JsonBinException(""); + } + }); + } + } + @Override public Bin readBin(@NonNull String binId) throws JsonBinException { final String URL = String.format("%s/b/%s", baseUrl, binId); @@ -51,7 +103,7 @@ private Bin newCall(Request request) { throw new JsonBinException(errorResponse.getMessage()); } - return JsonUtil.toValue(json, typeReference); + return deserializer.apply(json); } catch (IOException exception) { throw new JsonBinException(exception.getMessage(), exception.getCause()); } diff --git a/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java new file mode 100644 index 0000000..3e5e423 --- /dev/null +++ b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java @@ -0,0 +1,103 @@ +// package io.github.odunlamizo.jsonbin.okhttp; +// +// import static org.junit.jupiter.api.Assertions.*; +// +// import io.github.odunlamizo.jsonbin.JsonBinException; +// import io.github.odunlamizo.jsonbin.model.Bin; +// import io.github.odunlamizo.jsonbin.model.User; +// import io.github.odunlamizo.jsonbin.model.UserList; +// import java.util.List; +// import okhttp3.mockwebserver.MockResponse; +// import okhttp3.mockwebserver.MockWebServer; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.BeforeEach; +// import org.junit.jupiter.api.Test; +// +// class JsonBinOkHttpTest { +// +// private MockWebServer mockWebServer; +// +// @BeforeEach +// void setup() throws Exception { +// mockWebServer = new MockWebServer(); +// mockWebServer.start(); +// } +// +// @AfterEach +// void teardown() throws Exception { +// mockWebServer.shutdown(); +// } +// +// @Test +// void shouldDeserializeValidBinResponse() { +// String json = +// """ +// { +// "record": { +// "users": [ +// { "name": "Morounfoluwa Mary", "age": 19 } +// ] +// }, +// "metadata": { +// "id": "abc123", +// "private": true, +// "createdAt": "2024-01-01T10:00:00Z", +// "name": "Test Bin" +// } +// } +// """; +// +// mockWebServer.enqueue( +// new MockResponse() +// .setBody(json) +// .addHeader("Content-Type", "application/json") +// .setResponseCode(200)); +// +// String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); +// +// JsonBinOkHttp jsonBin = +// new JsonBinOkHttp.Builder() +// .withMasterKey("dummy-key") +// .withBaseUrl(mockUrl) +// .build(); +// +// Bin result = jsonBin.readBin("test-bin-id"); +// +// assertNotNull(result); +// assertEquals("abc123", result.getMetadata().getId()); +// assertTrue(result.getMetadata().is_private()); +// assertEquals("Test Bin", result.getMetadata().getName()); +// +// List users = result.getRecord().getUsers(); +// assertEquals(1, users.size()); +// assertEquals("Morounfoluwa Mary", users.get(0).getName()); +// assertEquals(19, users.get(0).getAge()); +// } +// +// @Test +// void shouldThrowExceptionOnErrorResponse() { +// String errorJson = +// """ +// { "message": "Bin not found" } +// """; +// +// mockWebServer.enqueue( +// new MockResponse() +// .setBody(errorJson) +// .addHeader("Content-Type", "application/json") +// .setResponseCode(404)); +// +// String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); +// +// JsonBinOkHttp jsonBin = +// new JsonBinOkHttp.Builder() +// .withMasterKey("dummy-key") +// .withBaseUrl(mockUrl) +// .build(); +// +// JsonBinException exception = +// assertThrows(JsonBinException.class, () -> jsonBin.readBin("invalid-id")); +// +// assertTrue(exception.getMessage().contains("Bin not found")); +// } +// } diff --git a/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java b/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java new file mode 100644 index 0000000..15804e2 --- /dev/null +++ b/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java @@ -0,0 +1,69 @@ +package io.github.odunlamizo.jsonbin.util; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import io.github.odunlamizo.jsonbin.model.*; +import java.time.ZonedDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JsonUtilTest { + + @Test + void shouldDeserializeBinWithUserList() throws JsonProcessingException { + String json = + """ + { + "record": { + "users": [ + { + "name": "Morounfoluwa Mary", + "age": 19 + }, + { + "name": "John Doe", + "age": 22 + } + ] + }, + "metadata": { + "id": "65b9e853266cfc3fde83a460", + "private": true, + "createdAt": "2024-01-31T06:27:31.021Z", + "name": "User List Bin" + } + } + """; + + Bin bin = JsonUtil.toValue(json, new TypeReference<>() {}); + + assertNotNull(bin); + assertNotNull(bin.getRecord()); + assertNotNull(bin.getMetadata()); + + List users = bin.getRecord().getUsers(); + assertEquals(2, users.size()); + + assertEquals("Morounfoluwa Mary", users.get(0).getName()); + assertEquals(19, users.get(0).getAge()); + + assertEquals("John Doe", users.get(1).getName()); + assertEquals(22, users.get(1).getAge()); + + assertEquals("65b9e853266cfc3fde83a460", bin.getMetadata().getId()); + assertTrue(bin.getMetadata().is_private()); + assertEquals("User List Bin", bin.getMetadata().getName()); + assertEquals( + ZonedDateTime.parse("2024-01-31T06:27:31.021Z"), bin.getMetadata().getCreatedAt()); + } + + @Test + void shouldThrowOnInvalidJson() { + String invalidJson = "{ invalid json }"; + assertThrows( + JsonProcessingException.class, + () -> JsonUtil.toValue(invalidJson, new TypeReference>() {})); + } +} From 64836a9bf0846ff2e6cf27a9676111c907d64492 Mon Sep 17 00:00:00 2001 From: Odunlami Zacchaeus Date: Tue, 15 Jul 2025 17:14:42 +0100 Subject: [PATCH 2/2] update tests --- .../io/github/odunlamizo/jsonbin/App.java | 2 +- .../jsonbin/okhttp/JsonBinOkHttpTest.java | 206 +++++++++--------- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/src/main/java/io/github/odunlamizo/jsonbin/App.java b/src/main/java/io/github/odunlamizo/jsonbin/App.java index 37fe419..a859a73 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/App.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/App.java @@ -23,6 +23,6 @@ public static void main(String[] args) { JsonBin jsonBin = new JsonBinOkHttp.Builder().withMasterKey(masterKey).build(UserList.class); Bin bin = jsonBin.readBin("687644d36063391d31ae163f"); - System.out.println(bin.getRecord().getUsers().size()); + System.out.println(bin); } } diff --git a/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java index 3e5e423..ef781c0 100644 --- a/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java +++ b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java @@ -1,103 +1,103 @@ -// package io.github.odunlamizo.jsonbin.okhttp; -// -// import static org.junit.jupiter.api.Assertions.*; -// -// import io.github.odunlamizo.jsonbin.JsonBinException; -// import io.github.odunlamizo.jsonbin.model.Bin; -// import io.github.odunlamizo.jsonbin.model.User; -// import io.github.odunlamizo.jsonbin.model.UserList; -// import java.util.List; -// import okhttp3.mockwebserver.MockResponse; -// import okhttp3.mockwebserver.MockWebServer; -// import org.junit.jupiter.api.AfterEach; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// -// class JsonBinOkHttpTest { -// -// private MockWebServer mockWebServer; -// -// @BeforeEach -// void setup() throws Exception { -// mockWebServer = new MockWebServer(); -// mockWebServer.start(); -// } -// -// @AfterEach -// void teardown() throws Exception { -// mockWebServer.shutdown(); -// } -// -// @Test -// void shouldDeserializeValidBinResponse() { -// String json = -// """ -// { -// "record": { -// "users": [ -// { "name": "Morounfoluwa Mary", "age": 19 } -// ] -// }, -// "metadata": { -// "id": "abc123", -// "private": true, -// "createdAt": "2024-01-01T10:00:00Z", -// "name": "Test Bin" -// } -// } -// """; -// -// mockWebServer.enqueue( -// new MockResponse() -// .setBody(json) -// .addHeader("Content-Type", "application/json") -// .setResponseCode(200)); -// -// String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); -// -// JsonBinOkHttp jsonBin = -// new JsonBinOkHttp.Builder() -// .withMasterKey("dummy-key") -// .withBaseUrl(mockUrl) -// .build(); -// -// Bin result = jsonBin.readBin("test-bin-id"); -// -// assertNotNull(result); -// assertEquals("abc123", result.getMetadata().getId()); -// assertTrue(result.getMetadata().is_private()); -// assertEquals("Test Bin", result.getMetadata().getName()); -// -// List users = result.getRecord().getUsers(); -// assertEquals(1, users.size()); -// assertEquals("Morounfoluwa Mary", users.get(0).getName()); -// assertEquals(19, users.get(0).getAge()); -// } -// -// @Test -// void shouldThrowExceptionOnErrorResponse() { -// String errorJson = -// """ -// { "message": "Bin not found" } -// """; -// -// mockWebServer.enqueue( -// new MockResponse() -// .setBody(errorJson) -// .addHeader("Content-Type", "application/json") -// .setResponseCode(404)); -// -// String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); -// -// JsonBinOkHttp jsonBin = -// new JsonBinOkHttp.Builder() -// .withMasterKey("dummy-key") -// .withBaseUrl(mockUrl) -// .build(); -// -// JsonBinException exception = -// assertThrows(JsonBinException.class, () -> jsonBin.readBin("invalid-id")); -// -// assertTrue(exception.getMessage().contains("Bin not found")); -// } -// } +package io.github.odunlamizo.jsonbin.okhttp; + +import static org.junit.jupiter.api.Assertions.*; + +import io.github.odunlamizo.jsonbin.JsonBinException; +import io.github.odunlamizo.jsonbin.model.Bin; +import io.github.odunlamizo.jsonbin.model.User; +import io.github.odunlamizo.jsonbin.model.UserList; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JsonBinOkHttpTest { + + private MockWebServer mockWebServer; + + @BeforeEach + void setup() throws Exception { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + } + + @AfterEach + void teardown() throws Exception { + mockWebServer.shutdown(); + } + + @Test + void shouldDeserializeValidBinResponse() { + String json = + """ + { + "record": { + "users": [ + { "name": "Morounfoluwa Mary", "age": 19 } + ] + }, + "metadata": { + "id": "abc123", + "private": true, + "createdAt": "2024-01-01T10:00:00Z", + "name": "Test Bin" + } + } + """; + + mockWebServer.enqueue( + new MockResponse() + .setBody(json) + .addHeader("Content-Type", "application/json") + .setResponseCode(200)); + + String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); + + JsonBinOkHttp jsonBin = + new JsonBinOkHttp.Builder() + .withMasterKey("dummy-key") + .withBaseUrl(mockUrl) + .build(UserList.class); + + Bin result = jsonBin.readBin("test-bin-id"); + + assertNotNull(result); + assertEquals("abc123", result.getMetadata().getId()); + assertTrue(result.getMetadata().is_private()); + assertEquals("Test Bin", result.getMetadata().getName()); + + List users = result.getRecord().getUsers(); + assertEquals(1, users.size()); + assertEquals("Morounfoluwa Mary", users.get(0).getName()); + assertEquals(19, users.get(0).getAge()); + } + + @Test + void shouldThrowExceptionOnErrorResponse() { + String errorJson = + """ + { "message": "Bin not found" } + """; + + mockWebServer.enqueue( + new MockResponse() + .setBody(errorJson) + .addHeader("Content-Type", "application/json") + .setResponseCode(404)); + + String mockUrl = mockWebServer.url("").toString().replaceAll("/$", ""); + + JsonBinOkHttp jsonBin = + new JsonBinOkHttp.Builder() + .withMasterKey("dummy-key") + .withBaseUrl(mockUrl) + .build(UserList.class); + + JsonBinException exception = + assertThrows(JsonBinException.class, () -> jsonBin.readBin("invalid-id")); + + assertTrue(exception.getMessage().contains("Bin not found")); + } +}