Compare commits

..

No commits in common. "master" and "0.9.1" have entirely different histories.

42 changed files with 283 additions and 730 deletions

View file

@ -1,8 +1,4 @@
version: 2.1 version: 2
orbs:
sdkman: joshdholtz/sdkman@0.2.0
defaults: &defaults defaults: &defaults
working_directory: ~/repo working_directory: ~/repo
environment: environment:
@ -10,31 +6,18 @@ defaults: &defaults
TERM: dumb TERM: dumb
CI_NAME: "CircleCI" CI_NAME: "CircleCI"
commands: defaults_gradle: &defaults_bld
build_and_test: steps:
parameters: - checkout
reports-dir: - run:
type: string name: Download the bld dependencies
default: "build/reports/test_results" command: ./bld download
steps: - run:
- checkout name: Compile source with bld
- sdkman/setup-sdkman command: ./bld compile
- sdkman/sdkman-install: - run:
candidate: kotlin name: Run tests with bld
version: 2.0.20 command: ./bld test
- 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
jobs: jobs:
bld_jdk17: bld_jdk17:
@ -43,8 +26,7 @@ jobs:
docker: docker:
- image: cimg/openjdk:17.0 - image: cimg/openjdk:17.0
steps: <<: *defaults_bld
- build_and_test
bld_jdk20: bld_jdk20:
<<: *defaults <<: *defaults
@ -52,10 +34,10 @@ jobs:
docker: docker:
- image: cimg/openjdk:20.0 - image: cimg/openjdk:20.0
steps: <<: *defaults_bld
- build_and_test
workflows: workflows:
version: 2
bld: bld:
jobs: jobs:
- bld_jdk17 - bld_jdk17

View file

@ -2,50 +2,48 @@ name: bld-ci
on: [ push, pull_request, workflow_dispatch ] on: [ push, pull_request, workflow_dispatch ]
env:
COVERAGE_JDK: "21"
COVERAGE_KOTLIN: "2.1.20"
jobs: jobs:
build-bld-project: build-bld-project:
runs-on: ubuntu-latest
env:
COVERAGE_SDK: "17"
strategy: strategy:
matrix: matrix:
java-version: [ 17, 21, 24 ] java-version: [ 17, 20 ]
kotlin-version: [ 1.9.25, 2.0.21, 2.1.20 ]
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout source repository - name: Checkout source repository
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }} - name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: ${{ matrix.java-version }} java-version: ${{ matrix.java-version }}
- name: Download dependencies - name: Grant bld execute permission
run: chmod +x bld
- name: Download the bld dependencies
run: ./bld download run: ./bld download
- name: Compile source - name: Compile source with bld
run: ./bld compile run: ./bld compile
- name: Run tests - name: Run tests with bld
run: ./bld jacoco run: ./bld jacoco
- name: Remove pom.xml - name: Remove pom.xml
if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN if: success() && matrix.java-version == env.COVERAGE_SDK
&& matrix.os == 'ubuntu-latest'
run: rm -rf pom.xml run: rm -rf pom.xml
- name: SonarCloud Scan - name: SonarCloud Scan
uses: sonarsource/sonarcloud-github-action@master uses: sonarsource/sonarcloud-github-action@master
if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN if: success() && matrix.java-version == env.COVERAGE_SDK
&& matrix.os == 'ubuntu-latest'
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

6
.idea/bld.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BldConfiguration">
<events />
</component>
</project>

View file

