Added apiCall function

This commit is contained in:
Erik C. Thauvin 2022-09-23 23:17:48 -07:00
parent f07b1a4258
commit 9b5047e5fa
4 changed files with 127 additions and 36 deletions

View file

@ -11,6 +11,7 @@ val joke = getJoke()
val safe = getJoke(safe = true) val safe = getJoke(safe = true)
val pun = getJoke(category = Category.PUN) val pun = getJoke(category = Category.PUN)
``` ```
The parameters match the [joke endpoint](/https://v2.jokeapi.dev/#joke-endpoint).
A `Joke` class instance is returned: A `Joke` class instance is returned:
@ -19,13 +20,14 @@ data class Joke(
val error: Boolean, val error: Boolean,
val category: Category, val category: Category,
val type: Type, val type: Type,
val joke: Set<String>, val joke: List<String>,
val flags: Set<Flag>, val flags: Set<Flag>,
val id: Int, val id: Int,
val safe: Boolean, val safe: Boolean,
val language: Language 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: If an error occurs, a `JokeException` is thrown:
@ -51,6 +53,7 @@ class HttpErrorException(
cause: Throwable? = null cause: Throwable? = null
) : IOException(message, cause) ) : 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. ## Gradle, Maven, etc.
To use with [Gradle](https://gradle.org/), include the following dependency in your build file: To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
@ -89,6 +92,28 @@ safe: true
lang: "en" 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)...

View file

@ -40,6 +40,7 @@ import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Parameter
import net.thauvin.erik.jokeapi.models.Type import net.thauvin.erik.jokeapi.models.Type
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -53,11 +54,42 @@ import java.util.stream.Collectors
class JokeApi { class JokeApi {
companion object { 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 @JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } 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<String, String> = 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 @JvmStatic
@JvmOverloads @JvmOverloads
@Throws(HttpErrorException::class, IOException::class) @Throws(HttpErrorException::class, IOException::class)
@ -72,53 +104,50 @@ class JokeApi {
amount: Int = 1, amount: Int = 1,
safe: Boolean = false, safe: Boolean = false,
): String { ): String {
val urlBuilder = StringBuilder(API_URL) val params = mutableMapOf<String, String>()
val urlParams = mutableListOf<String>()
// Category // Categories
if (!categories.contains(Category.ANY)) { val path = if (!categories.contains(Category.ANY)) {
urlBuilder.append(categories.stream().map(Category::value).collect(Collectors.joining(","))) categories.stream().map(Category::value).collect(Collectors.joining(","))
} else { } else {
urlBuilder.append(Category.ANY.value) Category.ANY.value
} }
// Language // Language
if (language != Language.ENGLISH) { if (language != Language.ENGLISH) {
urlParams.add("lang=${language.value}") params[Parameter.LANG] = language.value
} }
// Flags // Flags
if (flags.isNotEmpty()) { if (flags.isNotEmpty()) {
if (flags.contains(Flag.ALL)) { if (flags.contains(Flag.ALL)) {
urlParams.add("blacklistFlags=${Flag.ALL.value}") params[Parameter.FLAGS] = Flag.ALL.value
} else { } else {
urlParams.add( params[Parameter.FLAGS] = flags.stream().map(Flag::value).collect(Collectors.joining(","))
"blacklistFlags=" + flags.stream().map(Flag::value).collect(Collectors.joining(","))
)
} }
} }
// Type // Type
if (type != Type.ALL) { if (type != Type.ALL) {
urlParams.add("type=${type.value}") params[Parameter.TYPE] = type.value
} }
// Format // Format
if (format != Format.JSON) { if (format != Format.JSON) {
urlParams.add("format=${format.value}") params[Parameter.FORMAT] = format.value
} }
// Contains // Contains
if (search.isNotBlank()) { if (search.isNotBlank()) {
urlParams.add("contains=${URLEncoder.encode(search, StandardCharsets.UTF_8)}") params[Parameter.CONTAINS] = URLEncoder.encode(search, StandardCharsets.UTF_8).replace("+", "%20")
} }
// Range // Range
if (idRange.start >= 0) { if (idRange.start >= 0) {
if (idRange.end == -1 || idRange.start == idRange.end) { 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) { } 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)) { } else if (logger.isLoggable(Level.WARNING)) {
logger.warning("Invalid ID Range: ${idRange.start}, ${idRange.end}") logger.warning("Invalid ID Range: ${idRange.start}, ${idRange.end}")
} }
@ -126,28 +155,17 @@ class JokeApi {
// Amount // Amount
if (amount in 2..10) { if (amount in 2..10) {
urlParams.add("amount=${amount}") params[Parameter.AMOUNT] = amount.toString()
} else if (amount != 1 && logger.isLoggable(Level.WARNING)) { } else if (amount != 1 && logger.isLoggable(Level.WARNING)) {
logger.warning("Invalid Amount: $amount") logger.warning("Invalid Amount: $amount")
} }
// Safe // Safe
if (safe) { if (safe) {
urlParams.add("safe-mode") params[Parameter.SAFE] = ""
} }
if (urlParams.isNotEmpty()) { return apiCall(JOKE_ENDPOINT, path, params)
urlBuilder.append('?')
val it = urlParams.iterator()
while (it.hasNext()) {
urlBuilder.append(it.next())
if (it.hasNext()) {
urlBuilder.append("&")
}
}
}
return fetchUrl(urlBuilder.toString())
} }
@Throws(HttpErrorException::class, IOException::class) @Throws(HttpErrorException::class, IOException::class)
@ -270,7 +288,7 @@ class JokeApi {
timestamp = json.getLong("timestamp") timestamp = json.getLong("timestamp")
) )
} else { } else {
val jokes = mutableSetOf<String>() val jokes = mutableListOf<String>()
if (json.has("setup")) { if (json.has("setup")) {
jokes.add(json.getString("setup")) jokes.add(json.getString("setup"))
jokes.add(json.getString(("delivery"))) jokes.add(json.getString(("delivery")))
@ -291,12 +309,12 @@ class JokeApi {
return Joke( return Joke(
error = false, error = false,
category = Category.valueOf(json.getString("category").uppercase()), category = Category.valueOf(json.getString("category").uppercase()),
type = Type.valueOf(json.getString("type").uppercase()), type = Type.valueOf(json.getString(Parameter.TYPE).uppercase()),
joke = jokes, joke = jokes,
flags = enabledFlags, flags = enabledFlags,
safe = json.getBoolean("safe"), safe = json.getBoolean("safe"),
id = json.getInt("id"), id = json.getInt("id"),
language = Language.valueOf(json.getString("lang").uppercase()) language = Language.valueOf(json.getString(Parameter.LANG).uppercase())
) )
} }
} }

View file

@ -36,7 +36,7 @@ data class Joke(
val error: Boolean, val error: Boolean,
val category: Category, val category: Category,
val type: Type, val type: Type,
val joke: Set<String>, val joke: List<String>,
val flags: Set<Flag>, val flags: Set<Flag>,
val id: Int, val id: Int,
val safe: Boolean, val safe: Boolean,

View file

@ -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
}