Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<repositories>
<repository>
<id>github</id>
<name>GitHub Packages - java-jsonbin</name>
<url>https://maven.pkg.github.com/OdunlamiZO/java-jsonbin</url>
</repository>
</repositories>
```

2. Add the dependency:

```xml
<dependency>
<groupId>io.github.odunlamizo</groupId>
<artifactId>java-jsonbin</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```

3. Configure GitHub credentials in your Maven settings.xml (usually located at ~/.m2/settings.xml):

```xml
<settings>
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_PERSONAL_ACCESS_TOKEN</password>
</server>
</servers>
</settings>
```

> 📌&nbsp;&nbsp;&nbsp; Step 1 & 3 is required for now, since we are only deploying to github packages.

## Getting Started

### Setup

```java
JsonBin<UserList> 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<UserList> 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.

> 📌&nbsp;&nbsp;&nbsp;Please ensure your code adheres to the project's style and passes all tests before submitting a PR.
3 changes: 2 additions & 1 deletion src/main/java/io/github/odunlamizo/jsonbin/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public static void main(String[] args) {
System.exit(1);
}

JsonBin<UserList> jsonBin = new JsonBinOkHttp<>(masterKey);
JsonBin<UserList> jsonBin =
new JsonBinOkHttp.Builder().withMasterKey(masterKey).build(UserList.class);
Bin<UserList> bin = jsonBin.readBin("687644d36063391d31ae163f");
System.out.println(bin);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,24 +18,73 @@
public class JsonBinOkHttp<T> implements JsonBin<T> {

private final OkHttpClient client;

private final String baseUrl;
private final Function<String, Bin<T>> deserializer;

private final TypeReference<Bin<T>> 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<String, Bin<T>> deserializer) {
this.baseUrl = baseUrl;
this.typeReference = new TypeReference<Bin<T>>() {};
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 <T> JsonBinOkHttp<T> build(Class<T> 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<Bin<T>> 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<T> readBin(@NonNull String binId) throws JsonBinException {
final String URL = String.format("%s/b/%s", baseUrl, binId);
Expand All @@ -51,7 +103,7 @@ private Bin<T> 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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserList> jsonBin =
new JsonBinOkHttp.Builder()
.withMasterKey("dummy-key")
.withBaseUrl(mockUrl)
.build(UserList.class);

Bin<UserList> 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<User> 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<UserList> 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"));
}
}
69 changes: 69 additions & 0 deletions src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java
Original file line number Diff line number Diff line change
@@ -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<UserList> bin = JsonUtil.toValue(json, new TypeReference<>() {});

assertNotNull(bin);
assertNotNull(bin.getRecord());
assertNotNull(bin.getMetadata());

List<User> 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<Bin<UserList>>() {}));
}
}