Compare commits

...

52 commits

Author SHA1 Message Date
ad99c4c838
Fix typo 2025-03-26 12:27:19 -07:00
04e636feb4
Add OS matrix for Ubuntu, Windows and macOS 2025-03-26 12:20:35 -07:00
af753c7ac3
Add extensions logging 2025-03-26 12:20:03 -07:00
708550ad5c
Update to latest snapshot 2025-03-26 12:19:41 -07:00
a2ab48e67c
Combine Kotlin compile options 2025-03-20 20:42:09 -07:00
7df1c20e82
Replace deprecated URL(url) with URI(url) 2025-03-20 09:21:50 -07:00
f737077b1d
Update root POM 2025-03-20 09:19:14 -07:00
40568463dd
Add Kotlin compile options for JDK 24 2025-03-20 09:19:03 -07:00
b2163c7ee7
1.0.1-SNAPSHOT 2025-03-20 08:53:29 -07:00
7ba1b0ecf6
Bump Kotlin to version 2.1.20 2025-03-20 08:53:06 -07:00
d39542478f
JDK 24 2025-03-18 23:51:16 -07:00
e211159757
Add Junit Platform Launcher dependency 2025-03-18 04:24:15 -07:00
f16d54fd53
Fix yaml example 2025-03-16 20:43:54 -07:00
4b4f778490
Version 1.0.0 2025-03-16 18:03:39 -07:00
8978b56750
Fix fetchUrl handling of HTTP errors with html content types 2025-03-16 16:54:21 -07:00
4cc92e956f
Update root pom 2025-03-16 15:37:59 -07:00
b17a4fd588
Cleanup tests 2025-03-16 15:32:14 -07:00
944fbf1228
Bump JUnit to version 5.12.1 2025-03-16 13:17:08 -07:00
427dc1a248
Bump org.json to version 20250107 2025-03-16 13:16:42 -07:00
256903cec1
Update extensions dependencies
Bump Detekt extension to version 0.9.9
Bump Dokka extension to version 1.0.3
Bump Jacoco Report extension to version 0.9.9
Bump Kotlin extension to version 1.0.4
2025-03-16 13:15:40 -07:00
41e0d866e2
Bump Kotlin to version 2.1.10 2025-03-16 13:13:36 -07:00
3afbfb26ed
Bump bld to version 2.2.1 2025-03-16 13:12:40 -07:00
ccfe3ac4db
Updated copyright 2025-01-01 16:51:39 -08:00
b0631046b9
Bumped Json to version 20241224 2025-01-01 16:50:07 -08:00
97e0479ffb
Renamed JsonResource code to statusCode 2024-12-26 11:16:24 -08:00
f0ccff6529
1.0.0-SNAPSHOT 2024-12-25 18:20:31 -08:00
b95e1541fb
Added JokeResponse data class 2024-12-25 18:04:40 -08:00
5ac8f910b5
Only throw an HTTP error if the response body is empty 2024-12-22 20:33:44 -08:00
37517a2845
Updated dependencies
Bumped Kotlin to version 2.1.0
Bumped Kotlin extension to version 1.0.3
Bumped Dokka extension to version 1.0.2
Bumped JUnit to 5.11.4
Bumped UrlEncoder to version 1.6.0
2024-12-22 20:27:53 -08:00
76f78a8688
Made sure the connection is disconnected 2024-12-22 15:01:13 -08:00
137201c3e7
Updated copyright 2024-09-21 13:10:08 -07:00
6bd1aed18f
Updated dependencies
Bumped Kotlin to version 2.0.20
Bumped Junit to verson 5.11.0
2024-09-11 01:07:15 -07:00
08780a5c73
Bumped bld to version 2.1.0 2024-09-11 01:06:11 -07:00
f653a0be64
Cleaned up CI workflows 2024-07-15 22:27:24 -07:00
da0af8552e
Updated various SNAPSHOT dependencies 2024-07-12 15:02:18 -07:00
3d684d86c6
Bumped Kotlin extension to version 0.9.8 2024-05-27 18:07:41 -07:00
a7db176830
Make config constructor private 2024-05-26 00:27:45 -07:00
e6bfcdad80
Reworked config builder.
Added parameters' documentation.
2024-05-25 20:15:32 -07:00
8332db1a80
Bumped Detekt extension to version 0.9.4 2024-05-25 13:18:32 -07:00
e925f7a1c2
Bumped Kotlin to version 2.0.0 2024-05-23 22:12:13 -07:00
e81c6a0492
Bumped AssertK to version 0.28.1 2024-05-10 11:12:41 -07:00
e8deffa5ef
Bumped UrlEncoder to version 1.5.0 2024-05-10 11:08:20 -07:00
ab15abe11f
Bumped org.json to version 20240303 2024-05-10 11:06:51 -07:00
e3c7146d06
Bumped JaCoCo extension to version 0.9.5 2024-05-10 11:05:57 -07:00
185836c91a
Bumped Kotlin to version 1.9.24 2024-05-10 11:05:16 -07:00
7fecaa8f82
Bumped bld to version 1.9.1 2024-05-10 11:02:57 -07:00
ca04776f8b
Bumped workflow actions to latest versions 2024-05-10 11:01:13 -07:00
cb0c2efd4f Bumped bld to 1.9.0 2024-02-26 17:41:33 -08:00
cdeb91c7d4 Added contributing section 2024-01-31 18:05:53 -08:00
1a3c7fbac2 Minor cleanups 2024-01-31 18:05:32 -08:00
89b51307b3 Bumped bld to 1.8.0 2024-01-31 15:50:19 -08:00
2b4a7e5534 Updated version in README 2023-11-26 00:37:31 -08:00
42 changed files with 730 additions and 283 deletions

