Moved Kotlin-only functions to top-level

This commit is contained in:
Erik C. Thauvin 2022-10-08 22:36:01 -07:00
parent 9435df289a
commit de30b3f638
13 changed files with 525 additions and 366 deletions

View file

@ -9,7 +9,7 @@ A simple Kotlin/Java library to retrieve jokes from [Sv443's JokeAPI](https://v2
## Examples (TL;DR)
```kotlin
import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.getJoke
val joke = getJoke()
val safe = getJoke(safe = true)
@ -124,9 +124,7 @@ var config = new JokeConfig.Builder()
.safe(true)
.build();
var joke = JokeApi.getJoke(config);
for (var j : joke.getJoke()) {
System.out.println(j);
}
joke.getJoke().forEach(System.out::println);
```
## Extending
@ -136,7 +134,7 @@ A generic `apiCall()` function is available to access other [JokeAPI endpoints](
For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint):
```kotlin
val lang = apiCall(
val lang = JokeApi.apiCall(
endPoint = "langcode",
path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)

View file

@ -2,24 +2,22 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class) fun getRawJokes( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String</ID>
<ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( amount: Int, categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, )</ID>
<ID>LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set&lt;Category>, val language: Language, val flags: Set&lt;Flag>, val type: Type, val format: Format, val search: String, val idRange: IdRange, val amount: Int = 1, val safe: Boolean, val splitNewLine: Boolean, )</ID>
<ID>ComplexMethod:JokeApi.kt$fun getRawJokes( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" ): String</ID>
<ID>LongParameterList:JokeApi.kt$( amount: Int, categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false, auth: String = "" )</ID>
<ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )</ID>
<ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false, auth: String = "" )</ID>
<ID>LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set&lt;Category>, val language: Language, val flags: Set&lt;Flag>, val type: Type, val format: Format, val search: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )</ID>
<ID>LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$200</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$399</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$400</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$403</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$404</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$413</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$414</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$429</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$500</ID>
<ID>MagicNumber:JokeApi.kt$JokeApi.Companion$523</ID>
<ID>TooManyFunctions:JokeApi.kt$JokeApi$Companion</ID>
<ID>MagicNumber:util.kt$200</ID>
<ID>MagicNumber:util.kt$399</ID>
<ID>MagicNumber:util.kt$400</ID>
<ID>MagicNumber:util.kt$403</ID>
<ID>MagicNumber:util.kt$404</ID>
<ID>MagicNumber:util.kt$413</ID>
<ID>MagicNumber:util.kt$414</ID>
<ID>MagicNumber:util.kt$429</ID>
<ID>MagicNumber:util.kt$500</ID>
<ID>MagicNumber:util.kt$523</ID>
<ID>TooManyFunctions:JokeConfig.kt$JokeConfig$Builder</ID>
<ID>UtilityClassWithPublicConstructor:JokeApi.kt$JokeApi</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -10,7 +10,7 @@
<artifactId>jokeapi</artifactId>
<version>0.9-SNAPSHOT</version>
<name>jokeapi</name>
<description>Kotlin/Java Wrapper for Sv443's JokeApi</description>
<description>Kotlin/Java Wrapper for Sv443's JokeAPI</description>
<url>https://github.com/ethauvin/jokeapi</url>
<licenses>
<license>

View file

@ -43,22 +43,17 @@ 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
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.logging.Level
import java.util.logging.Logger
import java.util.stream.Collectors
/**
* Implements the [Sv443's JokeAPI](https://jokeapi.dev/).
*/
class JokeApi {
class JokeApi private constructor() {
companion object {
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) }
@ -70,8 +65,13 @@ class JokeApi {
*/
@JvmStatic
@JvmOverloads
@Throws(HttpErrorException::class, IOException::class)
fun apiCall(endPoint: String, path: String = "", params: Map<String, String> = emptyMap()): String {
@Throws(HttpErrorException::class)
fun apiCall(
endPoint: String,
path: String = "",
params: Map<String, String> = emptyMap(),
auth: String = ""
): String {
val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) {
@ -98,18 +98,166 @@ class JokeApi {
}
}
}
return fetchUrl(urlBuilder.toString())
return fetchUrl(urlBuilder.toString(), auth)
}
/**
* Returns one or more jokes using a [configuration][JokeConfig].
*
* See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): String {
return getRawJokes(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
format = config.format,
search = config.search,
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
auth = config.auth
)
}
/**
* Retrieve a [Joke] instance using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@JvmOverloads
@Throws(HttpErrorException::class, JokeException::class)
fun getJoke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return getJoke(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
search = config.search,
idRange = config.idRange,
safe = config.safe,
splitNewLine = config.splitNewLine,
auth = config.auth
)
}
/**
* Returns an array of [Joke] instances using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(HttpErrorException::class, JokeException::class)
fun getJokes(config: JokeConfig): Array<Joke> {
return getJokes(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
search = config.search,
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
splitNewLine = config.splitNewLine,
auth = config.auth
)
}
}
}
/**
* Returns a [Joke] instance.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun getJoke(
categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
search: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
splitNewLine: Boolean = false,
auth: String = ""
): Joke {
val json = JSONObject(
getRawJokes(
categories = categories,
language = language,
flags = flags,
type = type,
search = search,
idRange = idRange,
safe = safe,
auth = auth
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return parseJoke(json, splitNewLine)
}
}
/**
* Returns an array of [Joke] instances.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param amount The required amount of jokes to return.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun getJokes(
amount: Int,
categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
search: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
splitNewLine: Boolean = false,
auth: String = ""
): Array<Joke> {
val json = JSONObject(
getRawJokes(
categories = categories,
language = language,
flags = flags,
type = type,
search = search,
idRange = idRange,
amount = amount,
safe = safe,
auth = auth
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return if (json.has("amount")) {
val jokes = json.getJSONArray("jokes")
Array(jokes.length()) { i -> parseJoke(jokes.getJSONObject(i), splitNewLine) }
} else {
arrayOf(parseJoke(json, splitNewLine))
}
}
}
/**
* Returns one or more jokes.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
* @see [getJoke]
*/
@JvmStatic
@Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getRawJokes(
fun getRawJokes(
categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(),
@ -119,7 +267,8 @@ class JokeApi {
idRange: IdRange = IdRange(),
amount: Int = 1,
safe: Boolean = false,
): String {
auth: String = ""
): String {
val params = mutableMapOf<String, String>()
// Categories
@ -181,275 +330,5 @@ class JokeApi {
params[Parameter.SAFE] = ""
}
return apiCall(JOKE_ENDPOINT, path, params)
}
/**
* Returns one or more jokes using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getRawJokes(config: JokeConfig): String {
return getRawJokes(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
format = config.format,
search = config.search,
idRange = config.idRange,
amount = config.amount,
safe = config.safe
)
}
@Throws(HttpErrorException::class, IOException::class)
internal fun fetchUrl(url: String): String {
if (logger.isLoggable(Level.FINE)) {
logger.fine(url)
}
val connection = URL(url).openConnection() as HttpURLConnection
connection.setRequestProperty(
"User-Agent", "Mozilla/5.0 (Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
)
if (connection.responseCode in 200..399) {
val body = connection.inputStream.bufferedReader().readText()
if (logger.isLoggable(Level.FINE)) {
logger.fine(body)
}
return body
} else {
throw httpError(connection.responseCode)
}
}
private fun httpError(responseCode: Int): HttpErrorException {
val httpException: HttpErrorException
when (responseCode) {
400 -> httpException = HttpErrorException(
responseCode, "Bad Request", IOException(
"The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
)
)
403 -> httpException = HttpErrorException(
responseCode, "Forbidden", IOException(
"You have been added to the blacklist due to malicious behavior and are not allowed"
+ " to send requests to JokeAPI anymore."
)
)
404 -> httpException = HttpErrorException(
responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
)
413 -> httpException = HttpErrorException(
responseCode, "URI Too Long", IOException("The URL exceeds the maximum length of 250 characters.")
)
414 -> httpException = HttpErrorException(
responseCode,
"Payload Too Large",
IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
)
429 -> httpException = HttpErrorException(
responseCode, "Too Many Requests", IOException(
"You have exceeded the limit of 120 requests per minute and have to wait a bit"
+ " until you are allowed to send requests again."
)
)
500 -> httpException = HttpErrorException(
responseCode, "Internal Server Error", IOException(
"There was a general internal error within JokeAPI. You can get more info from"
+ " the properties in the response text."
)
)
523 -> httpException = HttpErrorException(
responseCode, "Origin Unreachable", IOException(
"The server is temporarily offline due to maintenance or a dynamic IP update."
+ " Please be patient in this case."
)
)
else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
}
return httpException
}
/**
* Returns a [Joke] instance.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param splitNewLine Split newline within [Type.SINGLE] joke.
* @see [getRawJokes]
*/
@JvmStatic
@Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getJoke(
categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
search: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
splitNewLine: Boolean = false
): Joke {
val json = JSONObject(
getRawJokes(
categories = categories,
language = language,
flags = flags,
type = type,
search = search,
idRange = idRange,
safe = safe
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return parseJoke(json, splitNewLine)
}
}
/**
* Returns an array of [Joke] instances.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param amount The required amount of jokes to return.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
* @see [getRawJokes]
*/
@JvmStatic
@Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getJokes(
amount: Int,
categories: Set<Category> = setOf(Category.ANY),
language: Language = Language.ENGLISH,
flags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
search: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
splitNewLine: Boolean = false
): Array<Joke> {
val json = JSONObject(
getRawJokes(
categories = categories,
language = language,
flags = flags,
type = type,
search = search,
idRange = idRange,
amount = amount,
safe = safe
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return if (json.has("amount")) {
val jokes = json.getJSONArray("jokes")
Array(jokes.length()) { i -> parseJoke(jokes.getJSONObject(i), splitNewLine) }
} else {
arrayOf(parseJoke(json, splitNewLine))
}
}
}
/**
* Retrieve a [Joke] instance using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getJoke(config: JokeConfig): Joke {
return getJoke(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
search = config.search,
idRange = config.idRange,
safe = config.safe,
splitNewLine = config.splitNewLine
)
}
/**
* Returns an array of [Joke] instances using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
fun getJokes(config: JokeConfig): Array<Joke> {
return getJokes(
categories = config.categories,
language = config.language,
flags = config.flags,
type = config.type,
search = config.search,
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
splitNewLine = config.splitNewLine
)
}
private fun parseError(json: JSONObject): JokeException {
val causedBy = json.getJSONArray("causedBy")
val causes = List<String>(causedBy.length()) { i -> causedBy.getString(i) }
return JokeException(
internalError = json.getBoolean("internalError"),
code = json.getInt("code"),
message = json.getString("message"),
causedBy = causes,
additionalInfo = json.getString("additionalInfo"),
timestamp = json.getLong("timestamp")
)
}
private fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
val jokes = mutableListOf<String>()
if (json.has("setup")) {
jokes.add(json.getString("setup"))
jokes.add(json.getString(("delivery")))
} else {
if (splitNewLine) {
jokes.addAll(json.getString("joke").split("\n"))
} else {
jokes.add(json.getString("joke"))
}
}
val enabledFlags = mutableSetOf<Flag>()
val jsonFlags = json.getJSONObject("flags")
Flag.values().filter { it != Flag.ALL }.forEach {
if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
enabledFlags.add(it)
}
}
return Joke(
category = Category.valueOf(json.getString("category").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(Parameter.LANG).uppercase())
)
}
}
return JokeApi.apiCall("joke", path, params, auth)
}

View file

@ -53,9 +53,10 @@ class JokeConfig private constructor(
val format: Format,
val search: String,
val idRange: IdRange,
val amount: Int = 1,
val amount: Int,
val safe: Boolean,
val splitNewLine: Boolean,
val auth: String
) {
/**
* [Builds][build] a new configuration.
@ -74,7 +75,8 @@ class JokeConfig private constructor(
var idRange: IdRange = IdRange(),
var amount: Int = 1,
var safe: Boolean = false,
var splitNewLine: Boolean = false
var splitNewLine: Boolean = false,
var auth: String = ""
) {
fun categories(categories: Set<Category>) = apply { this.categories = categories }
fun language(language: Language) = apply { this.language = language }
@ -86,9 +88,10 @@ class JokeConfig private constructor(
fun amount(amount: Int) = apply { this.amount = amount }
fun safe(safe: Boolean) = apply { this.safe = safe }
fun splitNewLine(splitNewLine: Boolean) = apply { this.splitNewLine = splitNewLine }
fun auth(auth: String) = apply { this.auth = auth }
fun build() = JokeConfig(
categories, language, flags, type, format, search, idRange, amount, safe, splitNewLine
categories, language, flags, type, format, search, idRange, amount, safe, splitNewLine, auth
)
}
}

View file

@ -45,7 +45,7 @@ class JokeException @JvmOverloads constructor(
val additionalInfo: String,
val timestamp: Long,
cause: Throwable? = null
) : Exception(message, cause) {
) : RuntimeException(message, cause) {
companion object {
private const val serialVersionUID = 1L
}

View file

@ -0,0 +1,172 @@
/*
* util.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
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
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
import java.net.HttpURLConnection
import java.net.URL
import java.util.logging.Level
internal fun fetchUrl(url: String, auth: String = ""): String {
if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(url)
}
val connection = URL(url).openConnection() as HttpURLConnection
connection.setRequestProperty(
"User-Agent", "Mozilla/5.0 (Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
)
if (auth.isNotEmpty()) {
connection.setRequestProperty("Authentication", auth)
}
if (connection.responseCode in 200..399) {
val body = connection.inputStream.bufferedReader().readText()
if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(body)
}
return body
} else {
throw httpError(connection.responseCode)
}
}
private fun httpError(responseCode: Int): HttpErrorException {
val httpException: HttpErrorException
when (responseCode) {
400 -> httpException = HttpErrorException(
responseCode, "Bad Request", IOException(
"The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
)
)
403 -> httpException = HttpErrorException(
responseCode, "Forbidden", IOException(
"You have been added to the blacklist due to malicious behavior and are not allowed"
+ " to send requests to JokeAPI anymore."
)
)
404 -> httpException = HttpErrorException(
responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
)
413 -> httpException = HttpErrorException(
responseCode, "URI Too Long", IOException("The URL exceeds the maximum length of 250 characters.")
)
414 -> httpException = HttpErrorException(
responseCode,
"Payload Too Large",
IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
)
429 -> httpException = HttpErrorException(
responseCode, "Too Many Requests", IOException(
"You have exceeded the limit of 120 requests per minute and have to wait a bit"
+ " until you are allowed to send requests again."
)
)
500 -> httpException = HttpErrorException(
responseCode, "Internal Server Error", IOException(
"There was a general internal error within JokeAPI. You can get more info from"
+ " the properties in the response text."
)
)
523 -> httpException = HttpErrorException(
responseCode, "Origin Unreachable", IOException(
"The server is temporarily offline due to maintenance or a dynamic IP update."
+ " Please be patient in this case."
)
)
else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
}
return httpException
}
internal fun parseError(json: JSONObject): JokeException {
val causedBy = json.getJSONArray("causedBy")
val causes = List<String>(causedBy.length()) { i -> causedBy.getString(i) }
return JokeException(
internalError = json.getBoolean("internalError"),
code = json.getInt("code"),
message = json.getString("message"),
causedBy = causes,
additionalInfo = json.getString("additionalInfo"),
timestamp = json.getLong("timestamp")
)
}
internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
val jokes = mutableListOf<String>()
if (json.has("setup")) {
jokes.add(json.getString("setup"))
jokes.add(json.getString(("delivery")))
} else {
if (splitNewLine) {
jokes.addAll(json.getString("joke").split("\n"))
} else {
jokes.add(json.getString("joke"))
}
}
val enabledFlags = mutableSetOf<Flag>()
val jsonFlags = json.getJSONObject("flags")
Flag.values().filter { it != Flag.ALL }.forEach {
if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
enabledFlags.add(it)
}
}
return Joke(
category = Category.valueOf(json.getString("category").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(Parameter.LANG).uppercase())
)
}

View file

@ -44,8 +44,6 @@ import assertk.assertions.isNull
import assertk.assertions.prop
import assertk.assertions.size
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.Companion.fetchUrl
import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
@ -59,8 +57,6 @@ import java.util.logging.ConsoleHandler
import java.util.logging.Level
internal class ExceptionsTest {
private val httpStat = "https://httpstat.us"
@Test
fun `Validate Joke Exception`() {
val e = assertThrows<JokeException> {
@ -78,28 +74,18 @@ internal class ExceptionsTest {
}
}
@ParameterizedTest(name = "HTTP Status Code: {0}")
@ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523])
fun `Validate HTTP Error Exceptions`(input: Int) {
@ParameterizedTest
@ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
fun `Validate HTTP Exceptions`(code: Int) {
val e = assertThrows<HttpErrorException> {
fetchUrl("$httpStat/$input")
fetchUrl("https://httpstat.us/$code")
}
assertThat(e, "fetchUrl($httpStat/$input)").all {
prop(HttpErrorException::statusCode).isEqualTo(input)
assertThat(e, "fetchUrl($code)").all {
prop(HttpErrorException::statusCode).isEqualTo(code)
prop(HttpErrorException::message).isNotNull().isNotEmpty()
if (code < 600)
prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull()
}
}
@Test
fun `Fetch Invalid URL`() {
val statusCode = 999
val e = assertThrows<HttpErrorException> {
fetchUrl("$httpStat/$statusCode")
}
assertThat(e, "fetchUrl($httpStat/$statusCode).statusCode").all {
prop(HttpErrorException::statusCode).isEqualTo(statusCode)
prop(HttpErrorException::message).isNotNull().isNotEmpty()
else
prop(HttpErrorException::cause).isNull()
}
}

View file

@ -49,7 +49,6 @@ import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category

View file

@ -1,5 +1,5 @@
/*
* GetJokeTest.kt
* GetJokeTests.kt
*
* Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -44,7 +44,6 @@ import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.Companion.getJokes
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
@ -79,10 +78,6 @@ internal class GetJokesTest {
@Test
fun `Get One Joke as Multiple`() {
val jokes = getJokes(amount = 1, safe = true)
jokes.forEach {
println(it.joke.joinToString("\n"))
println("-".repeat(46))
}
assertThat(jokes, "jokes").all {
size().isEqualTo(1)
index(0).all {

View file

@ -1,5 +1,5 @@
/*
* GetRawJokeTest.kt
* GetRawJokesTest.kt
*
* Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -37,9 +37,9 @@ import assertk.assertThat
import assertk.assertions.doesNotContain
import assertk.assertions.isNotEmpty
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.Companion.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import java.util.logging.ConsoleHandler
@ -74,6 +74,12 @@ internal class GetRawJokesTest {
assertContains(response, "\"amount\": 2", false, "getRawJoke(2)")
}
@Test
fun `Get Raw Invalid Jokes`() {
val response = getRawJokes(search = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
}
companion object {
@JvmStatic
@BeforeAll

View file

@ -60,6 +60,15 @@ import java.util.logging.Level
import kotlin.test.assertContains
class JokeConfigTest {
@Test
fun `Get Joke with Default Builder`() {
val joke = getJoke()
assertThat(joke, "joke").all {
prop(Joke::id).isGreaterThanOrEqualTo(0)
prop(Joke::language).isEqualTo(Language.EN)
}
}
@Test
fun `Get Joke with Builder`() {
val id = 266
@ -132,6 +141,47 @@ class JokeConfigTest {
}
}
@Test
fun `Validate Config`() {
val categories = setOf(Category.ANY)
val language = Language.CS
val flags = setOf(Flag.POLITICAL, Flag.RELIGIOUS)
val type = Type.TWOPART
val format = Format.XML
val search = "foo"
val idRange = IdRange(1, 20)
val amount = 10
val safe = true
val splitNewLine = true
val auth = "token"
val config = JokeConfig.Builder().apply {
categories(categories)
language(language)
flags(flags)
type(type)
format(format)
search(search)
idRange(idRange)
amount(amount)
safe(safe)
splitNewLine(splitNewLine)
auth(auth)
}.build()
assertThat(config, "config").all {
prop(JokeConfig::categories).isEqualTo(categories)
prop(JokeConfig::language).isEqualTo(language)
prop(JokeConfig::flags).isEqualTo(flags)
prop(JokeConfig::type).isEqualTo(type)
prop(JokeConfig::format).isEqualTo(format)
prop(JokeConfig::search).isEqualTo(search)
prop(JokeConfig::idRange).isEqualTo(idRange)
prop(JokeConfig::amount).isEqualTo(amount)
prop(JokeConfig::safe).isEqualTo(safe)
prop(JokeConfig::splitNewLine).isEqualTo(splitNewLine)
prop(JokeConfig::auth).isEqualTo(auth)
}
}
companion object {
@JvmStatic
@BeforeAll

View file

@ -0,0 +1,73 @@
/*
* FetchUrlTest.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
import assertk.assertThat
import assertk.assertions.contains
import org.json.JSONException
import org.json.JSONObject
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.logging.ConsoleHandler
import java.util.logging.Level
class UtilTest {
@Test
fun `Invalid JSON Error`() {
assertThrows<JSONException> { parseError(JSONObject("{}")) }
}
@Test
fun `Invalid JSON Joke`() {
assertThrows<JSONException> { parseJoke(JSONObject("{}"), false) }
}
@Test
fun `Validate Authentication Header`() {
val token = "AUTH-TOKEN"
val body = fetchUrl("https://postman-echo.com/get", token)
assertThat(body, "body").contains("\"authentication\":\"$token\"")
}
companion object {
@JvmStatic
@BeforeAll
fun beforeAll() {
with(JokeApi.logger) {
addHandler(ConsoleHandler().apply { level = Level.FINE })
level = Level.FINE
}
}
}
}