@ -1,204 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaDocConfiguration">
<GENERAL>
<MODE>UPDATE</MODE>
<OVERRIDDEN_METHODS>false</OVERRIDDEN_METHODS>
<SPLITTED_CLASS_NAME>true</SPLITTED_CLASS_NAME>
<LEVELS>
<LEVEL>TYPE</LEVEL>
<LEVEL>METHOD</LEVEL>
<LEVEL>FIELD</LEVEL>
</LEVELS>
<VISIBILITIES>
<VISIBILITY>DEFAULT</VISIBILITY>
<VISIBILITY>PROTECTED</VISIBILITY>
<VISIBILITY>PUBLIC</VISIBILITY>
</VISIBILITIES>
</GENERAL>
<TEMPLATES>
<CLASSES>
<CLASS>
<KEY>^.*(public|protected|private)*.+interface\s+\w+.*</KEY>
<VALUE>/**\n
* The interface ${name}.\n
&lt;#if element.typeParameters?has_content&gt; * \n
&lt;/#if&gt;
&lt;#list element.typeParameters as typeParameter&gt;
* @param &lt;${typeParameter.name}&gt; the type parameter\n
&lt;/#list&gt;
*/</VALUE>
</CLASS>
<CLASS>
<KEY>^.*(public|protected|private)*.+enum\s+\w+.*</KEY>
<VALUE>/**\n
* The enum ${name}.\n
*/</VALUE>
</CLASS>
<CLASS>
<KEY>^.*(public|protected|private)*.+class\s+\w+.*</KEY>
<VALUE>/**\n
* The type ${name}.\n
&lt;#if element.typeParameters?has_content&gt; * \n
&lt;/#if&gt;
&lt;#list element.typeParameters as typeParameter&gt;
* @param &lt;${typeParameter.name}&gt; the type parameter\n
&lt;/#list&gt;
*/</VALUE>
</CLASS>
<CLASS>
<KEY>.+</KEY>
<VALUE>/**\n
* The type ${name}.\n
*/</VALUE>
</CLASS>
</CLASSES>
<CONSTRUCTORS>
<CONSTRUCTOR>
<KEY>.+</KEY>
<VALUE>/**\n
* Instantiates a new ${name}.\n
&lt;#if element.parameterList.parameters?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.parameterList.parameters as parameter&gt;
* @param ${parameter.name} the ${paramNames[parameter.name]}\n
&lt;/#list&gt;
&lt;#if element.throwsList.referenceElements?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.throwsList.referenceElements as exception&gt;
* @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
&lt;/#list&gt;
*/</VALUE>
</CONSTRUCTOR>
</CONSTRUCTORS>
<METHODS>
<METHOD>
<KEY>^.*(public|protected|private)*\s*.*(\w(\s*&lt;.+&gt;)*)+\s+get\w+\s*\(.*\).+</KEY>
<VALUE>/**\n
* Gets ${partName}.\n
&lt;#if element.typeParameters?has_content&gt; * \n
&lt;/#if&gt;
&lt;#list element.typeParameters as typeParameter&gt;
* @param &lt;${typeParameter.name}&gt; the type parameter\n
&lt;/#list&gt;
&lt;#if element.parameterList.parameters?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.parameterList.parameters as parameter&gt;
* @param ${parameter.name} the ${paramNames[parameter.name]}\n
&lt;/#list&gt;
&lt;#if isNotVoid&gt;
*\n
* @return the ${partName}\n
&lt;/#if&gt;
&lt;#if element.throwsList.referenceElements?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.throwsList.referenceElements as exception&gt;
* @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
&lt;/#list&gt;
*/</VALUE>
</METHOD>
<METHOD>
<KEY>^.*(public|protected|private)*\s*.*(void|\w(\s*&lt;.+&gt;)*)+\s+set\w+\s*\(.*\).+</KEY>
<VALUE>/**\n
* Sets ${partName}.\n
&lt;#if element.typeParameters?has_content&gt; * \n
&lt;/#if&gt;
&lt;#list element.typeParameters as typeParameter&gt;
* @param &lt;${typeParameter.name}&gt; the type parameter\n
&lt;/#list&gt;
&lt;#if element.parameterList.parameters?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.parameterList.parameters as parameter&gt;
* @param ${parameter.name} the ${paramNames[parameter.name]}\n
&lt;/#list&gt;
&lt;#if isNotVoid&gt;
*\n
* @return the ${partName}\n
&lt;/#if&gt;
&lt;#if element.throwsList.referenceElements?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.throwsList.referenceElements as exception&gt;
* @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
&lt;/#list&gt;
*/</VALUE>
</METHOD>
<METHOD>
<KEY>^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+</KEY>
<VALUE>/**\n
* The entry point of application.\n
&lt;#if element.parameterList.parameters?has_content&gt;
*\n
&lt;/#if&gt;
* @param ${element.parameterList.parameters[0].name} the input arguments\n
&lt;#if element.throwsList.referenceElements?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.throwsList.referenceElements as exception&gt;
* @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
&lt;/#list&gt;
*/</VALUE>
</METHOD>
<METHOD>
<KEY>.+</KEY>
<VALUE>/**\n
* ${name}&lt;#if isNotVoid&gt; ${return}&lt;/#if&gt;.\n
&lt;#if element.typeParameters?has_content&gt; * \n
&lt;/#if&gt;
&lt;#list element.typeParameters as typeParameter&gt;
* @param &lt;${typeParameter.name}&gt; the type parameter\n
&lt;/#list&gt;
&lt;#if element.parameterList.parameters?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.parameterList.parameters as parameter&gt;
* @param ${parameter.name} the ${paramNames[parameter.name]}\n
&lt;/#list&gt;
&lt;#if isNotVoid&gt;
*\n
* @return the ${return}\n
&lt;/#if&gt;
&lt;#if element.throwsList.referenceElements?has_content&gt;
*\n
&lt;/#if&gt;
&lt;#list element.throwsList.referenceElements as exception&gt;
* @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n
&lt;/#list&gt;
*/</VALUE>
</METHOD>
</METHODS>
<FIELDS>
<FIELD>
<KEY>^.*(public|protected|private)*.+static.*(\w\s\w)+.+</KEY>
<VALUE>/**\n
* The constant ${element.getName()}.\n
*/</VALUE>
</FIELD>
<FIELD>
<KEY>^.*(public|protected|private)*.*(\w\s\w)+.+</KEY>
<VALUE>/**\n
&lt;#if element.parent.isInterface()&gt;
* The constant ${element.getName()}.\n
&lt;#else&gt;
* The ${name}.\n
&lt;/#if&gt; */</VALUE>
</FIELD>
<FIELD>
<KEY>.+</KEY>
<VALUE>/**\n
&lt;#if element.parent.isEnum()&gt;
*${name} ${typeName}.\n
&lt;#else&gt;
* The ${name}.\n
&lt;/#if&gt;*/</VALUE>
</FIELD>
</FIELDS>
</TEMPLATES>
</component>
</project>

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" /> <option name="version" value="1.9.10" />
</component> </component>
</project> </project>

View file

@ -2,12 +2,12 @@
<library name="bld"> <library name="bld">
<CLASSES> <CLASSES>
<root url="file://$PROJECT_DIR$/lib/bld" /> <root url="file://$PROJECT_DIR$/lib/bld" />
<root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1.jar!/" /> <root url="jar://$USER_HOME$/.bld/dist/bld-1.7.5.jar!/" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES> <SOURCES>
<root url="file://$PROJECT_DIR$/lib/bld" /> <root url="file://$PROJECT_DIR$/lib/bld" />
<root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1-sources.jar!/" /> <root url="jar://$USER_HOME$/.bld/dist/bld-1.7.5-sources.jar!/" />
</SOURCES> </SOURCES>
<excluded> <excluded>
<root url="jar://$PROJECT_DIR$/lib/bld/bld-wrapper.jar!/" /> <root url="jar://$PROJECT_DIR$/lib/bld/bld-wrapper.jar!/" />

View file

@ -7,7 +7,7 @@
<SOURCES> <SOURCES>
<root url="file://$PROJECT_DIR$/lib/compile" /> <root url="file://$PROJECT_DIR$/lib/compile" />
</SOURCES> </SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" /> <jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="false" />
<jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="false" type="SOURCES" />
</library> </library>
</component> </component>