View file

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

View file

@ -2,48 +2,50 @@ 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, 20 ] 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: steps:
- name: Checkout source repository - name: Checkout source repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK ${{ matrix.java-version }} - name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }}
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'zulu' distribution: "zulu"
java-version: ${{ matrix.java-version }} java-version: ${{ matrix.java-version }}
- name: Grant bld execute permission - name: Download dependencies
run: chmod +x bld
- name: Download the bld dependencies
run: ./bld download run: ./bld download
- name: Compile source with bld - name: Compile source
run: ./bld compile run: ./bld compile
- name: Run tests with bld - name: Run tests
run: ./bld jacoco run: ./bld jacoco
- name: Remove pom.xml - name: Remove pom.xml
if: success() && matrix.java-version == env.COVERAGE_SDK if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
&& 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_SDK if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
&& 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 Normal file
View file

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

204
.idea/intellij-javadocs-4.0.1.xml generated Normal file
View file

@ -0,0 +1,204 @@
<?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>

4
.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="1.9.10" /> <option name="version" value="2.1.0" />
</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-1.7.5.jar!/" /> <root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1.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-1.7.5-sources.jar!/" /> <root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1-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="false" /> <jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="false" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" 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="false" /> <jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="false" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" 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="false" /> <jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="false" type="SOURCES" /> <jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" type="SOURCES" />
</library> </library>
</component> </component>

26
.idea/misc.xml generated
View file

@ -1,21 +1,19 @@
<?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" />
</component> <pattern value="net.thauvin.erik.JokeApiBuild" method="detekt" />
<component name="PDMPlugin"> <pattern value="net.thauvin.erik.JokeApiBuild" method="detektBaseline" />
<option name="customRuleSets"> <pattern value="net.thauvin.erik.jokeapi.models.Category" />
<list> <pattern value="net.thauvin.erik.jokeapi.models.Category" method="Category" />
<option value="K:\java\semver\config\pmd.xml" /> <pattern value="net.thauvin.erik.jokeapi.models.Flag" method="Flag" />
<option value="$PROJECT_DIR$/../../java/bld-generated-version/config/pmd.xml" /> <pattern value="net.thauvin.erik.jokeapi.models.Format" method="Format" />
<option value="$PROJECT_DIR$/../../java/bld-pitest/config/pmd.xml" /> <pattern value="net.thauvin.erik.jokeapi.models.Language" method="Language" />
<option value="$PROJECT_DIR$/../../java/bld-jacoco-report/config/pmd.xml" /> <pattern value="net.thauvin.erik.jokeapi.models.Type" method="Type" />
<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" />

View file

@ -1,9 +0,0 @@
<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
View file

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

15
.vscode/settings.json vendored
View file

