diff --git a/README.md b/README.md index eb8a8e6..9ce4d62 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Java JsonBin +# JSONBin.io Java SDK [![codecov](https://codecov.io/gh/OdunlamiZO/java-jsonbin/graph/badge.svg?token=HULR9R4NAH)](https://codecov.io/gh/OdunlamiZO/java-jsonbin) diff --git a/pom.xml b/pom.xml index 0473e8b..f0c1761 100644 --- a/pom.xml +++ b/pom.xml @@ -30,22 +30,22 @@ com.fasterxml.jackson.core jackson-annotations - 2.17.1 + 2.19.1 com.fasterxml.jackson.core jackson-databind - 2.17.0 + 2.19.1 com.fasterxml.jackson.core jackson-core - 2.18.0 + 2.19.1 - jakarta.validation - jakarta.validation-api - 3.0.2 + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.19.1 io.github.cdimascio diff --git a/src/main/java/io/github/odunlamizo/jsonbin/App.java b/src/main/java/io/github/odunlamizo/jsonbin/App.java index b22da4c..06c1e4e 100644 --- a/src/main/java/io/github/odunlamizo/jsonbin/App.java +++ b/src/main/java/io/github/odunlamizo/jsonbin/App.java @@ -1,11 +1,27 @@ package io.github.odunlamizo.jsonbin; import io.github.cdimascio.dotenv.Dotenv; +import io.github.odunlamizo.jsonbin.model.Bin; +import io.github.odunlamizo.jsonbin.model.UserList; +import io.github.odunlamizo.jsonbin.okhttp.JsonBinOkHttp; public class App { public static void main(String[] args) { // Load environment variables from .env file Dotenv dotenv = Dotenv.configure().load(); + + // Get master key from .env + String masterKey = dotenv.get("JSONBIN_MASTER_KEY"); + + // Check if the master key exists + if (masterKey == null || masterKey.isEmpty()) { + System.err.println("Error: JSONBIN_MASTER_KEY not found in .env file"); + System.exit(1); + } + + JsonBin jsonBin = new JsonBinOkHttp<>(masterKey); + Bin bin = jsonBin.readBin("687644d36063391d31ae163f"); + System.out.println(bin); } } diff --git a/src/main/java/io/github/odunlamizo/jsonbin/JsonBin.java b/src/main/java/io/github/odunlamizo/jsonbin/JsonBin.java new file mode 100644 index 0000000..4e5f6d4 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/JsonBin.java @@ -0,0 +1,20 @@ +package io.github.odunlamizo.jsonbin; + +import io.github.odunlamizo.jsonbin.model.Bin; +import lombok.NonNull; + +/** JSONBin.io Java SDK */ +public interface JsonBin { + + /** + * Reads the contents of a bin from JSONBin.io using its unique identifier. + * + *

This method fetches the bin data from the remote JSONBin.io API and deserializes it into a + * strongly-typed {@link Bin} object containing the expected data type {@code T}. + * + * @param binId the unique identifier of the bin to retrieve; must not be {@code null} + * @return a {@link Bin} object containing the deserialized data + * @throws JsonBinException if the request fails, the bin is not found, or deserialization fails + */ + Bin readBin(@NonNull String binId) throws JsonBinException; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/model/Bin.java b/src/main/java/io/github/odunlamizo/jsonbin/model/Bin.java new file mode 100644 index 0000000..f709c34 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/model/Bin.java @@ -0,0 +1,26 @@ +package io.github.odunlamizo.jsonbin.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a response wrapper from JSONBin.io containing both the data record and its metadata. + * + *