View file

@ -8,7 +8,7 @@
<SOURCES> <SOURCES>
<root url="file://$PROJECT_DIR$/lib/runtime" /> <root url="file://$PROJECT_DIR$/lib/runtime" />
</SOURCES> </SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" /> <jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="false" />
<jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="false" type="SOURCES" />
</library> </library>
</component> </component>

View file

@ -8,7 +8,7 @@
<SOURCES> <SOURCES>
<root url="file://$PROJECT_DIR$/lib/test" /> <root url="file://$PROJECT_DIR$/lib/test" />
</SOURCES> </SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" /> <jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="false" />
<jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="false" type="SOURCES" />
</library> </library>
</component> </component>

26
.idea/misc.xml generated
View file

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <component name="EntryPointsManager">
<entry_points version="2.0">
<entry_point TYPE="field" FQNAME="net.thauvin.erik.jokeapi.models.Category SPOOKY" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.jokeapi.JokeApi logger$delegate" />
</entry_points>
<pattern value="net.thauvin.erik.JokeApiBuild" method="jacoco" /> <pattern value="net.thauvin.erik.JokeApiBuild" method="jacoco" />
<pattern value="net.thauvin.erik.JokeApiBuild" method="detekt" /> </component>
<pattern value="net.thauvin.erik.JokeApiBuild" method="detektBaseline" /> <component name="PDMPlugin">
<pattern value="net.thauvin.erik.jokeapi.models.Category" /> <option name="customRuleSets">
<pattern value="net.thauvin.erik.jokeapi.models.Category" method="Category" /> <list>
<pattern value="net.thauvin.erik.jokeapi.models.Flag" method="Flag" /> <option value="K:\java\semver\config\pmd.xml" />
<pattern value="net.thauvin.erik.jokeapi.models.Format" method="Format" /> <option value="$PROJECT_DIR$/../../java/bld-generated-version/config/pmd.xml" />
<pattern value="net.thauvin.erik.jokeapi.models.Language" method="Language" /> <option value="$PROJECT_DIR$/../../java/bld-pitest/config/pmd.xml" />
<pattern value="net.thauvin.erik.jokeapi.models.Type" method="Type" /> <option value="$PROJECT_DIR$/../../java/bld-jacoco-report/config/pmd.xml" />
<option value="$PROJECT_DIR$/../../java/bld-checkstyle/config/pmd.xml" />
<option value="$PROJECT_DIR$/../../java/bld-exec/config/pmd.xml" />
<option value="$PROJECT_DIR$/../../java/bld-testng/config/pmd.xml" />
</list>
</option>
<option name="skipTestSources" value="false" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build" /> <output url="file://$PROJECT_DIR$/build" />

9
.idea/runConfigurations/Run Tests.xml generated Normal file
View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="net.thauvin.erik.JokeapiTest" />
<module name="app" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Run Tests",
"request": "launch",
"mainClass": "net.thauvin.erik.JokeapiTest"
}
]
}

15
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"java.project.sourcePaths": [
"src/main/java",
"src/main/resources",
"src/test/java",
"src/bld/java"
],
"java.configuration.updateBuildConfiguration": "automatic",
"java.project.referencedLibraries": [
"${HOME}/.bld/dist/bld-1.7.5.jar",
"lib/compile/*.jar",
"lib/runtime/*.jar",
"lib/test/*.jar"
]
}

View file

@ -1,4 +1,4 @@
Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View file

@ -1,6 +1,5 @@
[![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) [![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-2.1.20-7f52ff)](https://kotlinlang.org/) [![Kotlin](https://img.shields.io/badge/kotlin-1.9.21-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) [![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) [![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/) [![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/)
@ -16,7 +15,7 @@ A simple library to retrieve jokes from [Sv443's JokeAPI](https://v2.jokeapi.dev
## Examples (TL;DR) ## Examples (TL;DR)
```kotlin ```kotlin
import net.thauvin.erik.jokeapi.joke import net.thauvin.erik.jokeapi.getJoke
val joke = joke() val joke = joke()
val safe = joke(safe = true) val safe = joke(safe = true)
@ -95,10 +94,10 @@ joke.getJoke().forEach(System.out::println);
To use with [bld](https://rife2.com/bld), include the following dependency in your build file: To use with [bld](https://rife2.com/bld), include the following dependency in your build file:
```java ```java
repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY); repositories = List.of(MAVEN_CENTRAL);
scope(compile) scope(compile)
.include(dependency("net.thauvin.erik", "jokeapi", "1.0.0")); .include(dependency("net.thauvin.erik:cryptoprice:1.0.1"));
``` ```
Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project. Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
@ -112,7 +111,7 @@ repositories {
} }
dependencies { dependencies {
implementation("net.thauvin.erik:jokeapi:1.0.0") implementation("net.thauvin.erik:jokeapi:0.9.0")
} }
``` ```
@ -124,10 +123,9 @@ You can also retrieve one or more raw (unprocessed) jokes in all [supported form
For example for YAML: For example for YAML:
```kotlin ```kotlin
var jokes = getRawJokes(format = Format.YAML, idRange = IdRange(22)) var joke = getRawJokes(format = Format.YAML, idRange = IdRange(22))
println(jokes.data) println(joke)
``` ```
```yaml ```yaml
error: false error: false
category: "Programming" category: "Programming"
@ -143,8 +141,8 @@ flags:
id: 22 id: 22
safe: true safe: true
lang: "en" lang: "en"
```
```
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt#L46)... - View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt#L46)...
## Extending ## Extending
@ -154,37 +152,15 @@ 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): For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint):
```kotlin ```kotlin
val response = JokeApi.apiCall( val lang = JokeApi.apiCall(
endPoint = "langcode", endPoint = "langcode",
path = "french", path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value) params = mapOf(Parameter.FORMAT to Format.YAML.value)
) )
if (response.statusCode == 200) { println(lang)
println(response.data)
}
``` ```
```yaml ```yaml
error: false error: false
code: "fr" code: "fr"
``` ```
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt#L48)... - 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.