@ -1,15 +0,0 @@
{
"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-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 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,5 +1,6 @@
[![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-1.9.21-7f52ff)](https://kotlinlang.org/) [![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) [![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/)
@ -15,7 +16,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.getJoke import net.thauvin.erik.jokeapi.joke
val joke = joke() val joke = joke()
val safe = joke(safe = true) val safe = joke(safe = true)
@ -94,10 +95,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); repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
scope(compile) scope(compile)
.include(dependency("net.thauvin.erik:cryptoprice:1.0.1")); .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. Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
@ -111,7 +112,7 @@ repositories {
} }
dependencies { dependencies {
implementation("net.thauvin.erik:jokeapi:0.9.0") implementation("net.thauvin.erik:jokeapi:1.0.0")
} }
``` ```
@ -123,9 +124,10 @@ 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 joke = getRawJokes(format = Format.YAML, idRange = IdRange(22)) var jokes = getRawJokes(format = Format.YAML, idRange = IdRange(22))
println(joke) println(jokes.data)
``` ```
```yaml ```yaml
error: false error: false
category: "Programming" category: "Programming"
@ -141,8 +143,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
@ -152,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): For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint):
```kotlin ```kotlin
val lang = JokeApi.apiCall( val response = 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)
) )
println(lang) if (response.statusCode == 200) {
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' encoding='UTF-8'?> <?xml version="1.0" ?>
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues> <CurrentIssues>
<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$( 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$( 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, 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&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: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: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: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>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>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,6 +22,7 @@
<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,9 +1,10 @@
bld.downloadExtensionJavadoc=false bld.downloadExtensionJavadoc=false
bld.downloadExtensionSources=true bld.downloadExtensionSources=true
bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.1
bld.extensions=com.uwyn.rife2:bld-kotlin:0.9.0-SNAPSHOT
bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.0-SNAPSHOT
bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
bld.downloadLocation= 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.sourceDirectories=
bld.version=1.7.5 bld.version=2.2.1

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>0.9.1</version> <version>1.0.1-SNAPSHOT</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,37 +18,19 @@
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId> <artifactId>kotlin-stdlib</artifactId>
<version>1.9.21</version> <version>2.1.20</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>20231013</version> <version>20250107</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.4.0</version> <version>1.6.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View file

@ -1,7 +1,7 @@
/* /*
* JokeApiBuild.java * JokeApiBuild.java
* *
* 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 * 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,10 +35,11 @@ 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;
@ -49,41 +50,45 @@ 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(0, 9, 1); version = version(1, 0, 1, "SNAPSHOT");
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(1, 9, 21); final var kotlin = version(2, 1, 20);
scope(compile) scope(compile)
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin)) .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin)) .include(dependency("org.json", "json", "20250107"))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk7", kotlin)) .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 6, 0)));
.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", version(1, 9, 21))) .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 1))) .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 1))) .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1)))
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 27, 0))); .include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 1)))
.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)
@ -110,18 +115,29 @@ public class JokeApiBuild extends Project {
.signKey(property("sign.key")) .signKey(property("sign.key"))
.signPassphrase(property("sign.passphrase")); .signPassphrase(property("sign.passphrase"));
jarSourcesOperation().sourceDirectories(new File(srcMainDirectory(), "kotlin")); jarSourcesOperation().sourceDirectories(srcMainKotlin);
} }
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 IOException { public void compile() throws Exception {
new CompileKotlinOperation() new CompileKotlinOperation()
.fromProject(this) .fromProject(this)
.compileOptions(new CompileOptions().verbose(true))
.execute(); .execute();
} }
@ -142,9 +158,10 @@ public class JokeApiBuild extends Project {
} }
@BuildCommand(summary = "Generates JaCoCo Reports") @BuildCommand(summary = "Generates JaCoCo Reports")
public void jacoco() throws IOException { public void jacoco() throws Exception {
new JacocoReportOperation() new JacocoReportOperation()
.fromProject(this) .fromProject(this)
.sourceFiles(srcMainKotlin)
.execute(); .execute();
} }
@ -166,6 +183,12 @@ 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-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 * 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,13 +45,16 @@ 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.
* *
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details. * See the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
*/ */
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@ -61,7 +64,7 @@ object JokeApi {
path: String = "", path: String = "",
params: Map<String, String> = emptyMap(), params: Map<String, String> = emptyMap(),
auth: String = "" auth: String = ""
): String { ): JokeResponse {
val urlBuilder = StringBuilder("$API_URL$endPoint") val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
@ -95,11 +98,11 @@ object JokeApi {
*/ */
@JvmStatic @JvmStatic
@Throws(HttpErrorException::class) @Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): String { fun getRawJokes(config: JokeConfig): JokeResponse {
return rawJokes( return rawJokes(
categories = config.categories, categories = config.categories,
lang = config.language, lang = config.lang,
blacklistFlags = config.flags, blacklistFlags = config.blacklistFlags,
type = config.type, type = config.type,
format = config.format, format = config.format,
contains = config.contains, contains = config.contains,
@ -121,8 +124,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.language, lang = config.lang,
blacklistFlags = config.flags, blacklistFlags = config.blacklistFlags,
type = config.type, type = config.type,
contains = config.contains, contains = config.contains,
idRange = config.idRange, idRange = config.idRange,
@ -142,8 +145,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.language, lang = config.lang,
blacklistFlags = config.flags, blacklistFlags = config.blacklistFlags,
type = config.type, type = config.type,
contains = config.contains, contains = config.contains,
idRange = config.idRange, idRange = config.idRange,
@ -161,6 +164,32 @@ 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(
@ -184,7 +213,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)
@ -198,7 +227,35 @@ 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 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. * @param splitNewLine Split newline within [Type.SINGLE] joke.
*/ */
fun jokes( fun jokes(
@ -224,7 +281,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)
@ -241,8 +298,42 @@ fun jokes(
/** /**
* Returns one or more 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( fun rawJokes(
categories: Set<Category> = setOf(Category.ANY), categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN, lang: Language = Language.EN,
@ -254,7 +345,7 @@ fun rawJokes(
amount: Int = 1, amount: Int = 1,
safe: Boolean = false, safe: Boolean = false,
auth: String = "" auth: String = ""
): String { ): JokeResponse {
val params = mutableMapOf<String, String>() val params = mutableMapOf<String, String>()
// Categories // Categories

View file

@ -1,7 +1,7 @@
/* /*
* JokeConfig.kt * 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 * 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( class JokeConfig private constructor(builder: Builder) {
val categories: Set<Category>, val categories = builder.categories
val language: Language, val lang = builder.lang
val flags: Set<Flag>, val blacklistFlags = builder.blacklistFlags
val type: Type, val type = builder.type
val format: Format, val format = builder.format
val contains: String, val contains = builder.contains
val idRange: IdRange, val idRange = builder.idRange
val amount: Int, val amount = builder.amount
val safe: Boolean, val safe = builder.safe
val splitNewLine: Boolean, val splitNewLine = builder.splitNewLine
val auth: String val auth = builder.auth
) {
/** /**
* [Builds][build] a new configuration. * [Builds][build] a new configuration.
* *
@ -72,20 +72,86 @@ class JokeConfig private constructor(
var splitNewLine: Boolean = false, var splitNewLine: Boolean = false,
var auth: String = "" var auth: String = ""
) { ) {
fun categories(categories: Set<Category>) = apply { this.categories = categories } /**
fun lang(language: Language) = apply { lang = language } * JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
fun blacklistFlags(flags: Set<Flag>) = apply { blacklistFlags = flags } * about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING]
fun type(type: Type) = apply { this.type = type } * category, dark humor will be in the [Category.DARK] category and so on. If you want jokes from all
fun format(format: Format) = apply { this.format = format } * categories, you can instead use [Category.ANY], which will make JokeAPI randomly choose a category.
fun contains(search: String) = apply { contains = search } */
fun idRange(idRange: IdRange) = apply { this.idRange = idRange } fun categories(categories: Set<Category>): Builder = apply { this.categories = categories }
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 * 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<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-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 * 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,30 +39,37 @@ 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.URL import java.net.URI
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 = URL(url).openConnection() as HttpURLConnection val connection = URI(url).toURL().openConnection() as HttpURLConnection
connection.setRequestProperty( try {
"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0" 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 (auth.isNotEmpty()) {
} connection.setRequestProperty("Authentication", auth)
}
if (connection.responseCode in 200..399) { val isSuccess = connection.responseCode in 200..399
val body = connection.inputStream.bufferedReader().use { it.readText() } val stream = if (isSuccess) connection.inputStream else connection.errorStream
if (JokeApi.logger.isLoggable(Level.FINE)) { 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) JokeApi.logger.fine(body)
} }
return body return JokeResponse(connection.responseCode, body)
} else { } finally {
throw httpError(connection.responseCode) connection.disconnect()
} }
} }
@ -123,6 +130,9 @@ 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) }
@ -136,6 +146,9 @@ 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-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 * 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,7 +44,6 @@ 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-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 * 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-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 * 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-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 * 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-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 * 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-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 * 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-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 * 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

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

View file

@ -1,7 +1,7 @@
/* /*
* Language.kt * 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 * 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-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 * 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,6 +34,7 @@ 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-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 * 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-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 * 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,6 +32,7 @@
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
@ -51,8 +52,9 @@ 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) val json = JSONObject(response.data)
assertAll("Validate JSON", assertAll(
"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) })
@ -65,14 +67,16 @@ 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)
) )
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 @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, "apiCall(ping, txt)").startsWith("Pong!") assertThat(ping.statusCode).isEqualTo(200)
assertThat(ping.data).startsWith("Pong!")
} }
@Test @Test
@ -82,6 +86,7 @@ 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).startsWith("<?xml version='1.0'?>") assertThat(lang.statusCode).isEqualTo(200)
assertThat(lang.data).startsWith("<?xml version='1.0'?>")
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* BeforeAllTests.kt * 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 * 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-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 * 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,8 +41,6 @@ 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 {
@ -63,19 +61,20 @@ internal class ExceptionsTest {
} }
} }
@ParameterizedTest @Test
@ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666]) fun `Validate HTTP Exceptions`() {
fun `Validate HTTP Exceptions`(code: Int) { val locs = ArrayList<Pair<String, Int>>()
val e = assertThrows<HttpErrorException> { locs.add(Pair("https://apichallenges.herokuapp.com/secret/note", 401))
fetchUrl("https://httpstat.us/$code") locs.add(Pair("https://apichallenges.herokuapp.com/todo", 404))
}
assertThat(e, "fetchUrl($code)").all { for ((url, code) in locs) {
prop(HttpErrorException::statusCode).isEqualTo(code) val e = assertThrows<HttpErrorException> {
prop(HttpErrorException::message).isNotNull().isNotEmpty() fetchUrl(url)
if (code < 600) }
prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull() assertThat(e, "fetchUrl($code)").all {
else prop(HttpErrorException::statusCode).isEqualTo(code)
prop(HttpErrorException::cause).isNull() prop(HttpErrorException::message).isNotNull().isNotEmpty()
}
} }
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* GetJokeTest.kt * 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 * 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,15 +70,12 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with ID`() { fun `Get Joke with ID`() {
val id = 172 val id = 201
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).all { prop(Joke::flags).contains(Flag.RELIGIOUS);
contains(Flag.EXPLICIT) prop(Joke::id).isEqualTo(id)
contains(Flag.NSFW)
}
prop(Joke::id).isEqualTo(172)
prop(Joke::category).isEqualTo(Category.PUN) prop(Joke::category).isEqualTo(Category.PUN)
} }
} }
@ -137,12 +134,10 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with Split Newline`() { fun `Get Joke with Split Newline`() {
val joke = joke( val joke = joke(type = Type.SINGLE, idRange = IdRange(18), splitNewLine = true)
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().isEqualTo(2) size().isGreaterThanOrEqualTo(2)
each { each {
containsNone("\n") containsNone("\n")
} }
@ -177,13 +172,12 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke using Search`() { fun `Get Joke using Search`() {
val id = 265 val search = "UDP joke"
val search = "his wife"
val 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()) logger.fine(joke.toString())
assertThat(joke, "joke($search)").all { assertThat(joke, "joke($search)").all {
prop(Joke::id).isEqualTo(id) prop(Joke::id).isEqualTo(0)
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-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 * 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-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 * 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,47 +33,60 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.doesNotContain import assertk.assertions.*
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, "rawJoke(txt)").all { assertThat(response).all {
isNotEmpty() prop("statusCode", JokeResponse::statusCode).isEqualTo(200)
doesNotContain("Error") prop("data", JokeResponse::data).all {
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, "rawJoke(xml)").startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>") assertThat(response).all {
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, "rawJoke(yaml)").startsWith("error: false") assertThat(response).all {
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)
assertContains(response, "\"amount\": 2", false, "rawJoke(2)") assertThat(response).all {
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))
assertContains(response, "\"error\": true", false, "getRawJokes(foo)") assertThat(response).all {
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-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 * 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,8 +102,9 @@ internal class JokeConfigTest {
amount(2) amount(2)
safe(true) safe(true)
}.build() }.build()
val joke = getRawJokes(config) val jokes = getRawJokes(config)
assertContains(joke, "----------------------------------------------", false, "config.amount(2)") assertThat(jokes.statusCode).isEqualTo(200)
assertContains(jokes.data, "----------------------------------------------", false, "config.amount(2)")
} }
@Test @Test
@ -154,8 +155,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::language).isEqualTo(language) prop(JokeConfig::lang).isEqualTo(language)
prop(JokeConfig::flags).isEqualTo(flags) prop(JokeConfig::blacklistFlags).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-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 * 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,6 +33,7 @@ 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
@ -54,7 +55,8 @@ internal class JokeUtilTest {
@Test @Test
fun `Validate Authentication Header`() { fun `Validate Authentication Header`() {
val token = "AUTH-TOKEN" val token = "AUTH-TOKEN"
val body = fetchUrl("https://postman-echo.com/get", token) val response = fetchUrl("https://postman-echo.com/get", token)
assertThat(body, "body").contains("\"authentication\": \"$token\"") assertThat(response.statusCode).isEqualTo(200)
assertThat(response.data, "body").contains("\"authentication\": \"$token\"")
} }
} }