diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2922277..8dabc3f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,53 +1,62 @@
-version: 2
+version: 2.1
+
+orbs:
+ sdkman: joshdholtz/sdkman@0.2.0
+
defaults: &defaults
working_directory: ~/repo
environment:
JVM_OPTS: -Xmx3200m
TERM: dumb
- CI: true
+ CI_NAME: "CircleCI"
+
+commands:
+ build_and_test:
+ parameters:
+ reports-dir:
+ type: string
+ default: "build/reports/test_results"
+ steps:
+ - checkout
+ - sdkman/setup-sdkman
+ - sdkman/sdkman-install:
+ candidate: kotlin
+ version: 2.0.20
+ - run:
+ name: Download dependencies
+ command: ./bld download
+ - run:
+ name: Compile source
+ command: ./bld compile
+ - run:
+ name: Run tests
+ command: ./bld jacoco -reports-dir=<< parameters.reports-dir >>
+ - store_test_results:
+ path: << parameters.reports-dir >>
+ - store_artifacts:
+ path: build/reports/jacoco/test/html
-defaults_gradle: &defaults_gradle
- steps:
- - checkout
- - restore_cache:
- keys:
- - gradle-dependencies-{{ checksum "build.gradle.kts" }}
- # fallback to using the latest cache if no exact match is found
- - gradle-dependencies-
- - run:
- name: Gradle Dependencies
- command: ./gradlew dependencies
- - save_cache:
- paths: ~/.m2
- key: gradle-dependencies-{{ checksum "build.gradle.kts" }}
- - run:
- name: Run All Checks
- command: ./gradlew check
- - store_artifacts:
- path: build/reports/
- destination: reports
- - store_test_results:
- path: build/reports/
jobs:
- build_gradle_jdk17:
+ bld_jdk17:
<<: *defaults
docker:
- image: cimg/openjdk:17.0
- <<: *defaults_gradle
+ steps:
+ - build_and_test
- build_gradle_jdk11:
+ bld_jdk20:
<<: *defaults
docker:
- - image: cimg/openjdk:11.0
+ - image: cimg/openjdk:20.0
- <<: *defaults_gradle
+ steps:
+ - build_and_test
workflows:
- version: 2
- gradle:
+ bld:
jobs:
- - build_gradle_jdk11
- - build_gradle_jdk17
+ - bld_jdk17
+ - bld_jdk20
diff --git a/.github/workflows/bld.yml b/.github/workflows/bld.yml
new file mode 100644
index 0000000..007e63a
--- /dev/null
+++ b/.github/workflows/bld.yml
@@ -0,0 +1,51 @@
+name: bld-ci
+
+on: [ push, pull_request, workflow_dispatch ]
+
+env:
+ COVERAGE_JDK: "21"
+ COVERAGE_KOTLIN: "2.1.20"
+
+jobs:
+ build-bld-project:
+ strategy:
+ matrix:
+ java-version: [ 17, 21, 24 ]
+ kotlin-version: [ 1.9.25, 2.0.21, 2.1.20 ]
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout source repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }}
+ uses: actions/setup-java@v4
+ with:
+ distribution: "zulu"
+ java-version: ${{ matrix.java-version }}
+
+ - name: Download dependencies
+ run: ./bld download
+
+ - name: Compile source
+ run: ./bld compile
+
+ - name: Run tests
+ run: ./bld jacoco
+
+ - name: Remove pom.xml
+ if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
+ && matrix.os == 'ubuntu-latest'
+ run: rm -rf pom.xml
+
+ - name: SonarCloud Scan
+ uses: sonarsource/sonarcloud-github-action@master
+ if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
+ && matrix.os == 'ubuntu-latest'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
deleted file mode 100644
index 42bc1da..0000000
--- a/.github/workflows/gradle.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: gradle-ci
-
-on: [ push, pull_request, workflow_dispatch ]
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- env:
- GRADLE_OPTS: "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m"
- SONAR_JDK: "17"
-
- strategy:
- matrix:
- java-version: [ 11, 17, 20 ]
-
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - name: Set up JDK ${{ matrix.java-version }}
- uses: actions/setup-java@v3
- with:
- distribution: 'zulu'
- java-version: ${{ matrix.java-version }}
-
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
-
- - name: Cache SonarCloud packages
- if: matrix.java-version == env.SONAR_JDK
- uses: actions/cache@v3
- with:
- path: ~/.sonar/cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
-
- - name: Test with Gradle
- uses: gradle/gradle-build-action@v2
- with:
- arguments: build check --stacktrace
-
- - name: SonarCloud
- if: success() && matrix.java-version == env.SONAR_JDK
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: ./gradlew sonar --info
diff --git a/.gitignore b/.gitignore
index 0742f86..ea86fe8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,84 +1,57 @@
-!.vscode/extensions.json
-!.vscode/launch.json
-!.vscode/settings.json
-!.vscode/tasks.json
-*.class
-*.code-workspace
-*.ctxt
-*.iws
-*.log
-*.nar
-*.rar
-*.sublime-*
-*.tar.gz
-*.zip
-.DS_Store
-.classpath
.gradle
-.history
-.kobalt
-.mtj.tmp/
-.mvn/timing.properties
-.mvn/wrapper/maven-wrapper.jar
-.nb-gradle
-.project
-.scannerwork
-.settings
-.vscode/*
-/**/.idea/$CACHE_FILE$
-/**/.idea/$PRODUCT_WORKSPACE_FILE$
-/**/.idea/**/caches/build_file_checksums.ser
-/**/.idea/**/contentModel.xml
-/**/.idea/**/dataSources.ids
-/**/.idea/**/dataSources.local.xml
-/**/.idea/**/dataSources/
-/**/.idea/**/dbnavigator.xml
-/**/.idea/**/dictionaries
-/**/.idea/**/dynamic.xml
-/**/.idea/**/gradle.xml
-/**/.idea/**/httpRequests
-/**/.idea/**/libraries
-/**/.idea/**/mongoSettings.xml
-/**/.idea/**/replstate.xml
-/**/.idea/**/shelf
-/**/.idea/**/shelf/
-/**/.idea/**/sqlDataSources.xml
-/**/.idea/**/tasks.xml
-/**/.idea/**/uiDesigner.xml
-/**/.idea/**/usage.statistics.xml
-/**/.idea/**/workspace.xml
-/**/.idea/sonarlint*
-/**/.idea_modules/
-Thumbs.db
-__pycache__
+.DS_Store
+build
+lib/bld/**
+!lib/bld/bld-wrapper.jar
+!lib/bld/bld-wrapper.properties
+lib/compile/
+lib/runtime/
+lib/standalone/
+lib/test/
+
+# IDEA ignores
+
+# User-specific
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
atlassian-ide-plugin.xml
-bin/
-build/
-cmake-build-*/
-com_crashlytics_export_strings.xml
-crashlytics-build.properties
-crashlytics.properties
-dependency-reduced-pom.xml
-deploy/
-dist/
-ehthumbs.db
-fabric.properties
-gen/
-hs_err_pid*
-kobaltBuild
-kobaltw*-test
-lib/kotlin*
-libs/
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Editor-based Rest Client
+.idea/httpRequests
+
local.properties
-out/
-pom.xml.asc
-pom.xml.next
-pom.xml.releaseBackup
-pom.xml.tag
-pom.xml.versionsBackup
-proguard-project.txt
-project.properties
-release.properties
-target/
-test-output
-venv
diff --git a/.idea/app.iml b/.idea/app.iml
new file mode 100644
index 0000000..2c1fe21
--- /dev/null
+++ b/.idea/app.iml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/bld.iml b/.idea/bld.iml
new file mode 100644
index 0000000..e63e11e
--- /dev/null
+++ b/.idea/bld.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/bld.xml b/.idea/bld.xml
new file mode 100644
index 0000000..6600cee
--- /dev/null
+++ b/.idea/bld.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/intellij-javadocs-4.0.1.xml b/.idea/intellij-javadocs-4.0.1.xml
new file mode 100644
index 0000000..4b17413
--- /dev/null
+++ b/.idea/intellij-javadocs-4.0.1.xml
@@ -0,0 +1,204 @@
+
+
+
+
+ UPDATE
+ false
+ true
+
+ TYPE
+ METHOD
+ FIELD
+
+
+ DEFAULT
+ PROTECTED
+ PUBLIC
+
+
+
+
+
+ ^.*(public|protected|private)*.+interface\s+\w+.*
+ /**\n
+ * The interface ${name}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+ */
+
+
+ ^.*(public|protected|private)*.+enum\s+\w+.*
+ /**\n
+ * The enum ${name}.\n
+ */
+
+
+ ^.*(public|protected|private)*.+class\s+\w+.*
+ /**\n
+ * The type ${name}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+ */
+
+
+ .+
+ /**\n
+ * The type ${name}.\n
+ */
+
+
+
+
+ .+
+ /**\n
+ * Instantiates a new ${name}.\n
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+
+
+ ^.*(public|protected|private)*\s*.*(\w(\s*<.+>)*)+\s+get\w+\s*\(.*\).+
+ /**\n
+ * Gets ${partName}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${partName}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ ^.*(public|protected|private)*\s*.*(void|\w(\s*<.+>)*)+\s+set\w+\s*\(.*\).+
+ /**\n
+ * Sets ${partName}.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${partName}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ ^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+
+ /**\n
+ * The entry point of application.\n
+
+ <#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+ * @param ${element.parameterList.parameters[0].name} the input arguments\n
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+ .+
+ /**\n
+ * ${name}<#if isNotVoid> ${return}</#if>.\n
+<#if element.typeParameters?has_content> * \n
+</#if>
+<#list element.typeParameters as typeParameter>
+ * @param <${typeParameter.name}> the type parameter\n
+</#list>
+<#if element.parameterList.parameters?has_content>
+ *\n
+</#if>
+<#list element.parameterList.parameters as parameter>
+ * @param ${parameter.name} the ${paramNames[parameter.name]}\n
+</#list>
+<#if isNotVoid>
+ *\n
+ * @return the ${return}\n
+</#if>
+<#if element.throwsList.referenceElements?has_content>
+ *\n
+</#if>
+<#list element.throwsList.referenceElements as exception>
+ * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
+</#list>
+ */
+
+
+
+
+ ^.*(public|protected|private)*.+static.*(\w\s\w)+.+
+ /**\n
+ * The constant ${element.getName()}.\n
+ */
+
+
+ ^.*(public|protected|private)*.*(\w\s\w)+.+
+ /**\n
+ <#if element.parent.isInterface()>
+ * The constant ${element.getName()}.\n
+<#else>
+ * The ${name}.\n
+</#if> */
+
+
+ .+
+ /**\n
+ <#if element.parent.isEnum()>
+ *${name} ${typeName}.\n
+<#else>
+ * The ${name}.\n
+</#if>*/
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index f8467b4..a8d9757 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml
new file mode 100644
index 0000000..153a060
--- /dev/null
+++ b/.idea/libraries/bld.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml
new file mode 100644
index 0000000..99cc0c0
--- /dev/null
+++ b/.idea/libraries/compile.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml
new file mode 100644
index 0000000..d4069f2
--- /dev/null
+++ b/.idea/libraries/runtime.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml
new file mode 100644
index 0000000..57ed5ef
--- /dev/null
+++ b/.idea/libraries/test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index caf34dd..f2b4c1e 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,9 +1,21 @@
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..55adcb9
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index 4331a4d..82ecd17 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
diff --git a/README.md b/README.md
index 3627830..bb86972 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
[](https://opensource.org/licenses/BSD-3-Clause)
-[](https://kotlinlang.org/)
-[](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/)
+[](https://kotlinlang.org/)
+[](https://rife2.com/bld)
[](https://github.com/ethauvin/jokeapi/releases/latest)
[](https://central.sonatype.com/artifact/net.thauvin.erik/jokeapi)
+[](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/)
[](https://sonarcloud.io/dashboard?id=ethauvin_jokeapi)
-[](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml)
+[](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml)
[](https://circleci.com/gh/ethauvin/jokeapi/tree/master)
# JokeAPI for Kotlin, Java and Android
@@ -15,7 +16,7 @@ A simple library to retrieve jokes from [Sv443's JokeAPI](https://v2.jokeapi.dev
## Examples (TL;DR)
```kotlin
-import net.thauvin.erik.jokeapi.getJoke
+import net.thauvin.erik.jokeapi.joke
val joke = joke()
val safe = joke(safe = true)
@@ -88,6 +89,19 @@ var config = new JokeConfig.Builder()
var joke = JokeApi.joke(config);
joke.getJoke().forEach(System.out::println);
```
+
+## bld
+
+To use with [bld](https://rife2.com/bld), include the following dependency in your build file:
+
+```java
+repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
+
+scope(compile)
+ .include(dependency("net.thauvin.erik", "jokeapi", "1.0.0"));
+```
+Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
+
## Gradle, Maven, etc.
To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
@@ -98,7 +112,7 @@ repositories {
}
dependencies {
- implementation("net.thauvin.erik:jokeapi:0.9.0")
+ implementation("net.thauvin.erik:jokeapi:1.0.0")
}
```
@@ -110,9 +124,10 @@ You can also retrieve one or more raw (unprocessed) jokes in all [supported form
For example for YAML:
```kotlin
-var joke = getRawJokes(format = Format.YAML, idRange = IdRange(22))
-println(joke)
+var jokes = getRawJokes(format = Format.YAML, idRange = IdRange(22))
+println(jokes.data)
```
+
```yaml
error: false
category: "Programming"
@@ -128,8 +143,8 @@ flags:
id: 22
safe: true
lang: "en"
-
```
+
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt#L46)...
## Extending
@@ -139,15 +154,37 @@ 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 = JokeApi.apiCall(
+val response = JokeApi.apiCall(
endPoint = "langcode",
path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)
)
-println(lang)
+if (response.statusCode == 200) {
+ println(response.data)
+}
```
+
```yaml
error: false
code: "fr"
```
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt#L48)...
+
+## Contributing
+
+If you want to contribute to this project, all you have to do is clone the GitHub
+repository:
+
+```console
+git clone git@github.com:ethauvin/jokeapi.git
+```
+
+Then use [bld](https://rife2.com/bld) to build:
+
+```console
+cd jokeapi
+./bld compile
+```
+
+The project has an [IntelliJ IDEA](https://www.jetbrains.com/idea/) project structure. You can just open it after all
+the dependencies were downloaded and peruse the code.
diff --git a/bin/main/net/thauvin/erik/jokeapi/JokeApi.kt b/bin/main/net/thauvin/erik/jokeapi/JokeApi.kt
new file mode 100644
index 0000000..b4df9aa
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/JokeApi.kt
@@ -0,0 +1,318 @@
+/*
+ * JokeApi.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.*
+import net.thauvin.erik.urlencoder.UrlEncoderUtil
+import org.json.JSONObject
+import java.util.logging.Logger
+import java.util.stream.Collectors
+
+/**
+ * Implements the [Sv443's JokeAPI](https://jokeapi.dev/).
+ */
+object JokeApi {
+ private const val API_URL = "https://v2.jokeapi.dev/"
+
+ @JvmStatic
+ val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
+
+ /**
+ * Makes a direct API call.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
+ */
+ @JvmStatic
+ @JvmOverloads
+ @Throws(HttpErrorException::class)
+ fun apiCall(
+ endPoint: String,
+ path: String = "",
+ params: Map = emptyMap(),
+ auth: String = ""
+ ): 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("=").append(UrlEncoderUtil.encode(param.value))
+ }
+ if (it.hasNext()) {
+ urlBuilder.append("&")
+ }
+ }
+ }
+ 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 rawJokes(
+ categories = config.categories,
+ lang = config.language,
+ blacklistFlags = config.flags,
+ type = config.type,
+ format = config.format,
+ contains = config.contains,
+ 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 joke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
+ return joke(
+ categories = config.categories,
+ lang = config.language,
+ blacklistFlags = config.flags,
+ type = config.type,
+ contains = config.contains,
+ idRange = config.idRange,
+ safe = config.safe,
+ auth = config.auth,
+ 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(HttpErrorException::class, JokeException::class)
+ fun jokes(config: JokeConfig): Array {
+ return jokes(
+ categories = config.categories,
+ lang = config.language,
+ blacklistFlags = config.flags,
+ type = config.type,
+ contains = config.contains,
+ idRange = config.idRange,
+ amount = config.amount,
+ safe = config.safe,
+ auth = config.auth,
+ splitNewLine = config.splitNewLine
+ )
+ }
+}
+
+
+/**
+ * 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 joke(
+ categories: Set = setOf(Category.ANY),
+ lang: Language = Language.EN,
+ blacklistFlags: Set = emptySet(),
+ type: Type = Type.ALL,
+ contains: String = "",
+ idRange: IdRange = IdRange(),
+ safe: Boolean = false,
+ auth: String = "",
+ splitNewLine: Boolean = false
+): Joke {
+ val json = JSONObject(
+ rawJokes(
+ categories = categories,
+ lang = lang,
+ blacklistFlags = blacklistFlags,
+ type = type,
+ contains = contains,
+ 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 jokes(
+ amount: Int,
+ categories: Set = setOf(Category.ANY),
+ lang: Language = Language.EN,
+ blacklistFlags: Set = emptySet(),
+ type: Type = Type.ALL,
+ contains: String = "",
+ idRange: IdRange = IdRange(),
+ safe: Boolean = false,
+ auth: String = "",
+ splitNewLine: Boolean = false
+): Array {
+ val json = JSONObject(
+ rawJokes(
+ categories = categories,
+ lang = lang,
+ blacklistFlags = blacklistFlags,
+ type = type,
+ contains = contains,
+ 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.
+ */
+fun rawJokes(
+ categories: Set = setOf(Category.ANY),
+ lang: Language = Language.EN,
+ blacklistFlags: Set = emptySet(),
+ type: Type = Type.ALL,
+ format: Format = Format.JSON,
+ contains: String = "",
+ idRange: IdRange = IdRange(),
+ amount: Int = 1,
+ safe: Boolean = false,
+ auth: String = ""
+): String {
+ val params = mutableMapOf()
+
+ // Categories
+ val path = if (categories.isEmpty() || categories.contains(Category.ANY)) {
+ Category.ANY.value
+ } else {
+ categories.stream().map(Category::value).collect(Collectors.joining(","))
+ }
+
+ // Language
+ if (lang != Language.EN) {
+ params[Parameter.LANG] = lang.value
+ }
+
+ // Flags
+ if (blacklistFlags.isNotEmpty()) {
+ if (blacklistFlags.contains(Flag.ALL)) {
+ params[Parameter.FLAGS] = Flag.ALL.value
+ } else {
+ params[Parameter.FLAGS] = blacklistFlags.stream().map(Flag::value).collect(Collectors.joining(","))
+ }
+ }
+
+ // Type
+ if (type != Type.ALL) {
+ params[Parameter.TYPE] = type.value
+ }
+
+ // Format
+ if (format != Format.JSON) {
+ params[Parameter.FORMAT] = format.value
+ }
+
+ // Contains
+ if (contains.isNotBlank()) {
+ params[Parameter.CONTAINS] = contains
+ }
+
+ // Range
+ if (idRange.start >= 0) {
+ if (idRange.end == -1 || idRange.start == idRange.end) {
+ params[Parameter.RANGE] = idRange.start.toString()
+ } else {
+ require(idRange.end > idRange.start) { "Invalid ID Range: ${idRange.start}, ${idRange.end}" }
+ params[Parameter.RANGE] = "${idRange.start}-${idRange.end}"
+ }
+ }
+
+ // Amount
+ require(amount > 0) { "Invalid Amount: $amount" }
+ if (amount > 1) {
+ params[Parameter.AMOUNT] = amount.toString()
+ }
+
+ // Safe
+ if (safe) {
+ params[Parameter.SAFE] = ""
+ }
+
+ return JokeApi.apiCall("joke", path, params, auth)
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/JokeConfig.kt b/bin/main/net/thauvin/erik/jokeapi/JokeConfig.kt
new file mode 100644
index 0000000..544383c
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/JokeConfig.kt
@@ -0,0 +1,96 @@
+/*
+ * JokeConfig.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.JokeConfig.Builder
+import net.thauvin.erik.jokeapi.models.Category
+import net.thauvin.erik.jokeapi.models.Flag
+import net.thauvin.erik.jokeapi.models.Format
+import net.thauvin.erik.jokeapi.models.IdRange
+import net.thauvin.erik.jokeapi.models.Language
+import net.thauvin.erik.jokeapi.models.Type
+
+/**
+ * Joke Configuration.
+ *
+ * Use the [Builder] to create a new configuration.
+ */
+class JokeConfig private constructor(
+ val categories: Set,
+ val language: Language,
+ val flags: Set,
+ val type: Type,
+ val format: Format,
+ val contains: String,
+ val idRange: IdRange,
+ val amount: Int,
+ val safe: Boolean,
+ val splitNewLine: Boolean,
+ val auth: String
+) {
+ /**
+ * [Builds][build] a new configuration.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ *
+ * @param splitNewLine Split newline within [Type.SINGLE] joke.
+ */
+ data class Builder(
+ var categories: Set = setOf(Category.ANY),
+ var lang: Language = Language.EN,
+ var blacklistFlags: Set = emptySet(),
+ var type: Type = Type.ALL,
+ var format: Format = Format.JSON,
+ var contains: String = "",
+ var idRange: IdRange = IdRange(),
+ var amount: Int = 1,
+ var safe: Boolean = false,
+ var splitNewLine: Boolean = false,
+ var auth: String = ""
+ ) {
+ fun categories(categories: Set) = apply { this.categories = categories }
+ fun lang(language: Language) = apply { lang = language }
+ fun blacklistFlags(flags: Set) = apply { blacklistFlags = flags }
+ fun type(type: Type) = apply { this.type = type }
+ fun format(format: Format) = apply { this.format = format }
+ fun contains(search: String) = apply { contains = search }
+ fun idRange(idRange: IdRange) = apply { this.idRange = idRange }
+ 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, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth
+ )
+ }
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/JokeUtil.kt b/bin/main/net/thauvin/erik/jokeapi/JokeUtil.kt
new file mode 100644
index 0000000..9d838f8
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/JokeUtil.kt
@@ -0,0 +1,173 @@
+/*
+ * JokeUtil.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.
+ */
+
+@file:JvmName("JokeUtil")
+
+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 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
+ )
+ if (auth.isNotEmpty()) {
+ connection.setRequestProperty("Authentication", auth)
+ }
+
+ if (connection.responseCode in 200..399) {
+ val body = connection.inputStream.bufferedReader().use { it.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(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()
+ 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()
+ 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"),
+ lang = Language.valueOf(json.getString(Parameter.LANG).uppercase())
+ )
+}
+
diff --git a/bin/main/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt b/bin/main/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt
new file mode 100644
index 0000000..cd17ca8
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt
@@ -0,0 +1,49 @@
+/*
+ * HttpErrorException.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.exceptions
+
+import java.io.IOException
+
+/**
+ * Signals that a server error has occurred.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#status-codes) for more details.
+ */
+class HttpErrorException @JvmOverloads constructor(
+ val statusCode: Int,
+ message: String,
+ cause: Throwable? = null
+) : IOException(message, cause) {
+ companion object {
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/exceptions/JokeException.kt b/bin/main/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
new file mode 100644
index 0000000..919216e
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
@@ -0,0 +1,56 @@
+/*
+ * JokeException.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.exceptions
+
+/**
+ * Signals that an error has occurred.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#errors) for more details.
+ */
+class JokeException @JvmOverloads constructor(
+ val internalError: Boolean,
+ val code: Int,
+ message: String,
+ val causedBy: List,
+ val additionalInfo: String,
+ val timestamp: Long,
+ cause: Throwable? = null
+) : RuntimeException(message, cause) {
+ companion object {
+ private const val serialVersionUID = 1L
+ }
+
+ fun debug(): String {
+ return "JokeException(message=$message, internalError=$internalError, code=$code," +
+ " causedBy=$causedBy, additionalInfo='$additionalInfo', timestamp=$timestamp)"
+ }
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Category.kt b/bin/main/net/thauvin/erik/jokeapi/models/Category.kt
new file mode 100644
index 0000000..4951d4a
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Category.kt
@@ -0,0 +1,45 @@
+/*
+ * Category.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The supported [categories](https://jokeapi.dev/#categories), use [ANY] for all.
+ */
+enum class Category(val value: String) {
+ ANY("Any"),
+ CHRISTMAS("Christmas"),
+ DARK("Dark"),
+ MISC("Misc"),
+ PROGRAMMING("Programming"),
+ PUN("Pun"),
+ SPOOKY("Spooky")
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Flag.kt b/bin/main/net/thauvin/erik/jokeapi/models/Flag.kt
new file mode 100644
index 0000000..af92e90
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Flag.kt
@@ -0,0 +1,45 @@
+/*
+ * Flag.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The supported [blacklist flags](https://jokeapi.dev/#flags-param), use [ALL] to prevent all.
+ */
+enum class Flag(val value: String) {
+ EXPLICIT("explicit"),
+ NSFW("nsfw"),
+ POLITICAL("political"),
+ RACIST("racist"),
+ RELIGIOUS("religious"),
+ SEXIST("sexist"),
+ ALL("${NSFW.value},${RELIGIOUS.value},${POLITICAL.value},${RACIST.value},${SEXIST.value},${EXPLICIT.value}")
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Format.kt b/bin/main/net/thauvin/erik/jokeapi/models/Format.kt
new file mode 100644
index 0000000..2678a21
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Format.kt
@@ -0,0 +1,44 @@
+/*
+ * Format.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The supported response [formats](https://jokeapi.dev/#format-param).
+ */
+enum class Format(val value: String) {
+ JSON("json"),
+
+ /** Plain Text */
+ TXT("txt"),
+ XML("xml"),
+ YAML("yaml")
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/IdRange.kt b/bin/main/net/thauvin/erik/jokeapi/models/IdRange.kt
new file mode 100644
index 0000000..62a6eb6
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/IdRange.kt
@@ -0,0 +1,37 @@
+/*
+ * IdRange.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * Specifies a joke [ID or range of IDs](https://jokeapi.dev/#idrange-param).
+ */
+data class IdRange(val start: Int = -1, val end: Int = -1)
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Joke.kt b/bin/main/net/thauvin/erik/jokeapi/models/Joke.kt
new file mode 100644
index 0000000..0309977
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Joke.kt
@@ -0,0 +1,45 @@
+/*
+ * Joke.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * Stores a joke's data.
+ */
+data class Joke(
+ val category: Category,
+ val type: Type,
+ val joke: List,
+ val flags: Set,
+ val id: Int,
+ val safe: Boolean,
+ val lang: Language
+)
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Language.kt b/bin/main/net/thauvin/erik/jokeapi/models/Language.kt
new file mode 100644
index 0000000..10c00fb
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Language.kt
@@ -0,0 +1,55 @@
+/*
+ * Language.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The supported [languages](https://jokeapi.dev/#lang).
+ */
+enum class Language(val value: String) {
+ /** Czech */
+ CS("cs"),
+
+ /** German */
+ DE("de"),
+
+ /** English */
+ EN("en"),
+
+ /** Spanish */
+ ES("es"),
+
+ /** French */
+ FR("fr"),
+
+ /** Portuguese */
+ PT("pt")
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Parameter.kt b/bin/main/net/thauvin/erik/jokeapi/models/Parameter.kt
new file mode 100644
index 0000000..b9e1106
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Parameter.kt
@@ -0,0 +1,51 @@
+/*
+ * Parameter.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The available [URL Parameters](https://jokeapi.dev/#url-parameters).
+ */
+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
+ const val SEARCH = CONTAINS
+}
diff --git a/bin/main/net/thauvin/erik/jokeapi/models/Type.kt b/bin/main/net/thauvin/erik/jokeapi/models/Type.kt
new file mode 100644
index 0000000..59126b4
--- /dev/null
+++ b/bin/main/net/thauvin/erik/jokeapi/models/Type.kt
@@ -0,0 +1,41 @@
+/*
+ * Type.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The supported [types](https://jokeapi.dev/#type-param), use [ALL] for all.
+ */
+enum class Type(val value: String) {
+ SINGLE("single"),
+ TWOPART("twopart"),
+ ALL("${SINGLE.value},${TWOPART.value}")
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/ApiCallTest.kt b/bin/test/net/thauvin/erik/jokeapi/ApiCallTest.kt
new file mode 100644
index 0000000..d9f9b30
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/ApiCallTest.kt
@@ -0,0 +1,87 @@
+/*
+ * ApiCallTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.isGreaterThan
+import assertk.assertions.startsWith
+import net.thauvin.erik.jokeapi.JokeApi.apiCall
+import net.thauvin.erik.jokeapi.models.Format
+import net.thauvin.erik.jokeapi.models.Language
+import net.thauvin.erik.jokeapi.models.Parameter
+import org.json.JSONObject
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
+import org.junit.jupiter.api.extension.ExtendWith
+import kotlin.test.assertContains
+
+@ExtendWith(BeforeAllTests::class)
+internal class ApiCallTest {
+ @Test
+ fun `Get Flags`() {
+ // See https://v2.jokeapi.dev/#flags-endpoint
+ val response = apiCall(endPoint = "flags")
+ val json = JSONObject(response)
+ assertAll("Validate JSON",
+ { assertFalse(json.getBoolean("error"), "apiCall(flags).error") },
+ { assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) },
+ { assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) })
+ }
+
+ @Test
+ fun `Get Language Code`() {
+ // See https://v2.jokeapi.dev/#langcode-endpoint
+ val lang = apiCall(
+ endPoint = "langcode", path = "french",
+ params = mapOf(Parameter.FORMAT to Format.YAML.value)
+ )
+ assertContains(lang, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
+ }
+
+ @Test
+ fun `Get Ping Response`() {
+ // See https://v2.jokeapi.dev/#ping-endpoint
+ val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value))
+ assertThat(ping, "apiCall(ping, txt)").startsWith("Pong!")
+ }
+
+ @Test
+ fun `Get Supported Language`() {
+ // See https://v2.jokeapi.dev/languages
+ val lang = apiCall(
+ endPoint = "languages",
+ params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value)
+ )
+ assertThat(lang).startsWith("")
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/BeforeAllTests.kt b/bin/test/net/thauvin/erik/jokeapi/BeforeAllTests.kt
new file mode 100644
index 0000000..de9d48a
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/BeforeAllTests.kt
@@ -0,0 +1,47 @@
+/*
+ * BeforeAllTests.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 org.junit.jupiter.api.extension.BeforeAllCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+import java.util.logging.ConsoleHandler
+import java.util.logging.Level
+
+class BeforeAllTests : BeforeAllCallback {
+ override fun beforeAll(context: ExtensionContext?) {
+ with(JokeApi.logger) {
+ addHandler(ConsoleHandler().apply { level = Level.FINE })
+ level = Level.FINE
+ }
+ }
+}
+
diff --git a/bin/test/net/thauvin/erik/jokeapi/ExceptionsTest.kt b/bin/test/net/thauvin/erik/jokeapi/ExceptionsTest.kt
new file mode 100644
index 0000000..adacf75
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/ExceptionsTest.kt
@@ -0,0 +1,90 @@
+/*
+ * ExceptionsTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.all
+import assertk.assertThat
+import assertk.assertions.index
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isGreaterThan
+import assertk.assertions.isNotEmpty
+import assertk.assertions.isNotNull
+import assertk.assertions.isNull
+import assertk.assertions.prop
+import assertk.assertions.size
+import assertk.assertions.startsWith
+import net.thauvin.erik.jokeapi.JokeApi.logger
+import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
+import net.thauvin.erik.jokeapi.exceptions.JokeException
+import net.thauvin.erik.jokeapi.models.Category
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+@ExtendWith(BeforeAllTests::class)
+internal class ExceptionsTest {
+ @Test
+ fun `Validate Joke Exception`() {
+ val e = assertThrows {
+ joke(categories = setOf(Category.CHRISTMAS), contains = "foo")
+ }
+ logger.fine(e.debug())
+ assertThat(e, "joke(${Category.CHRISTMAS},foo)").all {
+ prop(JokeException::code).isEqualTo(106)
+ prop(JokeException::internalError).isFalse()
+ prop(JokeException::message).isEqualTo("No matching joke found")
+ prop(JokeException::causedBy).size().isEqualTo(1)
+ prop(JokeException::causedBy).index(0).startsWith("No jokes")
+ prop(JokeException::additionalInfo).isNotEmpty()
+ prop(JokeException::timestamp).isGreaterThan(0)
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
+ fun `Validate HTTP Exceptions`(code: Int) {
+ val e = assertThrows {
+ fetchUrl("https://httpstat.us/$code")
+ }
+ 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()
+ else
+ prop(HttpErrorException::cause).isNull()
+ }
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/GetJokeTest.kt b/bin/test/net/thauvin/erik/jokeapi/GetJokeTest.kt
new file mode 100644
index 0000000..a2b06db
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/GetJokeTest.kt
@@ -0,0 +1,211 @@
+/*
+ * GetJokeTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.all
+import assertk.assertThat
+import assertk.assertions.any
+import assertk.assertions.contains
+import assertk.assertions.containsNone
+import assertk.assertions.each
+import assertk.assertions.isBetween
+import assertk.assertions.isEmpty
+import assertk.assertions.isEqualTo
+import assertk.assertions.isGreaterThan
+import assertk.assertions.isGreaterThanOrEqualTo
+import assertk.assertions.isIn
+import assertk.assertions.isNotEmpty
+import assertk.assertions.isNotNull
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import assertk.assertions.size
+import net.thauvin.erik.jokeapi.JokeApi.logger
+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.IdRange
+import net.thauvin.erik.jokeapi.models.Joke
+import net.thauvin.erik.jokeapi.models.Language
+import net.thauvin.erik.jokeapi.models.Type
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(BeforeAllTests::class)
+internal class GetJokeTest {
+ @Test
+ fun `Get Joke`() {
+ val joke = joke()
+ logger.fine(joke.toString())
+ assertThat(joke, "joke()").all {
+ prop(Joke::joke).isNotEmpty()
+ prop(Joke::type).isIn(Type.SINGLE, Type.TWOPART)
+ prop(Joke::id).isGreaterThanOrEqualTo(0)
+ prop(Joke::lang).isEqualTo(Language.EN)
+ }
+ }
+
+ @Test
+ fun `Get Joke without Blacklist Flags`() {
+ val joke = joke(blacklistFlags = setOf(Flag.ALL))
+ assertThat(joke::flags).isEmpty()
+ }
+
+ @Test
+ fun `Get Joke without any Blacklist Flags`() {
+ val allFlags = Flag.values().filter { it != Flag.ALL }.toSet()
+ val joke = joke(blacklistFlags = allFlags)
+ assertThat(joke::flags).isEmpty()
+ }
+
+ @Test
+ fun `Get Joke with ID`() {
+ val id = 172
+ val joke = joke(idRange = IdRange(id))
+ logger.fine(joke.toString())
+ assertThat(joke, "joke($id)").all {
+ prop(Joke::flags).all {
+ contains(Flag.EXPLICIT)
+ contains(Flag.NSFW)
+ }
+ prop(Joke::id).isEqualTo(172)
+ prop(Joke::category).isEqualTo(Category.PUN)
+ }
+ }
+
+ @Test
+ fun `Get Joke with ID Range`() {
+ val idRange = IdRange(1, 100)
+ val joke = joke(idRange = idRange)
+ logger.fine(joke.toString())
+ assertThat(joke::id).isBetween(idRange.start, idRange.end)
+ }
+
+ @Test
+ fun `Get Joke with invalid ID Range`() {
+ val idRange = IdRange(100, 1)
+ val e = assertThrows { joke(idRange = idRange, lang = Language.DE) }
+ assertThat(e::message).isNotNull().contains("100, 1")
+ }
+
+ @Test
+ fun `Get Joke with max ID Range`() {
+ val idRange = IdRange(1, 30000)
+ val e = assertThrows { joke(idRange = idRange) }
+ assertThat(e, "joke{${idRange})").all {
+ prop(JokeException::additionalInfo).contains("ID range")
+ }
+ }
+
+ @Test
+ fun `Get Joke with two Categories`() {
+ val joke = joke(categories = setOf(Category.PROGRAMMING, Category.MISC))
+ logger.fine(joke.toString())
+ assertThat(joke.category, "joke(${Category.PROGRAMMING},${Category.MISC})").isIn(
+ Category.PROGRAMMING,
+ Category.MISC
+ )
+ }
+
+ @Test
+ fun `Get Joke with each Categories`() {
+ Category.values().filter { it != Category.ANY }.forEach {
+ val joke = joke(categories = setOf(it))
+ logger.fine(joke.toString())
+ assertThat(joke::category, "joke($it)").prop(Category::value).isEqualTo(it.value)
+ }
+ }
+
+ @Test
+ fun `Get Joke with each Languages`() {
+ Language.values().forEach {
+ val joke = joke(lang = it)
+ logger.fine(joke.toString())
+ assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value)
+ }
+ }
+
+ @Test
+ fun `Get Joke with Split Newline`() {
+ val joke = joke(
+ categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true
+ )
+ logger.fine(joke.toString())
+ assertThat(joke::joke, "joke(splitNewLine=true)").all {
+ size().isEqualTo(2)
+ each {
+ containsNone("\n")
+ }
+ }
+ }
+
+ @Test
+ fun `Get Safe Joke`() {
+ val joke = joke(safe = true)
+ logger.fine(joke.toString())
+ assertThat(joke, "joke(safe)").all {
+ prop(Joke::safe).isTrue()
+ }
+ }
+
+ @Test
+ fun `Get Single Joke`() {
+ val joke = joke(type = Type.SINGLE)
+ logger.fine(joke.toString())
+ assertThat(joke::type).assertThat(Type.SINGLE)
+ }
+
+ @Test
+ fun `Get Two-Parts Joke`() {
+ val joke = joke(type = Type.TWOPART)
+ logger.fine(joke.toString())
+ assertThat(joke, "joke(${Type.TWOPART})").all {
+ prop(Joke::type).isEqualTo(Type.TWOPART)
+ prop(Joke::joke).size().isGreaterThan(1)
+ }
+ }
+
+ @Test
+ fun `Get Joke using Search`() {
+ val id = 265
+ val search = "his wife"
+ val joke =
+ joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true)
+ logger.fine(joke.toString())
+ assertThat(joke, "joke($search)").all {
+ prop(Joke::id).isEqualTo(id)
+ prop(Joke::joke).any {
+ it.contains(search)
+ }
+ }
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/GetJokesTest.kt b/bin/test/net/thauvin/erik/jokeapi/GetJokesTest.kt
new file mode 100644
index 0000000..1ab8b60
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/GetJokesTest.kt
@@ -0,0 +1,84 @@
+/*
+ * GetJokesTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.each
+import assertk.assertions.index
+import assertk.assertions.isEqualTo
+import assertk.assertions.isGreaterThanOrEqualTo
+import assertk.assertions.isNotNull
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import assertk.assertions.size
+import net.thauvin.erik.jokeapi.models.Joke
+import net.thauvin.erik.jokeapi.models.Language
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(BeforeAllTests::class)
+internal class GetJokesTest {
+ @Test
+ fun `Get Multiple Jokes`() {
+ val amount = 2
+ val jokes = jokes(amount = amount, safe = true, lang = Language.FR)
+ assertThat(jokes, "jokes").all {
+ size().isEqualTo(amount)
+ each {
+ it.prop(Joke::id).isGreaterThanOrEqualTo(0)
+ it.prop(Joke::safe).isTrue()
+ it.prop(Joke::lang).isEqualTo(Language.FR)
+ }
+ }
+ }
+
+ @Test
+ fun `Get Jokes with Invalid Amount`() {
+ val e = assertThrows { jokes(amount = -1) }
+ assertThat(e::message).isNotNull().contains("-1")
+ }
+
+ @Test
+ fun `Get One Joke as Multiple`() {
+ val jokes = jokes(amount = 1, safe = true)
+ assertThat(jokes, "jokes").all {
+ size().isEqualTo(1)
+ index(0).all {
+ prop(Joke::id).isGreaterThanOrEqualTo(0)
+ prop(Joke::safe).isTrue()
+ }
+ }
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/GetRawJokesTest.kt b/bin/test/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
new file mode 100644
index 0000000..7bcf1c6
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
@@ -0,0 +1,79 @@
+/*
+ * GetRawJokesTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.all
+import assertk.assertThat
+import assertk.assertions.doesNotContain
+import assertk.assertions.isNotEmpty
+import assertk.assertions.startsWith
+import net.thauvin.erik.jokeapi.models.Format
+import net.thauvin.erik.jokeapi.models.IdRange
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import kotlin.test.assertContains
+
+@ExtendWith(BeforeAllTests::class)
+internal class GetRawJokesTest {
+ @Test
+ fun `Get Raw Joke with TXT`() {
+ val response = rawJokes(format = Format.TXT)
+ assertThat(response, "rawJoke(txt)").all {
+ isNotEmpty()
+ doesNotContain("Error")
+ }
+ }
+
+ @Test
+ fun `Get Raw Joke with XML`() {
+ val response = rawJokes(format = Format.XML)
+ assertThat(response, "rawJoke(xml)").startsWith("\n\n false")
+ }
+
+ @Test
+ fun `Get Raw Joke with YAML`() {
+ val response = rawJokes(format = Format.YAML)
+ assertThat(response, "rawJoke(yaml)").startsWith("error: false")
+ }
+
+ @Test
+ fun `Get Raw Jokes`() {
+ val response = rawJokes(amount = 2)
+ assertContains(response, "\"amount\": 2", false, "rawJoke(2)")
+ }
+
+ @Test
+ fun `Get Raw Invalid Jokes`() {
+ val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
+ assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/JokeConfigTest.kt b/bin/test/net/thauvin/erik/jokeapi/JokeConfigTest.kt
new file mode 100644
index 0000000..92de2e0
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/JokeConfigTest.kt
@@ -0,0 +1,182 @@
+/*
+ * JokeConfigTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.all
+import assertk.assertThat
+import assertk.assertions.each
+import assertk.assertions.isBetween
+import assertk.assertions.isEmpty
+import assertk.assertions.isEqualTo
+import assertk.assertions.isGreaterThanOrEqualTo
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import assertk.assertions.size
+import net.thauvin.erik.jokeapi.JokeApi.joke
+import net.thauvin.erik.jokeapi.JokeApi.jokes
+import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
+import net.thauvin.erik.jokeapi.JokeApi.logger
+import net.thauvin.erik.jokeapi.models.Category
+import net.thauvin.erik.jokeapi.models.Flag
+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.Type
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import kotlin.test.assertContains
+
+@ExtendWith(BeforeAllTests::class)
+internal class JokeConfigTest {
+ @Test
+ fun `Get Joke with Default Builder`() {
+ val joke = joke()
+ assertThat(joke, "joke").all {
+ prop(Joke::id).isGreaterThanOrEqualTo(0)
+ prop(Joke::lang).isEqualTo(Language.EN)
+ }
+ }
+
+ @Test
+ fun `Get Joke with Builder`() {
+ val id = 266
+ val config = JokeConfig.Builder().apply {
+ categories(setOf(Category.PROGRAMMING))
+ lang(Language.EN)
+ blacklistFlags(setOf(Flag.ALL))
+ type(Type.TWOPART)
+ idRange(IdRange(id - 2, id + 2))
+ safe(true)
+ }.build()
+ val joke = joke(config)
+ logger.fine(joke.toString())
+ assertThat(joke, "config").all {
+ prop(Joke::type).isEqualTo(Type.TWOPART)
+ prop(Joke::category).isEqualTo(Category.PROGRAMMING)
+ prop(Joke::joke).size().isEqualTo(2)
+ prop(Joke::lang).isEqualTo(Language.EN)
+ prop(Joke::flags).isEmpty()
+ prop(Joke::id).isBetween(id - 2, id + 2)
+ }
+ }
+
+ @Test
+ fun `Get joke with Builder and Split Newline`() {
+ val id = 5
+ val config = JokeConfig.Builder().apply {
+ categories(setOf(Category.PROGRAMMING))
+ idRange(IdRange(id))
+ splitNewLine(true)
+ }.build()
+ val joke = joke(config)
+ logger.fine(joke.toString())
+ assertThat(joke, "config").all {
+ prop(Joke::id).isEqualTo(id)
+ prop(Joke::joke).size().isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun `Get Raw Joke with Builder`() {
+ val config = JokeConfig.Builder().apply {
+ categories(setOf(Category.PROGRAMMING))
+ format(Format.TXT)
+ contains("bar")
+ amount(2)
+ safe(true)
+ }.build()
+ val joke = getRawJokes(config)
+ assertContains(joke, "----------------------------------------------", false, "config.amount(2)")
+ }
+
+ @Test
+ fun `Get Multiple Jokes with Builder`() {
+ val amount = 2
+ val config = JokeConfig.Builder().apply {
+ amount(amount)
+ safe(true)
+ lang(Language.FR)
+ }.build()
+ val jokes = jokes(config)
+ assertThat(jokes, "jokes").all {
+ size().isEqualTo(amount)
+ each {
+ it.prop(Joke::id).isGreaterThanOrEqualTo(0)
+ it.prop(Joke::safe).isTrue()
+ it.prop(Joke::flags).isEmpty()
+ it.prop(Joke::lang).isEqualTo(Language.FR)
+ }
+ }
+ }
+
+ @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)
+ lang(language)
+ blacklistFlags(flags)
+ type(type)
+ format(format)
+ contains(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::contains).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)
+ }
+ }
+}
diff --git a/bin/test/net/thauvin/erik/jokeapi/JokeUtilTest.kt b/bin/test/net/thauvin/erik/jokeapi/JokeUtilTest.kt
new file mode 100644
index 0000000..8f8d936
--- /dev/null
+++ b/bin/test/net/thauvin/erik/jokeapi/JokeUtilTest.kt
@@ -0,0 +1,60 @@
+/*
+ * UtilTest.kt
+ *
+ * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(BeforeAllTests::class)
+internal class JokeUtilTest {
+ @Test
+ fun `Invalid JSON Error`() {
+ assertThrows { parseError(JSONObject("{}")) }
+ }
+
+ @Test
+ fun `Invalid JSON Joke`() {
+ assertThrows { 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\"")
+ }
+}
diff --git a/bld b/bld
new file mode 100755
index 0000000..bfff52f
--- /dev/null
+++ b/bld
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+java -jar "$(dirname "$0")/lib/bld/bld-wrapper.jar" "$0" --build net.thauvin.erik.JokeApiBuild "$@"
diff --git a/bld.bat b/bld.bat
new file mode 100644
index 0000000..9f7473c
--- /dev/null
+++ b/bld.bat
@@ -0,0 +1,4 @@
+@echo off
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+java -jar "%DIRNAME%/lib/bld/bld-wrapper.jar" "%0" --build net.thauvin.erik.JokeApiBuild %*
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index fbea59b..0000000
--- a/build.gradle.kts
+++ /dev/null
@@ -1,185 +0,0 @@
-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import org.gradle.api.tasks.testing.logging.TestLogEvent
-import org.jetbrains.dokka.gradle.DokkaTask
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-plugins {
- id("com.github.ben-manes.versions") version "0.48.0"
- id("io.gitlab.arturbosch.detekt") version "1.23.1"
- id("java")
- id("maven-publish")
- id("org.jetbrains.dokka") version "1.9.0"
- id("org.jetbrains.kotlinx.kover") version "0.7.3"
- id("org.sonarqube") version "4.3.1.3277"
- id("signing")
- kotlin("jvm") version "1.9.10"
-}
-
-description = "Retrieve jokes from Sv443's JokeAPI"
-group = "net.thauvin.erik"
-version = "0.9.0"
-
-val deployDir = "deploy"
-val gitHub = "ethauvin/$name"
-val mavenUrl = "https://github.com/$gitHub"
-val publicationName = "mavenJava"
-
-repositories {
- mavenCentral()
- maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
-}
-
-dependencies {
- implementation(platform(kotlin("bom")))
-
- implementation("net.thauvin.erik.urlencoder:urlencoder-lib:1.4.0")
- implementation("org.json:json:20230618")
-
- testImplementation(kotlin("test"))
- testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
- testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.27.0")
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- withSourcesJar()
-}
-
-koverReport {
- defaults {
- xml {
- onCheck = true
- }
- html {
- onCheck = true
- }
- }
-}
-
-sonarqube {
- properties {
- property("sonar.projectKey", "ethauvin_$name")
- property("sonar.organization", "ethauvin-github")
- property("sonar.host.url", "https://sonarcloud.io")
- property("sonar.sourceEncoding", "UTF-8")
- property("sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/kover/report.xml")
- }
-}
-
-val javadocJar by tasks.creating(Jar::class) {
- dependsOn(tasks.dokkaJavadoc)
- from(tasks.dokkaJavadoc)
- archiveClassifier.set("javadoc")
-}
-
-tasks {
- test {
- useJUnitPlatform()
- }
-
- withType().configureEach {
- kotlinOptions.jvmTarget = java.targetCompatibility.toString()
- }
-
- withType {
- testLogging {
- exceptionFormat = TestExceptionFormat.FULL
- events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
- }
- }
-
- withType().configureEach {
- this.jvmTarget = java.targetCompatibility.toString()
- }
-
- withType().configureEach {
- this.jvmTarget = java.targetCompatibility.toString()
- }
-
- withType {
- destination = file("$projectDir/pom.xml")
- }
-
- clean {
- doLast {
- project.delete(fileTree(deployDir))
- }
- }
-
- withType().configureEach {
- dokkaSourceSets {
- named("main") {
- moduleName.set("Joke API")
- }
- }
- }
-
- val copyToDeploy by registering(Copy::class) {
- from(configurations.runtimeClasspath) {
- exclude("annotations-*.jar")
- }
- from(jar)
- into(deployDir)
- }
-
- register("deploy") {
- description = "Copies all needed files to the $deployDir directory."
- group = PublishingPlugin.PUBLISH_TASK_GROUP
- dependsOn(clean, build, jar)
- outputs.dir(deployDir)
- inputs.files(copyToDeploy)
- mustRunAfter(clean)
- }
-}
-
-publishing {
- publications {
- create(publicationName) {
- from(components["java"])
- artifact(javadocJar)
- pom {
- name.set(project.name)
- description.set(project.description)
- url.set(mavenUrl)
- licenses {
- license {
- name.set("BSD 3-Clause")
- url.set("https://opensource.org/licenses/BSD-3-Clause")
- }
- }
- developers {
- developer {
- id.set("ethauvin")
- name.set("Erik C. Thauvin")
- email.set("erik@thauvin.net")
- url.set("https://erik.thauvin.net/")
- }
- }
- scm {
- connection.set("scm:git:https://github.com/$gitHub.git")
- developerConnection.set("scm:git:git@github.com:$gitHub.git")
- url.set(mavenUrl)
- }
- issueManagement {
- system.set("GitHub")
- url.set("$mavenUrl/issues")
- }
- }
- }
- }
- repositories {
- maven {
- name = "ossrh"
- url = if (project.version.toString().contains("SNAPSHOT"))
- uri("https://oss.sonatype.org/content/repositories/snapshots/") else
- uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
- credentials(PasswordCredentials::class)
- }
- }
-}
-
-signing {
- useGpgCmd()
- sign(publishing.publications[publicationName])
-}
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index a53ce7c..1a99819 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -1,12 +1,12 @@
-
+
-
+
- LongParameterList:JokeApi.kt$( amount: Int, categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )
- LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )
- LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )
- LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )
- LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List<String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )
+ LongParameterList:JokeApi.kt$( amount: Int, categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )
+ LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )
+ LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )
+ LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )
+ LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List<String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )
MagicNumber:JokeUtil.kt$200
MagicNumber:JokeUtil.kt$399
MagicNumber:JokeUtil.kt$400
@@ -18,6 +18,15 @@
MagicNumber:JokeUtil.kt$500
MagicNumber:JokeUtil.kt$523
TooManyFunctions:JokeConfig.kt$JokeConfig$Builder
+ WildcardImport:ExceptionsTest.kt$import assertk.assertions.*
+ WildcardImport:GetJokeTest.kt$import assertk.assertions.*
+ WildcardImport:GetJokeTest.kt$import net.thauvin.erik.jokeapi.models.*
+ WildcardImport:GetJokesTest.kt$import assertk.assertions.*
+ WildcardImport:GetRawJokesTest.kt$import assertk.assertions.*
WildcardImport:JokeApi.kt$import net.thauvin.erik.jokeapi.models.*
+ WildcardImport:JokeConfig.kt$import net.thauvin.erik.jokeapi.models.*
+ WildcardImport:JokeConfigTest.kt$import assertk.assertions.*
+ WildcardImport:JokeConfigTest.kt$import net.thauvin.erik.jokeapi.models.*
+ WildcardImport:JokeUtil.kt$import net.thauvin.erik.jokeapi.models.*
diff --git a/gradle.properties b/gradle.properties
deleted file mode 100644
index 7fc6f1f..0000000
--- a/gradle.properties
+++ /dev/null
@@ -1 +0,0 @@
-kotlin.code.style=official
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 7f93135..0000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index ac72c34..0000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
deleted file mode 100755
index 0adc8e1..0000000
--- a/gradlew
+++ /dev/null
@@ -1,249 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index 93e3f59..0000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,92 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar
new file mode 100644
index 0000000..1eb86cf
Binary files /dev/null and b/lib/bld/bld-wrapper.jar differ
diff --git a/lib/bld/bld-wrapper.properties b/lib/bld/bld-wrapper.properties
new file mode 100644
index 0000000..fc9463a
--- /dev/null
+++ b/lib/bld/bld-wrapper.properties
@@ -0,0 +1,10 @@
+bld.downloadExtensionJavadoc=false
+bld.downloadExtensionSources=true
+bld.downloadLocation=
+bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.10-SNAPSHOT
+bld.extension-dokka=com.uwyn.rife2:bld-dokka:1.0.4-SNAPSHOT
+bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.10-SNAPSHOT
+bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT
+bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
+bld.sourceDirectories=
+bld.version=2.2.1
diff --git a/pom.xml b/pom.xml
index 31a633b..e480d48 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,16 +1,12 @@
-
-
-
-
-
-
+
4.0.0
net.thauvin.erik
jokeapi
- 0.9.0
+ 1.0.1-SNAPSHOT
jokeapi
- Retrieve jokes from Sv443's JokeAPI
+ Retrieve jokes from Sv443's JokeAPI
https://github.com/ethauvin/jokeapi
@@ -18,6 +14,26 @@
https://opensource.org/licenses/BSD-3-Clause
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 2.1.20
+ compile
+
+
+ org.json
+ json
+ 20250107
+ compile
+
+
+ net.thauvin.erik.urlencoder
+ urlencoder-lib-jvm
+ 1.6.0
+ compile
+
+
ethauvin
@@ -31,39 +47,4 @@
scm:git:git@github.com:ethauvin/jokeapi.git
https://github.com/ethauvin/jokeapi
-
- GitHub
- https://github.com/ethauvin/jokeapi/issues
-
-
-
-
- org.jetbrains.kotlin
- kotlin-bom
- 1.9.10
- pom
- import
-
-
-
-
-
- org.jetbrains.kotlin
- kotlin-stdlib-jdk8
- 1.9.10
- compile
-
-
- net.thauvin.erik.urlencoder
- urlencoder-lib-jvm
- 1.4.0
- runtime
-
-
- org.json
- json
- 20230618
- runtime
-
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
deleted file mode 100644
index dc0111b..0000000
--- a/settings.gradle.kts
+++ /dev/null
@@ -1,3 +0,0 @@
-
-rootProject.name = "jokeapi"
-
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..5fe9b51
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.organization=ethauvin-github
+sonar.projectKey=ethauvin_jokeapi
+sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
+sonar.sources=src/main/kotlin/
+sonar.tests=src/test/kotlin/
+sonar.java.binaries=build/main,build/test
+sonar.java.libraries=lib/compile/*.jar
diff --git a/src/bld/java/net/thauvin/erik/JokeApiBuild.java b/src/bld/java/net/thauvin/erik/JokeApiBuild.java
new file mode 100644
index 0000000..62b9d9a
--- /dev/null
+++ b/src/bld/java/net/thauvin/erik/JokeApiBuild.java
@@ -0,0 +1,197 @@
+/*
+ * JokeApiBuild.java
+ *
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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;
+
+import rife.bld.BuildCommand;
+import rife.bld.Project;
+import rife.bld.extension.CompileKotlinOperation;
+import rife.bld.extension.DetektOperation;
+import rife.bld.extension.DokkaOperation;
+import rife.bld.extension.JacocoReportOperation;
+import rife.bld.extension.dokka.LoggingLevel;
+import rife.bld.extension.dokka.OutputFormat;
+import rife.bld.extension.kotlin.CompileOptions;
+import rife.bld.operations.exceptions.ExitStatusException;
+import rife.bld.publish.PomBuilder;
+import rife.bld.publish.PublishDeveloper;
+import rife.bld.publish.PublishLicense;
+import rife.bld.publish.PublishScm;
+import rife.tools.exceptions.FileUtilsErrorException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static rife.bld.dependencies.Repository.*;
+import static rife.bld.dependencies.Scope.compile;
+import static rife.bld.dependencies.Scope.test;
+
+public class JokeApiBuild extends Project {
+ final File srcMainKotlin = new File(srcMainDirectory(), "kotlin");
+
+ public JokeApiBuild() {
+ pkg = "net.thauvin.erik";
+ name = "jokeapi";
+ version = version(1, 0, 1, "SNAPSHOT");
+
+ javaRelease = 11;
+ downloadSources = true;
+ autoDownloadPurge = true;
+ repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL);
+
+ final var kotlin = version(2, 1, 20);
+ scope(compile)
+ .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
+ .include(dependency("org.json", "json", "20250107"))
+ .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 6, 0)));
+ scope(test)
+ .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
+ .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1)))
+ .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1)))
+ .include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 1)))
+ .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)));
+
+ publishOperation()
+ .repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location())
+ .withCredentials(property("sonatype.user"), property("sonatype.password"))
+ : repository(SONATYPE_RELEASES_LEGACY.location())
+ .withCredentials(property("sonatype.user"), property("sonatype.password")))
+ .repository(repository("github"))
+ .info()
+ .groupId(pkg)
+ .artifactId(name)
+ .description("Retrieve jokes from Sv443's JokeAPI")
+ .url("https://github.com/ethauvin/" + name)
+ .developer(
+ new PublishDeveloper()
+ .id("ethauvin")
+ .name("Erik C. Thauvin")
+ .email("erik@thauvin.net")
+ .url("https://erik.thauvin.net/")
+ )
+ .license(
+ new PublishLicense()
+ .name("BSD 3-Clause")
+ .url("https://opensource.org/licenses/BSD-3-Clause")
+ )
+ .scm(
+ new PublishScm()
+ .connection("scm:git:https://github.com/ethauvin/" + name + ".git")
+ .developerConnection("scm:git:git@github.com:ethauvin/" + name + ".git")
+ .url("https://github.com/ethauvin/" + name)
+ )
+ .signKey(property("sign.key"))
+ .signPassphrase(property("sign.passphrase"));
+
+ jarSourcesOperation().sourceDirectories(srcMainKotlin);
+ }
+
+ public static void main(String[] args) {
+ // Enable detailed logging for the extensions
+ var level = Level.ALL;
+ var logger = Logger.getLogger("rife.bld.extension");
+ var consoleHandler = new ConsoleHandler();
+
+ consoleHandler.setLevel(level);
+ logger.addHandler(consoleHandler);
+ logger.setLevel(level);
+ logger.setUseParentHandlers(false);
+
+ new JokeApiBuild().start(args);
+ }
+
+ @BuildCommand(summary = "Compiles the Kotlin project")
+ @Override
+ public void compile() throws Exception {
+ new CompileKotlinOperation()
+ .fromProject(this)
+ .compileOptions(new CompileOptions().verbose(true))
+ .execute();
+ }
+
+ @BuildCommand(summary = "Checks source with Detekt")
+ public void detekt() throws ExitStatusException, IOException, InterruptedException {
+ new DetektOperation()
+ .fromProject(this)
+ .execute();
+ }
+
+ @BuildCommand(value = "detekt-baseline", summary = "Creates the Detekt baseline")
+ public void detektBaseline() throws ExitStatusException, IOException, InterruptedException {
+ new DetektOperation()
+ .fromProject(this)
+ .baseline("detekt-baseline.xml")
+ .createBaseline(true)
+ .execute();
+ }
+
+ @BuildCommand(summary = "Generates JaCoCo Reports")
+ public void jacoco() throws Exception {
+ new JacocoReportOperation()
+ .fromProject(this)
+ .sourceFiles(srcMainKotlin)
+ .execute();
+ }
+
+ @Override
+ public void javadoc() throws ExitStatusException, IOException, InterruptedException {
+ new DokkaOperation()
+ .fromProject(this)
+ .loggingLevel(LoggingLevel.INFO)
+ .moduleName("JokeApi")
+ .moduleVersion(version.toString())
+ .outputDir(new File(buildDirectory(), "javadoc"))
+ .outputFormat(OutputFormat.JAVADOC)
+ .execute();
+ }
+
+ @Override
+ public void publish() throws Exception {
+ super.publish();
+ pomRoot();
+ }
+
+ @Override
+ public void publishLocal() throws Exception {
+ super.publishLocal();
+ pomRoot();
+ }
+
+ @BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory")
+ public void pomRoot() throws FileUtilsErrorException {
+ PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(),
+ new File(workDirectory, "pom.xml"));
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
index b4df9aa..474aa27 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
@@ -1,7 +1,7 @@
/*
* JokeApi.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -45,13 +45,16 @@ import java.util.stream.Collectors
object JokeApi {
private const val API_URL = "https://v2.jokeapi.dev/"
+ /**
+ * The logger instance.
+ */
@JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
/**
* Makes a direct API call.
*
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
+ * See the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
*/
@JvmStatic
@JvmOverloads
@@ -61,7 +64,7 @@ object JokeApi {
path: String = "",
params: Map = emptyMap(),
auth: String = ""
- ): String {
+ ): JokeResponse {
val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) {
@@ -95,11 +98,11 @@ object JokeApi {
*/
@JvmStatic
@Throws(HttpErrorException::class)
- fun getRawJokes(config: JokeConfig): String {
+ fun getRawJokes(config: JokeConfig): JokeResponse {
return rawJokes(
categories = config.categories,
- lang = config.language,
- blacklistFlags = config.flags,
+ lang = config.lang,
+ blacklistFlags = config.blacklistFlags,
type = config.type,
format = config.format,
contains = config.contains,
@@ -121,8 +124,8 @@ object JokeApi {
fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return joke(
categories = config.categories,
- lang = config.language,
- blacklistFlags = config.flags,
+ lang = config.lang,
+ blacklistFlags = config.blacklistFlags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
@@ -142,8 +145,8 @@ object JokeApi {
fun jokes(config: JokeConfig): Array {
return jokes(
categories = config.categories,
- lang = config.language,
- blacklistFlags = config.flags,
+ lang = config.lang,
+ blacklistFlags = config.blacklistFlags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
@@ -161,6 +164,32 @@ object JokeApi {
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
+ * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
+ * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark
+ * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use
+ * [Category.ANY], which will make JokeAPI randomly choose a category.
+ * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other.
+ * All system messages like errors can have a certain system language, while jokes can only have a joke language.
+ * It is possible, that system languages don't yet exist for your language while jokes already do.
+ * If no suitable system language is found, JokeAPI will default to English.
+ * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be
+ * set on each joke, and they tell you something about the offensiveness of each joke.
+ * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type
+ * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated
+ * because you might want to present the users the delivery after a timeout or in a different section of the UI.
+ * A joke of type [Type.SINGLE] only has a single string, which is the entire joke.
+ * @param contains If the search string filter is used, only jokes that contain the specified string will be returned.
+ * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs.
+ * You don't necessarily need to provide an ID range though, a single ID will work just fine as well.
+ * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will
+ * mean you will only get the 6th joke.
+ * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for
+ * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its
+ * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as
+ * unsafe.
+ * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token.
+ * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business
+ * and need more than 120 requests per minute.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun joke(
@@ -184,7 +213,7 @@ fun joke(
idRange = idRange,
safe = safe,
auth = auth
- )
+ ).data
)
if (json.getBoolean("error")) {
throw parseError(json)
@@ -198,7 +227,35 @@ fun joke(
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
- * @param amount The required amount of jokes to return.
+ * @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the
+ * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a number
+ * larger than 10 will make JokeAPI default to the maximum (10).
+ * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
+ * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark
+ * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use
+ * [Category.ANY], which will make JokeAPI randomly choose a category.
+ * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other.
+ * All system messages like errors can have a certain system language, while jokes can only have a joke language.
+ * It is possible, that system languages don't yet exist for your language while jokes already do.
+ * If no suitable system language is found, JokeAPI will default to English.
+ * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be
+ * set on each joke, and they tell you something about the offensiveness of each joke.
+ * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type
+ * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated
+ * because you might want to present the users the delivery after a timeout or in a different section of the UI.
+ * A joke of type [Type.SINGLE] only has a single string, which is the entire joke.
+ * @param contains If the search string filter is used, only jokes that contain the specified string will be returned.
+ * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs.
+ * You don't necessarily need to provide an ID range though, a single ID will work just fine as well.
+ * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will
+ * mean you will only get the 6th joke.
+ * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for
+ * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its
+ * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as
+ * unsafe.
+ * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token.
+ * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business
+ * and need more than 120 requests per minute.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun jokes(
@@ -224,7 +281,7 @@ fun jokes(
amount = amount,
safe = safe,
auth = auth
- )
+ ).data
)
if (json.getBoolean("error")) {
throw parseError(json)
@@ -241,8 +298,42 @@ fun jokes(
/**
* Returns one or more jokes.
*
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ * See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ *
+ * @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
+ * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark
+ * humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use
+ * [Category.ANY], which will make JokeAPI randomly choose a category.
+ * @param lang There are two types of languages; system languages and joke languages. Both are separate from each other.
+ * All system messages like errors can have a certain system language, while jokes can only have a joke language.
+ * It is possible, that system languages don't yet exist for your language while jokes already do.
+ * If no suitable system language is found, JokeAPI will default to English.
+ * @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be
+ * set on each joke, and they tell you something about the offensiveness of each joke.
+ * @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type
+ * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated
+ * because you might want to present the users the delivery after a timeout or in a different section of the UI.
+ * A joke of type [Type.SINGLE] only has a single string, which is the entire joke.
+ * @param contains If the search string filter is used, only jokes that contain the specified string will be returned.
+ * @param format Response Formats (or just "Formats") are a way to get your data in a different file format.
+ * Maybe your environment or language doesn't support JSON natively. In that case, JokeAPI is able to convert the
+ * JSON-formatted joke to a different format for you.
+ * @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs.
+ * You don't necessarily need to provide an ID range though, a single ID will work just fine as well.
+ * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will
+ * mean you will only get the 6th joke.
+ * @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the
+ * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a number
+ * larger than 10 will make JokeAPI default to the maximum (10).
+ * @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for
+ * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its
+ * references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as
+ * unsafe.
+ * @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token.
+ * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business
+ * and need more than 120 requests per minute.
*/
+@Throws(HttpErrorException::class)
fun rawJokes(
categories: Set = setOf(Category.ANY),
lang: Language = Language.EN,
@@ -254,7 +345,7 @@ fun rawJokes(
amount: Int = 1,
safe: Boolean = false,
auth: String = ""
-): String {
+): JokeResponse {
val params = mutableMapOf()
// Categories
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
index 544383c..a4d4901 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
@@ -1,7 +1,7 @@
/*
* JokeConfig.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -32,31 +32,26 @@
package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.JokeConfig.Builder
-import net.thauvin.erik.jokeapi.models.Category
-import net.thauvin.erik.jokeapi.models.Flag
-import net.thauvin.erik.jokeapi.models.Format
-import net.thauvin.erik.jokeapi.models.IdRange
-import net.thauvin.erik.jokeapi.models.Language
-import net.thauvin.erik.jokeapi.models.Type
+import net.thauvin.erik.jokeapi.models.*
/**
* Joke Configuration.
*
* Use the [Builder] to create a new configuration.
*/
-class JokeConfig private constructor(
- val categories: Set,
- val language: Language,
- val flags: Set,
- val type: Type,
- val format: Format,
- val contains: String,
- val idRange: IdRange,
- val amount: Int,
- val safe: Boolean,
- val splitNewLine: Boolean,
- val auth: String
-) {
+class JokeConfig private constructor(builder: Builder) {
+ val categories = builder.categories
+ val lang = builder.lang
+ val blacklistFlags = builder.blacklistFlags
+ val type = builder.type
+ val format = builder.format
+ val contains = builder.contains
+ val idRange = builder.idRange
+ val amount = builder.amount
+ val safe = builder.safe
+ val splitNewLine = builder.splitNewLine
+ val auth = builder.auth
+
/**
* [Builds][build] a new configuration.
*
@@ -77,20 +72,86 @@ class JokeConfig private constructor(
var splitNewLine: Boolean = false,
var auth: String = ""
) {
- fun categories(categories: Set) = apply { this.categories = categories }
- fun lang(language: Language) = apply { lang = language }
- fun blacklistFlags(flags: Set) = apply { blacklistFlags = flags }
- fun type(type: Type) = apply { this.type = type }
- fun format(format: Format) = apply { this.format = format }
- fun contains(search: String) = apply { contains = search }
- fun idRange(idRange: IdRange) = apply { this.idRange = idRange }
- 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 }
+ /**
+ * JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
+ * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING]
+ * category, dark humor will be in the [Category.DARK] category and so on. If you want jokes from all
+ * categories, you can instead use [Category.ANY], which will make JokeAPI randomly choose a category.
+ */
+ fun categories(categories: Set): Builder = apply { this.categories = categories }
- fun build() = JokeConfig(
- categories, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth
- )
+ /**
+ * There are two types of languages; system languages and joke languages. Both are separate from each other.
+ * All system messages like errors can have a certain system language, while jokes can only have a joke
+ * language. It is possible, that system languages don't yet exist for your language while jokes already do.
+ * If no suitable system language is found, JokeAPI will default to English.
+ */
+ fun lang(language: Language): Builder = apply { lang = language }
+
+ /**
+ * Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be
+ * set on each joke, and they tell you something about the offensiveness of each joke.
+ */
+ fun blacklistFlags(flags: Set): Builder = apply { blacklistFlags = flags }
+
+ /**
+ * Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type
+ * [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are
+ * separated because you might want to present the users the delivery after a timeout or in a different section
+ * of the UI. A joke of type [Type.SINGLE] only has a single string, which is the entire joke.
+ */
+ fun type(type: Type): Builder = apply { this.type = type }
+
+ /**
+ * Response Formats (or just "Formats") are a way to get your data in a different file format.
+ * Maybe your environment or language doesn't support JSON natively. In that case, JokeAPI is able to convert
+ * the JSON-formatted joke to a different format for you.
+ */
+ fun format(format: Format): Builder = apply { this.format = format }
+
+ /**
+ * If the search string filter is used, only jokes that contain the specified string will be returned.
+ */
+ fun contains(search: String): Builder = apply { contains = search }
+
+ /**
+ * If this filter is used, you will only get jokes that are within the provided range of IDs.
+ * You don't necessarily need to provide an ID range though, a single ID will work just fine as well.
+ * For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range
+ * of 5 will mean you will only get the 6th joke.
+ */
+ fun idRange(idRange: IdRange): Builder = apply { this.idRange = idRange }
+
+ /**
+ * This filter allows you to set a certain amount of jokes to receive in a single call. Setting the
+ * filter to an invalid number will result in the API defaulting to serving a single joke. Setting it to a
+ * number larger than 10 will make JokeAPI default to the maximum (10).
+ */
+ fun amount(amount: Int): Builder = apply { this.amount = amount }
+
+ /**
+ * Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for
+ * everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language,
+ * its references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally
+ * marked as unsafe.
+ */
+ fun safe(safe: Boolean): Builder = apply { this.safe = safe }
+
+ /**
+ * Split newline within [Type.SINGLE] joke.
+ */
+ fun splitNewLine(splitNewLine: Boolean): Builder = apply { this.splitNewLine = splitNewLine }
+
+ /**
+ * JokeAPI has a way of whitelisting certain clients. This is achieved through an API token.
+ * At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a
+ * business and need more than 120 requests per minute.
+ */
+ fun auth(auth: String): Builder = apply { this.auth = auth }
+
+ /**
+ * Builds a new configuration.
+ */
+ fun build() = JokeConfig(this)
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt
index 9d838f8..651844c 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeUtil.kt
@@ -1,7 +1,7 @@
/*
* JokeUtil.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -35,39 +35,41 @@ 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 net.thauvin.erik.jokeapi.models.*
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
-import java.net.URL
+import java.net.URI
import java.util.logging.Level
-internal fun fetchUrl(url: String, auth: String = ""): String {
+/**
+ * Fetch a URL.
+ */
+internal fun fetchUrl(url: String, auth: String = ""): JokeResponse {
if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(url)
}
- val connection = URL(url).openConnection() as HttpURLConnection
- connection.setRequestProperty(
- "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
- )
- if (auth.isNotEmpty()) {
- connection.setRequestProperty("Authentication", auth)
- }
+ val connection = URI(url).toURL().openConnection() as HttpURLConnection
+ try {
+ connection.setRequestProperty(
+ "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0"
+ )
+ if (auth.isNotEmpty()) {
+ connection.setRequestProperty("Authentication", auth)
+ }
- if (connection.responseCode in 200..399) {
- val body = connection.inputStream.bufferedReader().use { it.readText() }
- if (JokeApi.logger.isLoggable(Level.FINE)) {
+ val isSuccess = connection.responseCode in 200..399
+ val stream = if (isSuccess) connection.inputStream else connection.errorStream
+ val body = stream.bufferedReader().use { it.readText() }
+ if (!isSuccess && (body.isBlank() || connection.contentType.contains("text/html"))) {
+ throw httpError(connection.responseCode)
+ } else if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(body)
}
- return body
- } else {
- throw httpError(connection.responseCode)
+ return JokeResponse(connection.responseCode, body)
+ } finally {
+ connection.disconnect()
}
}
@@ -128,6 +130,9 @@ private fun httpError(responseCode: Int): HttpErrorException {
return httpException
}
+/**
+ * Parse Error.
+ */
internal fun parseError(json: JSONObject): JokeException {
val causedBy = json.getJSONArray("causedBy")
val causes = List(causedBy.length()) { i -> causedBy.getString(i) }
@@ -141,6 +146,9 @@ internal fun parseError(json: JSONObject): JokeException {
)
}
+/**
+ * Parse Joke.
+ */
internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
val jokes = mutableListOf()
if (json.has("setup")) {
@@ -155,7 +163,7 @@ internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
}
val enabledFlags = mutableSetOf()
val jsonFlags = json.getJSONObject("flags")
- Flag.values().filter { it != Flag.ALL }.forEach {
+ Flag.entries.filter { it != Flag.ALL }.forEach {
if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
enabledFlags.add(it)
}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt
index cd17ca8..f2e8529 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/HttpErrorException.kt
@@ -1,7 +1,7 @@
/*
* HttpErrorException.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
index 919216e..ac77344 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
@@ -1,7 +1,7 @@
/*
* JokeException.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -29,6 +29,8 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+@file:Suppress("ConstPropertyName")
+
package net.thauvin.erik.jokeapi.exceptions
/**
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt
index 4951d4a..cfb008e 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Category.kt
@@ -1,7 +1,7 @@
/*
* Category.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt
index af92e90..be2e21f 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Flag.kt
@@ -1,7 +1,7 @@
/*
* Flag.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt
index 2678a21..1beb9d3 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Format.kt
@@ -1,7 +1,7 @@
/*
* Format.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt
index 62a6eb6..73d45ec 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/IdRange.kt
@@ -1,7 +1,7 @@
/*
* IdRange.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
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 0309977..c2124ae 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Joke.kt
@@ -1,7 +1,7 @@
/*
* Joke.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt
new file mode 100644
index 0000000..d34f2c3
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/JokeResponse.kt
@@ -0,0 +1,39 @@
+/*
+ * JokeResponse.kt
+ *
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The Joke API response.
+ *
+ * @property statusCode The HTTP status code.
+ * @property data The response body text.
+ */
+data class JokeResponse(val statusCode: Int, val data: String)
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt
index 10c00fb..3ee166e 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Language.kt
@@ -1,7 +1,7 @@
/*
* Language.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt
index b9e1106..8962b2a 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Parameter.kt
@@ -1,7 +1,7 @@
/*
* Parameter.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -34,6 +34,7 @@ package net.thauvin.erik.jokeapi.models
/**
* The available [URL Parameters](https://jokeapi.dev/#url-parameters).
*/
+@Suppress("unused")
object Parameter {
const val AMOUNT = "amount"
const val CONTAINS = "contains"
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt
index 59126b4..4fd80fe 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/models/Type.kt
@@ -1,7 +1,7 @@
/*
* Type.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt
index d9f9b30..6153825 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt
@@ -1,7 +1,7 @@
/*
* ApiCallTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -32,6 +32,7 @@
package net.thauvin.erik.jokeapi
import assertk.assertThat
+import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.apiCall
@@ -51,8 +52,9 @@ internal class ApiCallTest {
fun `Get Flags`() {
// See https://v2.jokeapi.dev/#flags-endpoint
val response = apiCall(endPoint = "flags")
- val json = JSONObject(response)
- assertAll("Validate JSON",
+ val json = JSONObject(response.data)
+ assertAll(
+ "Validate JSON",
{ assertFalse(json.getBoolean("error"), "apiCall(flags).error") },
{ assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) },
{ assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) })
@@ -65,14 +67,16 @@ internal class ApiCallTest {
endPoint = "langcode", path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)
)
- assertContains(lang, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
+ assertThat(lang.statusCode).isEqualTo(200)
+ assertContains(lang.data, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
}
@Test
fun `Get Ping Response`() {
// See https://v2.jokeapi.dev/#ping-endpoint
val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value))
- assertThat(ping, "apiCall(ping, txt)").startsWith("Pong!")
+ assertThat(ping.statusCode).isEqualTo(200)
+ assertThat(ping.data).startsWith("Pong!")
}
@Test
@@ -82,6 +86,7 @@ internal class ApiCallTest {
endPoint = "languages",
params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value)
)
- assertThat(lang).startsWith("")
+ assertThat(lang.statusCode).isEqualTo(200)
+ assertThat(lang.data).startsWith("")
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt
index de9d48a..50ce4b2 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/BeforeAllTests.kt
@@ -1,7 +1,7 @@
/*
* BeforeAllTests.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
index adacf75..eb6837a 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
@@ -1,7 +1,7 @@
/*
* ExceptionsTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,16 +33,7 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
-import assertk.assertions.index
-import assertk.assertions.isEqualTo
-import assertk.assertions.isFalse
-import assertk.assertions.isGreaterThan
-import assertk.assertions.isNotEmpty
-import assertk.assertions.isNotNull
-import assertk.assertions.isNull
-import assertk.assertions.prop
-import assertk.assertions.size
-import assertk.assertions.startsWith
+import assertk.assertions.*
import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
@@ -50,8 +41,6 @@ import net.thauvin.erik.jokeapi.models.Category
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
@ExtendWith(BeforeAllTests::class)
internal class ExceptionsTest {
@@ -72,19 +61,20 @@ internal class ExceptionsTest {
}
}
- @ParameterizedTest
- @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
- fun `Validate HTTP Exceptions`(code: Int) {
- val e = assertThrows {
- fetchUrl("https://httpstat.us/$code")
- }
- 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()
- else
- prop(HttpErrorException::cause).isNull()
+ @Test
+ fun `Validate HTTP Exceptions`() {
+ val locs = ArrayList>()
+ locs.add(Pair("https://apichallenges.herokuapp.com/secret/note", 401))
+ locs.add(Pair("https://apichallenges.herokuapp.com/todo", 404))
+
+ for ((url, code) in locs) {
+ val e = assertThrows {
+ fetchUrl(url)
+ }
+ assertThat(e, "fetchUrl($code)").all {
+ prop(HttpErrorException::statusCode).isEqualTo(code)
+ prop(HttpErrorException::message).isNotNull().isNotEmpty()
+ }
}
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
index a2b06db..e5a7d39 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
@@ -1,7 +1,7 @@
/*
* GetJokeTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,29 +33,10 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
-import assertk.assertions.any
-import assertk.assertions.contains
-import assertk.assertions.containsNone
-import assertk.assertions.each
-import assertk.assertions.isBetween
-import assertk.assertions.isEmpty
-import assertk.assertions.isEqualTo
-import assertk.assertions.isGreaterThan
-import assertk.assertions.isGreaterThanOrEqualTo
-import assertk.assertions.isIn
-import assertk.assertions.isNotEmpty
-import assertk.assertions.isNotNull
-import assertk.assertions.isTrue
-import assertk.assertions.prop
-import assertk.assertions.size
+import assertk.assertions.*
import net.thauvin.erik.jokeapi.JokeApi.logger
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.IdRange
-import net.thauvin.erik.jokeapi.models.Joke
-import net.thauvin.erik.jokeapi.models.Language
-import net.thauvin.erik.jokeapi.models.Type
+import net.thauvin.erik.jokeapi.models.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
@@ -82,22 +63,19 @@ internal class GetJokeTest {
@Test
fun `Get Joke without any Blacklist Flags`() {
- val allFlags = Flag.values().filter { it != Flag.ALL }.toSet()
+ val allFlags = Flag.entries.filter { it != Flag.ALL }.toSet()
val joke = joke(blacklistFlags = allFlags)
assertThat(joke::flags).isEmpty()
}
@Test
fun `Get Joke with ID`() {
- val id = 172
+ val id = 201
val joke = joke(idRange = IdRange(id))
logger.fine(joke.toString())
assertThat(joke, "joke($id)").all {
- prop(Joke::flags).all {
- contains(Flag.EXPLICIT)
- contains(Flag.NSFW)
- }
- prop(Joke::id).isEqualTo(172)
+ prop(Joke::flags).contains(Flag.RELIGIOUS);
+ prop(Joke::id).isEqualTo(id)
prop(Joke::category).isEqualTo(Category.PUN)
}
}
@@ -138,7 +116,7 @@ internal class GetJokeTest {
@Test
fun `Get Joke with each Categories`() {
- Category.values().filter { it != Category.ANY }.forEach {
+ Category.entries.filter { it != Category.ANY }.forEach {
val joke = joke(categories = setOf(it))
logger.fine(joke.toString())
assertThat(joke::category, "joke($it)").prop(Category::value).isEqualTo(it.value)
@@ -147,7 +125,7 @@ internal class GetJokeTest {
@Test
fun `Get Joke with each Languages`() {
- Language.values().forEach {
+ Language.entries.forEach {
val joke = joke(lang = it)
logger.fine(joke.toString())
assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value)
@@ -156,12 +134,10 @@ internal class GetJokeTest {
@Test
fun `Get Joke with Split Newline`() {
- val joke = joke(
- categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true
- )
+ val joke = joke(type = Type.SINGLE, idRange = IdRange(18), splitNewLine = true)
logger.fine(joke.toString())
assertThat(joke::joke, "joke(splitNewLine=true)").all {
- size().isEqualTo(2)
+ size().isGreaterThanOrEqualTo(2)
each {
containsNone("\n")
}
@@ -196,13 +172,12 @@ internal class GetJokeTest {
@Test
fun `Get Joke using Search`() {
- val id = 265
- val search = "his wife"
+ val search = "UDP joke"
val joke =
- joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true)
+ joke(contains = search, categories = setOf(Category.PROGRAMMING), safe = true)
logger.fine(joke.toString())
assertThat(joke, "joke($search)").all {
- prop(Joke::id).isEqualTo(id)
+ prop(Joke::id).isEqualTo(0)
prop(Joke::joke).any {
it.contains(search)
}
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
index 1ab8b60..ea49211 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
@@ -1,7 +1,7 @@
/*
* GetJokesTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,15 +33,7 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
-import assertk.assertions.contains
-import assertk.assertions.each
-import assertk.assertions.index
-import assertk.assertions.isEqualTo
-import assertk.assertions.isGreaterThanOrEqualTo
-import assertk.assertions.isNotNull
-import assertk.assertions.isTrue
-import assertk.assertions.prop
-import assertk.assertions.size
+import assertk.assertions.*
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import org.junit.jupiter.api.Test
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
index 7bcf1c6..aa85337 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
@@ -1,7 +1,7 @@
/*
* GetRawJokesTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,47 +33,60 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
-import assertk.assertions.doesNotContain
-import assertk.assertions.isNotEmpty
-import assertk.assertions.startsWith
+import assertk.assertions.*
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
+import net.thauvin.erik.jokeapi.models.JokeResponse
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import kotlin.test.assertContains
@ExtendWith(BeforeAllTests::class)
internal class GetRawJokesTest {
@Test
fun `Get Raw Joke with TXT`() {
val response = rawJokes(format = Format.TXT)
- assertThat(response, "rawJoke(txt)").all {
- isNotEmpty()
- doesNotContain("Error")
+ assertThat(response).all {
+ prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
+ prop("data", JokeResponse::data).all {
+ isNotEmpty()
+ doesNotContain("Error")
+ }
}
}
@Test
fun `Get Raw Joke with XML`() {
val response = rawJokes(format = Format.XML)
- assertThat(response, "rawJoke(xml)").startsWith("\n\n false")
+ assertThat(response).all {
+ prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
+ prop("data", JokeResponse::data).startsWith("\n\n false")
+ }
}
@Test
fun `Get Raw Joke with YAML`() {
val response = rawJokes(format = Format.YAML)
- assertThat(response, "rawJoke(yaml)").startsWith("error: false")
+ assertThat(response).all {
+ prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
+ prop("data", JokeResponse::data).startsWith("error: false")
+ }
}
@Test
fun `Get Raw Jokes`() {
val response = rawJokes(amount = 2)
- assertContains(response, "\"amount\": 2", false, "rawJoke(2)")
+ assertThat(response).all {
+ prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
+ prop("data", JokeResponse::data).isNotEmpty()
+ }
}
@Test
fun `Get Raw Invalid Jokes`() {
val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
- assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
+ assertThat(response).all {
+ prop("statusCode", JokeResponse::statusCode).isEqualTo(400)
+ prop("data", JokeResponse::data).contains("\"error\": true")
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
index 92de2e0..a4d4e0c 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
@@ -1,7 +1,7 @@
/*
* JokeConfigTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,25 +33,12 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
-import assertk.assertions.each
-import assertk.assertions.isBetween
-import assertk.assertions.isEmpty
-import assertk.assertions.isEqualTo
-import assertk.assertions.isGreaterThanOrEqualTo
-import assertk.assertions.isTrue
-import assertk.assertions.prop
-import assertk.assertions.size
+import assertk.assertions.*
+import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.joke
import net.thauvin.erik.jokeapi.JokeApi.jokes
-import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.logger
-import net.thauvin.erik.jokeapi.models.Category
-import net.thauvin.erik.jokeapi.models.Flag
-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.Type
+import net.thauvin.erik.jokeapi.models.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains
@@ -115,8 +102,9 @@ internal class JokeConfigTest {
amount(2)
safe(true)
}.build()
- val joke = getRawJokes(config)
- assertContains(joke, "----------------------------------------------", false, "config.amount(2)")
+ val jokes = getRawJokes(config)
+ assertThat(jokes.statusCode).isEqualTo(200)
+ assertContains(jokes.data, "----------------------------------------------", false, "config.amount(2)")
}
@Test
@@ -167,8 +155,8 @@ internal class JokeConfigTest {
}.build()
assertThat(config, "config").all {
prop(JokeConfig::categories).isEqualTo(categories)
- prop(JokeConfig::language).isEqualTo(language)
- prop(JokeConfig::flags).isEqualTo(flags)
+ prop(JokeConfig::lang).isEqualTo(language)
+ prop(JokeConfig::blacklistFlags).isEqualTo(flags)
prop(JokeConfig::type).isEqualTo(type)
prop(JokeConfig::format).isEqualTo(format)
prop(JokeConfig::contains).isEqualTo(search)
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt
index 8f8d936..d50b97a 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeUtilTest.kt
@@ -1,7 +1,7 @@
/*
- * UtilTest.kt
+ * JokeUtilTest.kt
*
- * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
+ * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -33,6 +33,7 @@ package net.thauvin.erik.jokeapi
import assertk.assertThat
import assertk.assertions.contains
+import assertk.assertions.isEqualTo
import org.json.JSONException
import org.json.JSONObject
import org.junit.jupiter.api.Test
@@ -54,7 +55,8 @@ internal class JokeUtilTest {
@Test
fun `Validate Authentication Header`() {
val token = "AUTH-TOKEN"
- val body = fetchUrl("https://postman-echo.com/get", token)
- assertThat(body, "body").contains("\"authentication\": \"$token\"")
+ val response = fetchUrl("https://postman-echo.com/get", token)
+ assertThat(response.statusCode).isEqualTo(200)
+ assertThat(response.data, "body").contains("\"authentication\": \"$token\"")
}
}