diff --git a/README.md b/README.md index c049d8c..a4bc6b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ val joke = getJoke() val safe = getJoke(safe = true) val pun = getJoke(category = Category.PUN) ``` +The parameters match the [joke endpoint](/https://v2.jokeapi.dev/#joke-endpoint). A `Joke` class instance is returned: @@ -19,13 +20,14 @@ data class Joke( val error: Boolean, val category: Category, val type: Type, - val joke: Set, + val joke: List, val flags: Set, val id: Int, val safe: Boolean, val language: Language ) ``` +- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt)... If an error occurs, a `JokeException` is thrown: @@ -51,6 +53,7 @@ class HttpErrorException( cause: Throwable? = null ) : IOException(message, cause) ``` +- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/Exceptions.kt)... ## Gradle, Maven, etc. To use with [Gradle](https://gradle.org/), include the following dependency in your build file: @@ -89,6 +92,28 @@ safe: true lang: "en" ``` +- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokeTest.kt)... + +## Extending + +A generic `apiCall()` function is available to access other [JokeAPI endpoints](https://v2.jokeapi.dev/#endpoints). + +For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint): + +```kotlin +val lang = apiCall( + endPoint = "langcode", + path = "french", + params = mapOf(Parameter.FORMAT to Format.YAML.value) +) +println(lang) +``` +```yaml +error: false +code: "fr" +``` +- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/Exceptions.kt)... + diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt index 73386d8..d4dd93b 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt @@ -40,6 +40,7 @@ import net.thauvin.erik.jokeapi.models.Format import net.thauvin.erik.jokeapi.models.IdRange import net.thauvin.erik.jokeapi.models.Joke import net.thauvin.erik.jokeapi.models.Language +import net.thauvin.erik.jokeapi.models.Parameter import net.thauvin.erik.jokeapi.models.Type import org.json.JSONObject import java.io.IOException @@ -53,11 +54,42 @@ import java.util.stream.Collectors class JokeApi { companion object { - private const val API_URL = "https://v2.jokeapi.dev/joke/" + private const val API_URL = "https://v2.jokeapi.dev/" + private const val JOKE_ENDPOINT = "joke" @JvmStatic val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } + @JvmStatic + @JvmOverloads + @Throws(HttpErrorException::class, IOException::class) + fun apiCall(endPoint: String, path: String = "", params: Map = emptyMap()): String { + val urlBuilder = StringBuilder("$API_URL$endPoint") + + if (path.isNotEmpty()) { + if (!urlBuilder.endsWith(('/'))) { + urlBuilder.append('/') + } + urlBuilder.append(path) + } + + if (params.isNotEmpty()) { + urlBuilder.append('?') + val it = params.iterator() + while (it.hasNext()) { + val param = it.next() + urlBuilder.append(param.key) + if (param.value.isNotEmpty()) { + urlBuilder.append("=${param.value}") + } + if (it.hasNext()) { + urlBuilder.append("&") + } + } + } + return fetchUrl(urlBuilder.toString()) + } + @JvmStatic @JvmOverloads @Throws(HttpErrorException::class, IOException::class) @@ -72,53 +104,50 @@ class JokeApi { amount: Int = 1, safe: Boolean = false, ): String { - val urlBuilder = StringBuilder(API_URL) - val urlParams = mutableListOf() + val params = mutableMapOf() - // Category - if (!categories.contains(Category.ANY)) { - urlBuilder.append(categories.stream().map(Category::value).collect(Collectors.joining(","))) + // Categories + val path = if (!categories.contains(Category.ANY)) { + categories.stream().map(Category::value).collect(Collectors.joining(",")) } else { - urlBuilder.append(Category.ANY.value) + Category.ANY.value } // Language if (language != Language.ENGLISH) { - urlParams.add("lang=${language.value}") + params[Parameter.LANG] = language.value } // Flags if (flags.isNotEmpty()) { if (flags.contains(Flag.ALL)) { - urlParams.add("blacklistFlags=${Flag.ALL.value}") + params[Parameter.FLAGS] = Flag.ALL.value } else { - urlParams.add( - "blacklistFlags=" + flags.stream().map(Flag::value).collect(Collectors.joining(",")) - ) + params[Parameter.FLAGS] = flags.stream().map(Flag::value).collect(Collectors.joining(",")) } } // Type if (type != Type.ALL) { - urlParams.add("type=${type.value}") + params[Parameter.TYPE] = type.value } // Format if (format != Format.JSON) { - urlParams.add("format=${format.value}") + params[Parameter.FORMAT] = format.value } // Contains if (search.isNotBlank()) { - urlParams.add("contains=${URLEncoder.encode(search, StandardCharsets.UTF_8)}") + params[Parameter.CONTAINS] = URLEncoder.encode(search, StandardCharsets.UTF_8).replace("+", "%20") } // Range if (idRange.start >= 0) { if (idRange.end == -1 || idRange.start == idRange.end) { - urlParams.add("idRange=${idRange.start}") + params[Parameter.RANGE] = idRange.start.toString() } else if (idRange.end > idRange.start) { - urlParams.add("idRange=${idRange.start}-${idRange.end}") + params[Parameter.RANGE] = "${idRange.start}-${idRange.end}" } else if (logger.isLoggable(Level.WARNING)) { logger.warning("Invalid ID Range: ${idRange.start}, ${idRange.end}") } @@ -126,28 +155,17 @@ class JokeApi { // Amount if (amount in 2..10) { - urlParams.add("amount=${amount}") + params[Parameter.AMOUNT] = amount.toString() } else if (amount != 1 && logger.isLoggable(Level.WARNING)) { logger.warning("Invalid Amount: $amount") } // Safe if (safe) { - urlParams.add("safe-mode") + params[Parameter.SAFE] = "" } - if (urlParams.isNotEmpty()) { - urlBuilder.append('?') - val it = urlParams.iterator() - while (it.hasNext()) { - urlBuilder.append(it.next()) - if (it.hasNext()) { - urlBuilder.append("&") - } - } - } - - return fetchUrl(urlBuilder.toString()) + return apiCall(JOKE_ENDPOINT, path, params) } @Throws(HttpErrorException::class, IOException::class) @@ -270,7 +288,7 @@ class JokeApi { timestamp = json.getLong("timestamp") ) } else { - val jokes = mutableSetOf() + val jokes = mutableListOf() if (json.has("setup")) { jokes.add(json.getString("setup")) jokes.add(json.getString(("delivery"))) @@ -291,12 +309,12 @@ class JokeApi { return Joke( error = false, category = Category.valueOf(json.getString("category").uppercase()), - type = Type.valueOf(json.getString("type").uppercase()), + type = Type.valueOf(json.getString(Parameter.TYPE).uppercase()), joke = jokes, flags = enabledFlags, safe = json.getBoolean("safe"), id = json.getInt("id"), - language = Language.valueOf(json.getString("lang").uppercase()) + language = Language.valueOf(json.getString(Parameter.LANG).uppercase()) ) } } diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt index 07ec05d..72b1d39 100644 --- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt @@ -36,7 +36,7 @@ data class Joke( val error: Boolean, val category: Category, val type: Type, - val joke: Set, + val joke: List, val flags: Set, val id: Int, val safe: Boolean, diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt new file mode 100644 index 0000000..38e12bd --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt @@ -0,0 +1,48 @@ +/* + * Parameter.kt + * + * Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.jokeapi.models + +object Parameter { + const val AMOUNT = "amount" + const val CONTAINS = "contains" + const val FLAGS = "blacklistFlags" + const val FORMAT = "format" + const val RANGE = "idRange" + const val LANG = "lang" + const val SAFE = "safe-mode" + const val TYPE = "type" + + const val BLACKLIST_FLAGS = FLAGS + const val ID_RANGE = RANGE + const val SAFE_MODE = SAFE +}