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 @@ [![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause) -[![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-7f52ff)](https://kotlinlang.org/) -[![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/) +[![Kotlin](https://img.shields.io/badge/kotlin-2.1.20-7f52ff)](https://kotlinlang.org/) +[![bld](https://img.shields.io/badge/2.2.1-FA9052?label=bld&labelColor=2392FF)](https://rife2.com/bld) [![Release](https://img.shields.io/github/release/ethauvin/jokeapi.svg)](https://github.com/ethauvin/jokeapi/releases/latest) [![Maven Central](https://img.shields.io/maven-central/v/net.thauvin.erik/jokeapi?color=blue)](https://central.sonatype.com/artifact/net.thauvin.erik/jokeapi) +[![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_jokeapi&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_jokeapi) -[![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml) +[![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml) [![CircleCI](https://circleci.com/gh/ethauvin/jokeapi/tree/master.svg?style=shield)](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\"") } }