This generic class is used to hold the actual data stored in the bin (as {@code record}) and + * additional information about the bin such as creation time, visibility, and name (as {@link + * Metadata}). + * + * @param the type of the data record stored in the bin + */ +@Getter +@Setter +@ToString +public class Bin { + + /** The actual content or data retrieved from the bin. */ + private T record; + + /** Metadata information about the bin, such as its ID, name, visibility, and timestamps. */ + private Metadata metadata; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/model/Error.java b/src/main/java/io/github/odunlamizo/jsonbin/model/Error.java new file mode 100644 index 0000000..c618c5b --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/model/Error.java @@ -0,0 +1,11 @@ +package io.github.odunlamizo.jsonbin.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Error { + + private String message; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/model/Metadata.java b/src/main/java/io/github/odunlamizo/jsonbin/model/Metadata.java new file mode 100644 index 0000000..08a9f29 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/model/Metadata.java @@ -0,0 +1,41 @@ +package io.github.odunlamizo.jsonbin.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.ZonedDateTime; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents metadata information returned from a JSONBin.io response. + * + *

This includes details such as the bin's unique identifier, visibility, creation timestamp, and + * assigned name. + */ +@Getter +@Setter +@ToString +public class Metadata { + + /** The unique identifier of the bin. */ + private String id; + + /** + * Indicates whether the bin is private. + * + *

Uses @JsonProperty to map from the JSON key {@code "private"} since it is a reserved + * keyword in Java. + */ + @JsonProperty("private") + private boolean _private; + + /** + * The timestamp when the bin was created. + * + *

Expected in ISO 8601 format with timezone information. + */ + private ZonedDateTime createdAt; + + /** The human-readable name assigned to the bin. */ + private String name; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/model/User.java b/src/main/java/io/github/odunlamizo/jsonbin/model/User.java new file mode 100644 index 0000000..c69db56 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/model/User.java @@ -0,0 +1,13 @@ +package io.github.odunlamizo.jsonbin.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class User { + + private String name; + + private int age; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/model/UserList.java b/src/main/java/io/github/odunlamizo/jsonbin/model/UserList.java new file mode 100644 index 0000000..83729e8 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/model/UserList.java @@ -0,0 +1,12 @@ +package io.github.odunlamizo.jsonbin.model; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserList { + + private List users; +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java new file mode 100644 index 0000000..d887fa2 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java @@ -0,0 +1,34 @@ +package io.github.odunlamizo.jsonbin.okhttp; + +import java.io.IOException; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +class AuthInterceptor implements Interceptor { + + private final String masterKey; + + private final String accessKey; + + public AuthInterceptor(String masterKey, String accessKey) { + this.masterKey = masterKey; + this.accessKey = accessKey; + } + + @Override + 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); + } +} diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java new file mode 100644 index 0000000..e541579 --- /dev/null +++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java @@ -0,0 +1,59 @@ +package io.github.odunlamizo.jsonbin.okhttp; + +import com.fasterxml.jackson.core.type.TypeReference; +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 lombok.NonNull; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +/** JSONBin.io Java SDK implementation powered by OkHttp */ +public class JsonBinOkHttp implements JsonBin { + + private final OkHttpClient client; + + private final String baseUrl; + + private final TypeReference> typeReference; + + public JsonBinOkHttp(@NonNull String masterKey) { + this(masterKey, "https://api.jsonbin.io/v3"); + } + + public JsonBinOkHttp(@NonNull String masterKey, @NonNull String baseUrl) { + this.baseUrl = baseUrl; + this.typeReference = new TypeReference>() {}; + this.client = + new OkHttpClient.Builder() + .addInterceptor(new AuthInterceptor(masterKey, null)) + .build(); + } + + @Override + public Bin readBin(@NonNull String binId) throws JsonBinException { + final String URL = String.format("%s/b/%s", baseUrl, binId); + Request request = new Request.Builder().url(URL).build(); + + return newCall(request); + } + + private Bin newCall(Request request) { + try (okhttp3.Response response = client.newCall(request).execute()) { + + String json = response.body().string(); + + if (!response.isSuccessful()) { + Error errorResponse = JsonUtil.toValue(json, new TypeReference() {}); + throw new JsonBinException(errorResponse.getMessage()); + } + + return JsonUtil.toValue(json, typeReference); + } catch (IOException exception) { + throw new JsonBinException(exception.getMessage(), exception.getCause()); + } + } +} diff --git a/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java b/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java deleted file mode 100644 index b01e23f..0000000 --- a/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java +++ /dev/null @@ -1,52 +0,0 @@ -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 java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class JsonUtilTest { - - @Test - void shouldDeserializeJsonToMap() throws JsonProcessingException { - String json = "{\"name\":\"Odun\", \"age\":25}"; - Map result = JsonUtil.toValue(json, new TypeReference<>() {}); - - assertEquals("Odun", result.get("name")); - assertEquals(25, result.get("age")); - } - - @Test - void shouldDeserializeJsonToList() throws JsonProcessingException { - String json = "[\"Lagos\", \"Abuja\", \"Ibadan\"]"; - List result = JsonUtil.toValue(json, new TypeReference<>() {}); - - assertEquals(3, result.size()); - assertTrue(result.contains("Lagos")); - assertTrue(result.contains("Ibadan")); - } - - // @Test - // void shouldDeserializeJsonToCustomObject() throws JsonProcessingException { - // String json = "{\"currency\":\"NGN\",\"name\":\"Zenith Bank\"}"; - // - // Bank bank = JsonUtil.toValue(json, new TypeReference<>() {}); - // - // assertEquals("NGN", bank.getCurrency()); - // assertEquals("Zenith Bank", bank.getName()); - // } - - @Test - void shouldThrowExceptionForInvalidJson() { - String invalidJson = "{invalidJson}"; - - assertThrows( - JsonProcessingException.class, - () -> { - JsonUtil.toValue(invalidJson, new TypeReference>() {}); - }); - } -}