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..a859a73 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/App.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/App.java @@ -20,7 +20,8 @@ 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); } 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..ef781c0 --- /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(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")); + } +} 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>() {})); + } +}