View file

@ -1,12 +1,12 @@
<?xml version="1.0" ?> <?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>LongParameterList:JokeApi.kt$( amount: Int, categories: Set&lt;Category&gt; = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag&gt; = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID> <ID>LongParameterList:JokeApi.kt$( amount: Int, categories: Set&lt;Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category&gt; = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag&gt; = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID> <ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category&gt; = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag&gt; = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )</ID> <ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )</ID>
<ID>LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set&lt;Category&gt;, val language: Language, val flags: Set&lt;Flag&gt;, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )</ID> <ID>LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set&lt;Category>, val language: Language, val flags: Set&lt;Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )</ID>
<ID>LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String&gt;, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID> <ID>LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID>
<ID>MagicNumber:JokeUtil.kt$200</ID> <ID>MagicNumber:JokeUtil.kt$200</ID>
<ID>MagicNumber:JokeUtil.kt$399</ID> <ID>MagicNumber:JokeUtil.kt$399</ID>
<ID>MagicNumber:JokeUtil.kt$400</ID> <ID>MagicNumber:JokeUtil.kt$400</ID>
@ -22,7 +22,6 @@
<ID>WildcardImport:GetJokeTest.kt$import assertk.assertions.*</ID> <ID>WildcardImport:GetJokeTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:GetJokeTest.kt$import net.thauvin.erik.jokeapi.models.*</ID> <ID>WildcardImport:GetJokeTest.kt$import net.thauvin.erik.jokeapi.models.*</ID>
<ID>WildcardImport:GetJokesTest.kt$import assertk.assertions.*</ID> <ID>WildcardImport:GetJokesTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:GetRawJokesTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:JokeApi.kt$import net.thauvin.erik.jokeapi.models.*</ID> <ID>WildcardImport:JokeApi.kt$import net.thauvin.erik.jokeapi.models.*</ID>
<ID>WildcardImport:JokeConfig.kt$import net.thauvin.erik.jokeapi.models.*</ID> <ID>WildcardImport:JokeConfig.kt$import net.thauvin.erik.jokeapi.models.*</ID>
<ID>WildcardImport:JokeConfigTest.kt$import assertk.assertions.*</ID> <ID>WildcardImport:JokeConfigTest.kt$import assertk.assertions.*</ID>

Binary file not shown.

View file

@ -1,10 +1,9 @@
bld.downloadExtensionJavadoc=false bld.downloadExtensionJavadoc=false
bld.downloadExtensionSources=true bld.downloadExtensionSources=true
bld.downloadLocation= bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.1
bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.10-SNAPSHOT bld.extensions=com.uwyn.rife2:bld-kotlin:0.9.0-SNAPSHOT
bld.extension-dokka=com.uwyn.rife2:bld-dokka:1.0.4-SNAPSHOT bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.0-SNAPSHOT
bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.10
bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT
bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
bld.downloadLocation=
bld.sourceDirectories= bld.sourceDirectories=
bld.version=2.2.1 bld.version=1.7.5

26
pom.xml
View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>net.thauvin.erik</groupId> <groupId>net.thauvin.erik</groupId>
<artifactId>jokeapi</artifactId> <artifactId>jokeapi</artifactId>
<version>1.0.1-SNAPSHOT</version> <version>0.9.1</version>
<name>jokeapi</name> <name>jokeapi</name>
<description>Retrieve jokes from Sv443&apos;s JokeAPI</description> <description>Retrieve jokes from Sv443&apos;s JokeAPI</description>
<url>https://github.com/ethauvin/jokeapi</url> <url>https://github.com/ethauvin/jokeapi</url>
@ -18,19 +18,37 @@
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId> <artifactId>kotlin-stdlib</artifactId>
<version>2.1.20</version> <version>1.9.21</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-common</artifactId>
<version>1.9.21</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>1.9.21</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.9.21</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.json</groupId> <groupId>org.json</groupId>
<artifactId>json</artifactId> <artifactId>json</artifactId>
<version>20250107</version> <version>20231013</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.thauvin.erik.urlencoder</groupId> <groupId>net.thauvin.erik.urlencoder</groupId>
<artifactId>urlencoder-lib-jvm</artifactId> <artifactId>urlencoder-lib-jvm</artifactId>
<version>1.6.0</version> <version>1.4.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View file

@ -1,7 +1,7 @@
/* /*
* JokeApiBuild.java * JokeApiBuild.java
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -35,11 +35,10 @@ import rife.bld.BuildCommand;
import rife.bld.Project; import rife.bld.Project;
import rife.bld.extension.CompileKotlinOperation; import rife.bld.extension.CompileKotlinOperation;
import rife.bld.extension.DetektOperation; import rife.bld.extension.DetektOperation;
import rife.bld.extension.DokkaOperation;
import rife.bld.extension.JacocoReportOperation; import rife.bld.extension.JacocoReportOperation;
import rife.bld.extension.dokka.DokkaOperation;
import rife.bld.extension.dokka.LoggingLevel; import rife.bld.extension.dokka.LoggingLevel;
import rife.bld.extension.dokka.OutputFormat; import rife.bld.extension.dokka.OutputFormat;
import rife.bld.extension.kotlin.CompileOptions;
import rife.bld.operations.exceptions.ExitStatusException; import rife.bld.operations.exceptions.ExitStatusException;
import rife.bld.publish.PomBuilder; import rife.bld.publish.PomBuilder;
import rife.bld.publish.PublishDeveloper; import rife.bld.publish.PublishDeveloper;
@ -50,45 +49,41 @@ import rife.tools.exceptions.FileUtilsErrorException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; 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.Repository.*;
import static rife.bld.dependencies.Scope.compile; import static rife.bld.dependencies.Scope.compile;
import static rife.bld.dependencies.Scope.test; import static rife.bld.dependencies.Scope.test;
public class JokeApiBuild extends Project { public class JokeApiBuild extends Project {
final File srcMainKotlin = new File(srcMainDirectory(), "kotlin");
public JokeApiBuild() { public JokeApiBuild() {
pkg = "net.thauvin.erik"; pkg = "net.thauvin.erik";
name = "jokeapi"; name = "jokeapi";
version = version(1, 0, 1, "SNAPSHOT"); version = version(0, 9, 1);
javaRelease = 11; javaRelease = 11;
downloadSources = true; downloadSources = true;
autoDownloadPurge = true; autoDownloadPurge = true;
repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL); repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL);
final var kotlin = version(2, 1, 20); final var kotlin = version(1, 9, 21);
scope(compile) scope(compile)
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin)) .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
.include(dependency("org.json", "json", "20250107")) .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 6, 0))); .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk7", kotlin))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", kotlin))
.include(dependency("org.json", "json", "20231013"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 4, 0)));
scope(test) scope(test)
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin)) .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", version(1, 9, 21)))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2))) .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 1)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2))) .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 1)))
.include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 2))) .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 27, 0)));
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)));
publishOperation() publishOperation()
.repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location()) .repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location())
.withCredentials(property("sonatype.user"), property("sonatype.password")) .withCredentials(property("sonatype.user"), property("sonatype.password"))
: repository(SONATYPE_RELEASES_LEGACY.location()) : repository(SONATYPE_RELEASES_LEGACY.location())
.withCredentials(property("sonatype.user"), property("sonatype.password"))) .withCredentials(property("sonatype.user"), property("sonatype.password")))
.repository(repository("github"))
.info() .info()
.groupId(pkg) .groupId(pkg)
.artifactId(name) .artifactId(name)
@ -115,29 +110,18 @@ public class JokeApiBuild extends Project {
.signKey(property("sign.key")) .signKey(property("sign.key"))
.signPassphrase(property("sign.passphrase")); .signPassphrase(property("sign.passphrase"));
jarSourcesOperation().sourceDirectories(srcMainKotlin); jarSourcesOperation().sourceDirectories(new File(srcMainDirectory(), "kotlin"));
} }
public static void main(String[] args) { 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); new JokeApiBuild().start(args);
} }
@BuildCommand(summary = "Compiles the Kotlin project") @BuildCommand(summary = "Compiles the Kotlin project")
@Override @Override
public void compile() throws Exception { public void compile() throws IOException {
new CompileKotlinOperation() new CompileKotlinOperation()
.fromProject(this) .fromProject(this)
.compileOptions(new CompileOptions().verbose(true))
.execute(); .execute();
} }
@ -158,10 +142,9 @@ public class JokeApiBuild extends Project {
} }
@BuildCommand(summary = "Generates JaCoCo Reports") @BuildCommand(summary = "Generates JaCoCo Reports")
public void jacoco() throws Exception { public void jacoco() throws IOException {
new JacocoReportOperation() new JacocoReportOperation()
.fromProject(this) .fromProject(this)
.sourceFiles(srcMainKotlin)
.execute(); .execute();
} }
@ -183,12 +166,6 @@ public class JokeApiBuild extends Project {
pomRoot(); pomRoot();
} }
@Override
public void publishLocal() throws Exception {
super.publishLocal();
pomRoot();
}
@BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory") @BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory")
public void pomRoot() throws FileUtilsErrorException { public void pomRoot() throws FileUtilsErrorException {
PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(), PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(),

View file

@ -1,7 +1,7 @@
/* /*
* JokeApi.kt * JokeApi.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -45,16 +45,13 @@ import java.util.stream.Collectors
object JokeApi { object JokeApi {
private const val API_URL = "https://v2.jokeapi.dev/" private const val API_URL = "https://v2.jokeapi.dev/"
/**
* The logger instance.
*/
@JvmStatic @JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) } val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
/** /**
* Makes a direct API call. * Makes a direct API call.
* *
* See the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details. * Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
*/ */
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@ -64,7 +61,7 @@ object JokeApi {
path: String = "", path: String = "",
params: Map<String, String> = emptyMap(), params: Map<String, String> = emptyMap(),
auth: String = "" auth: String = ""
): JokeResponse { ): String {
val urlBuilder = StringBuilder("$API_URL$endPoint") val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
@ -98,11 +95,11 @@ object JokeApi {
*/ */
@JvmStatic @JvmStatic
@Throws(HttpErrorException::class) @Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): JokeResponse { fun getRawJokes(config: JokeConfig): String {
return rawJokes( return rawJokes(
categories = config.categories, categories = config.categories,
lang = config.lang, lang = config.language,
blacklistFlags = config.blacklistFlags, blacklistFlags = config.flags,
type = config.type, type = config.type,
format = config.format, format = config.format,
contains = config.contains, contains = config.contains,
@ -124,8 +121,8 @@ object JokeApi {
fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke { fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return joke( return joke(
categories = config.categories, categories = config.categories,
lang = config.lang, lang = config.language,
blacklistFlags = config.blacklistFlags, blacklistFlags = config.flags,
type = config.type, type = config.type,
contains = config.contains, contains = config.contains,
idRange = config.idRange, idRange = config.idRange,
@ -145,8 +142,8 @@ object JokeApi {
fun jokes(config: JokeConfig): Array<Joke> { fun jokes(config: JokeConfig): Array<Joke> {
return jokes( return jokes(
categories = config.categories, categories = config.categories,
lang = config.lang, lang = config.language,
blacklistFlags = config.blacklistFlags, blacklistFlags = config.flags,
type = config.type, type = config.type,
contains = config.contains, contains = config.contains,
idRange = config.idRange, idRange = config.idRange,
@ -164,32 +161,6 @@ object JokeApi {
* *
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. * 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. * @param splitNewLine Split newline within [Type.SINGLE] joke.
*/ */
fun joke( fun joke(
@ -213,7 +184,7 @@ fun joke(
idRange = idRange, idRange = idRange,
safe = safe, safe = safe,
auth = auth auth = auth
).data )
) )
if (json.getBoolean("error")) { if (json.getBoolean("error")) {
throw parseError(json) throw parseError(json)
@ -227,35 +198,7 @@ fun joke(
* *
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
* *
* @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the * @param amount The required amount of jokes to return.
* 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. * @param splitNewLine Split newline within [Type.SINGLE] joke.
*/ */
fun jokes( fun jokes(
@ -281,7 +224,7 @@ fun jokes(
amount = amount, amount = amount,
safe = safe, safe = safe,
auth = auth auth = auth
).data )
) )
if (json.getBoolean("error")) { if (json.getBoolean("error")) {
throw parseError(json) throw parseError(json)
@ -298,42 +241,8 @@ fun jokes(
/** /**
* Returns one or more jokes. * Returns one or more jokes.
* *
* See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details. * 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 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( fun rawJokes(
categories: Set<Category> = setOf(Category.ANY), categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN, lang: Language = Language.EN,
@ -345,7 +254,7 @@ fun rawJokes(
amount: Int = 1, amount: Int = 1,
safe: Boolean = false, safe: Boolean = false,
auth: String = "" auth: String = ""
): JokeResponse { ): String {
val params = mutableMapOf<String, String>() val params = mutableMapOf<String, String>()
// Categories // Categories

View file

@ -1,7 +1,7 @@
/* /*
* JokeConfig.kt * JokeConfig.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -39,19 +39,19 @@ import net.thauvin.erik.jokeapi.models.*
* *
* Use the [Builder] to create a new configuration. * Use the [Builder] to create a new configuration.
*/ */
class JokeConfig private constructor(builder: Builder) { class JokeConfig private constructor(
val categories = builder.categories val categories: Set<Category>,
val lang = builder.lang val language: Language,
val blacklistFlags = builder.blacklistFlags val flags: Set<Flag>,
val type = builder.type val type: Type,
val format = builder.format val format: Format,
val contains = builder.contains val contains: String,
val idRange = builder.idRange val idRange: IdRange,
val amount = builder.amount val amount: Int,
val safe = builder.safe val safe: Boolean,
val splitNewLine = builder.splitNewLine val splitNewLine: Boolean,
val auth = builder.auth val auth: String
) {
/** /**
* [Builds][build] a new configuration. * [Builds][build] a new configuration.
* *
@ -72,86 +72,20 @@ class JokeConfig private constructor(builder: Builder) {
var splitNewLine: Boolean = false, var splitNewLine: Boolean = false,
var auth: String = "" var auth: String = ""
) { ) {
/** fun categories(categories: Set<Category>) = apply { this.categories = categories }
* JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is fun lang(language: Language) = apply { lang = language }
* about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] fun blacklistFlags(flags: Set<Flag>) = apply { blacklistFlags = flags }
* category, dark humor will be in the [Category.DARK] category and so on. If you want jokes from all fun type(type: Type) = apply { this.type = type }
* categories, you can instead use [Category.ANY], which will make JokeAPI randomly choose a category. fun format(format: Format) = apply { this.format = format }
*/ fun contains(search: String) = apply { contains = search }
fun categories(categories: Set<Category>): Builder = apply { this.categories = categories } 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(
* There are two types of languages; system languages and joke languages. Both are separate from each other. categories, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth
* 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<Flag>): 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)
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* JokeUtil.kt * JokeUtil.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -39,37 +39,30 @@ import net.thauvin.erik.jokeapi.models.*
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URI import java.net.URL
import java.util.logging.Level 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)) { if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(url) JokeApi.logger.fine(url)
} }
val connection = URI(url).toURL().openConnection() as HttpURLConnection val connection = URL(url).openConnection() as HttpURLConnection
try { connection.setRequestProperty(
connection.setRequestProperty( "User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0" )
) if (auth.isNotEmpty()) {
if (auth.isNotEmpty()) { connection.setRequestProperty("Authentication", auth)
connection.setRequestProperty("Authentication", auth) }
}
val isSuccess = connection.responseCode in 200..399 if (connection.responseCode in 200..399) {
val stream = if (isSuccess) connection.inputStream else connection.errorStream val body = connection.inputStream.bufferedReader().use { it.readText() }
val body = stream.bufferedReader().use { it.readText() } if (JokeApi.logger.isLoggable(Level.FINE)) {
if (!isSuccess && (body.isBlank() || connection.contentType.contains("text/html"))) {
throw httpError(connection.responseCode)
} else if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(body) JokeApi.logger.fine(body)
} }
return JokeResponse(connection.responseCode, body) return body
} finally { } else {
connection.disconnect() throw httpError(connection.responseCode)
} }
} }
@ -130,9 +123,6 @@ private fun httpError(responseCode: Int): HttpErrorException {
return httpException return httpException
} }
/**
* Parse Error.
*/
internal fun parseError(json: JSONObject): JokeException { internal fun parseError(json: JSONObject): JokeException {
val causedBy = json.getJSONArray("causedBy") val causedBy = json.getJSONArray("causedBy")
val causes = List<String>(causedBy.length()) { i -> causedBy.getString(i) } val causes = List<String>(causedBy.length()) { i -> causedBy.getString(i) }
@ -146,9 +136,6 @@ internal fun parseError(json: JSONObject): JokeException {
) )
} }
/**
* Parse Joke.
*/
internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke { internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
val jokes = mutableListOf<String>() val jokes = mutableListOf<String>()
if (json.has("setup")) { if (json.has("setup")) {

View file

@ -1,7 +1,7 @@
/* /*
* HttpErrorException.kt * HttpErrorException.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -44,6 +44,7 @@ class HttpErrorException @JvmOverloads constructor(
cause: Throwable? = null cause: Throwable? = null
) : IOException(message, cause) { ) : IOException(message, cause) {
companion object { companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L private const val serialVersionUID = 1L
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* JokeException.kt * JokeException.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* Category.kt * Category.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* Flag.kt * Flag.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* Format.kt * Format.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* IdRange.kt * IdRange.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* Joke.kt * Joke.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,39 +0,0 @@
/*
* 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)

View file

@ -1,7 +1,7 @@
/* /*
* Language.kt * Language.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* Parameter.kt * Parameter.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -34,7 +34,6 @@ package net.thauvin.erik.jokeapi.models
/** /**
* The available [URL Parameters](https://jokeapi.dev/#url-parameters). * The available [URL Parameters](https://jokeapi.dev/#url-parameters).
*/ */
@Suppress("unused")
object Parameter { object Parameter {
const val AMOUNT = "amount" const val AMOUNT = "amount"
const val CONTAINS = "contains" const val CONTAINS = "contains"

View file

@ -1,7 +1,7 @@
/* /*
* Type.kt * Type.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* ApiCallTest.kt * ApiCallTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -32,7 +32,6 @@
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi
import assertk.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan import assertk.assertions.isGreaterThan
import assertk.assertions.startsWith import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.apiCall import net.thauvin.erik.jokeapi.JokeApi.apiCall
@ -52,9 +51,8 @@ internal class ApiCallTest {
fun `Get Flags`() { fun `Get Flags`() {
// See https://v2.jokeapi.dev/#flags-endpoint // See https://v2.jokeapi.dev/#flags-endpoint
val response = apiCall(endPoint = "flags") val response = apiCall(endPoint = "flags")
val json = JSONObject(response.data) val json = JSONObject(response)
assertAll( assertAll("Validate JSON",
"Validate JSON",
{ assertFalse(json.getBoolean("error"), "apiCall(flags).error") }, { assertFalse(json.getBoolean("error"), "apiCall(flags).error") },
{ assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) }, { assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) },
{ assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) }) { assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) })
@ -67,16 +65,14 @@ internal class ApiCallTest {
endPoint = "langcode", path = "french", endPoint = "langcode", path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value) params = mapOf(Parameter.FORMAT to Format.YAML.value)
) )
assertThat(lang.statusCode).isEqualTo(200) assertContains(lang, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
assertContains(lang.data, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
} }
@Test @Test
fun `Get Ping Response`() { fun `Get Ping Response`() {
// See https://v2.jokeapi.dev/#ping-endpoint // See https://v2.jokeapi.dev/#ping-endpoint
val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value)) val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value))
assertThat(ping.statusCode).isEqualTo(200) assertThat(ping, "apiCall(ping, txt)").startsWith("Pong!")
assertThat(ping.data).startsWith("Pong!")
} }
@Test @Test
@ -86,7 +82,6 @@ internal class ApiCallTest {
endPoint = "languages", endPoint = "languages",
params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value) params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value)
) )
assertThat(lang.statusCode).isEqualTo(200) assertThat(lang).startsWith("<?xml version='1.0'?>")
assertThat(lang.data).startsWith("<?xml version='1.0'?>")
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* BeforeAllTests.kt * BeforeAllTests.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* ExceptionsTest.kt * ExceptionsTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -41,6 +41,8 @@ import net.thauvin.erik.jokeapi.models.Category
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
@ExtendWith(BeforeAllTests::class) @ExtendWith(BeforeAllTests::class)
internal class ExceptionsTest { internal class ExceptionsTest {
@ -61,20 +63,19 @@ internal class ExceptionsTest {
} }
} }
@Test @ParameterizedTest
fun `Validate HTTP Exceptions`() { @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
val locs = ArrayList<Pair<String, Int>>() fun `Validate HTTP Exceptions`(code: Int) {
locs.add(Pair("https://apichallenges.herokuapp.com/secret/note", 401)) val e = assertThrows<HttpErrorException> {
locs.add(Pair("https://apichallenges.herokuapp.com/todo", 404)) fetchUrl("https://httpstat.us/$code")
}
for ((url, code) in locs) { assertThat(e, "fetchUrl($code)").all {
val e = assertThrows<HttpErrorException> { prop(HttpErrorException::statusCode).isEqualTo(code)
fetchUrl(url) prop(HttpErrorException::message).isNotNull().isNotEmpty()
} if (code < 600)
assertThat(e, "fetchUrl($code)").all { prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull()
prop(HttpErrorException::statusCode).isEqualTo(code) else
prop(HttpErrorException::message).isNotNull().isNotEmpty() prop(HttpErrorException::cause).isNull()
}
} }
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* GetJokeTest.kt * GetJokeTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -70,12 +70,15 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with ID`() { fun `Get Joke with ID`() {
val id = 201 val id = 172
val joke = joke(idRange = IdRange(id)) val joke = joke(idRange = IdRange(id))
logger.fine(joke.toString()) logger.fine(joke.toString())
assertThat(joke, "joke($id)").all { assertThat(joke, "joke($id)").all {
prop(Joke::flags).contains(Flag.RELIGIOUS); prop(Joke::flags).all {
prop(Joke::id).isEqualTo(id) contains(Flag.EXPLICIT)
contains(Flag.NSFW)
}
prop(Joke::id).isEqualTo(172)
prop(Joke::category).isEqualTo(Category.PUN) prop(Joke::category).isEqualTo(Category.PUN)
} }
} }
@ -134,10 +137,12 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with Split Newline`() { fun `Get Joke with Split Newline`() {
val joke = joke(type = Type.SINGLE, idRange = IdRange(18), splitNewLine = true) val joke = joke(
categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true
)
logger.fine(joke.toString()) logger.fine(joke.toString())
assertThat(joke::joke, "joke(splitNewLine=true)").all { assertThat(joke::joke, "joke(splitNewLine=true)").all {
size().isGreaterThanOrEqualTo(2) size().isEqualTo(2)
each { each {
containsNone("\n") containsNone("\n")
} }
@ -172,12 +177,13 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke using Search`() { fun `Get Joke using Search`() {
val search = "UDP joke" val id = 265
val search = "his wife"
val joke = val joke =
joke(contains = search, categories = setOf(Category.PROGRAMMING), safe = true) joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true)
logger.fine(joke.toString()) logger.fine(joke.toString())
assertThat(joke, "joke($search)").all { assertThat(joke, "joke($search)").all {
prop(Joke::id).isEqualTo(0) prop(Joke::id).isEqualTo(id)
prop(Joke::joke).any { prop(Joke::joke).any {
it.contains(search) it.contains(search)
} }

View file

@ -1,7 +1,7 @@
/* /*
* GetJokesTest.kt * GetJokesTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:

View file

@ -1,7 +1,7 @@
/* /*
* GetRawJokesTest.kt * GetRawJokesTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -33,60 +33,47 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.* 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.Format
import net.thauvin.erik.jokeapi.models.IdRange 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.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains
@ExtendWith(BeforeAllTests::class) @ExtendWith(BeforeAllTests::class)
internal class GetRawJokesTest { internal class GetRawJokesTest {
@Test @Test
fun `Get Raw Joke with TXT`() { fun `Get Raw Joke with TXT`() {
val response = rawJokes(format = Format.TXT) val response = rawJokes(format = Format.TXT)
assertThat(response).all { assertThat(response, "rawJoke(txt)").all {
prop("statusCode", JokeResponse::statusCode).isEqualTo(200) isNotEmpty()
prop("data", JokeResponse::data).all { doesNotContain("Error")
isNotEmpty()
doesNotContain("Error")
}
} }
} }
@Test @Test
fun `Get Raw Joke with XML`() { fun `Get Raw Joke with XML`() {
val response = rawJokes(format = Format.XML) val response = rawJokes(format = Format.XML)
assertThat(response).all { assertThat(response, "rawJoke(xml)").startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>")
prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
prop("data", JokeResponse::data).startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>")
}
} }
@Test @Test
fun `Get Raw Joke with YAML`() { fun `Get Raw Joke with YAML`() {
val response = rawJokes(format = Format.YAML) val response = rawJokes(format = Format.YAML)
assertThat(response).all { assertThat(response, "rawJoke(yaml)").startsWith("error: false")
prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
prop("data", JokeResponse::data).startsWith("error: false")
}
} }
@Test @Test
fun `Get Raw Jokes`() { fun `Get Raw Jokes`() {
val response = rawJokes(amount = 2) val response = rawJokes(amount = 2)
assertThat(response).all { assertContains(response, "\"amount\": 2", false, "rawJoke(2)")
prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
prop("data", JokeResponse::data).isNotEmpty()
}
} }
@Test @Test
fun `Get Raw Invalid Jokes`() { fun `Get Raw Invalid Jokes`() {
val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161)) val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
assertThat(response).all { assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
prop("statusCode", JokeResponse::statusCode).isEqualTo(400)
prop("data", JokeResponse::data).contains("\"error\": true")
}
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* JokeConfigTest.kt * JokeConfigTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -102,9 +102,8 @@ internal class JokeConfigTest {
amount(2) amount(2)
safe(true) safe(true)
}.build() }.build()
val jokes = getRawJokes(config) val joke = getRawJokes(config)
assertThat(jokes.statusCode).isEqualTo(200) assertContains(joke, "----------------------------------------------", false, "config.amount(2)")
assertContains(jokes.data, "----------------------------------------------", false, "config.amount(2)")
} }
@Test @Test
@ -155,8 +154,8 @@ internal class JokeConfigTest {
}.build() }.build()
assertThat(config, "config").all { assertThat(config, "config").all {
prop(JokeConfig::categories).isEqualTo(categories) prop(JokeConfig::categories).isEqualTo(categories)
prop(JokeConfig::lang).isEqualTo(language) prop(JokeConfig::language).isEqualTo(language)
prop(JokeConfig::blacklistFlags).isEqualTo(flags) prop(JokeConfig::flags).isEqualTo(flags)
prop(JokeConfig::type).isEqualTo(type) prop(JokeConfig::type).isEqualTo(type)
prop(JokeConfig::format).isEqualTo(format) prop(JokeConfig::format).isEqualTo(format)
prop(JokeConfig::contains).isEqualTo(search) prop(JokeConfig::contains).isEqualTo(search)

View file

@ -1,7 +1,7 @@
/* /*
* JokeUtilTest.kt * JokeUtilTest.kt
* *
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: * modification, are permitted provided that the following conditions are met:
@ -33,7 +33,6 @@ package net.thauvin.erik.jokeapi
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.isEqualTo
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -55,8 +54,7 @@ internal class JokeUtilTest {
@Test @Test
fun `Validate Authentication Header`() { fun `Validate Authentication Header`() {
val token = "AUTH-TOKEN" val token = "AUTH-TOKEN"
val response = fetchUrl("https://postman-echo.com/get", token) val body = fetchUrl("https://postman-echo.com/get", token)
assertThat(response.statusCode).isEqualTo(200) assertThat(body, "body").contains("\"authentication\": \"$token\"")
assertThat(response.data, "body").contains("\"authentication\": \"$token\"")
} }
} }