Compare commits

..

60 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
79ab2287d3 Version 0.9.1 2023-11-26 00:13:19 -08:00
bddd116090 Added detekt extension 2023-11-25 21:23:05 -08:00
21fffc19a2 Upgraded to Kotlin 1.9.21 2023-11-23 18:00:16 -08:00
0216c11e0e Fixed bld-ci badge 2023-11-12 16:07:31 -08:00
2dd3f59fd5 Minor cleanup 2023-11-12 01:09:49 -08:00
aad1b0a486 Fixed sonar properties 2023-11-08 22:51:23 -08:00
f613b8cdfc Switched from Gradle to bld 2023-11-08 22:15:17 -08:00
4c031d7a61 Updated dependencies 2023-10-18 21:28:24 -07:00
75 changed files with 3059 additions and 969 deletions

View file

@ -1,53 +1,62 @@
version: 2 version: 2.1
orbs:
sdkman: joshdholtz/sdkman@0.2.0
defaults: &defaults defaults: &defaults
working_directory: ~/repo working_directory: ~/repo
environment: environment:
JVM_OPTS: -Xmx3200m JVM_OPTS: -Xmx3200m
TERM: dumb TERM: dumb
CI: true CI_NAME: "CircleCI"
commands:
build_and_test:
parameters:
reports-dir:
type: string
default: "build/reports/test_results"
steps:
- checkout
- sdkman/setup-sdkman
- sdkman/sdkman-install:
candidate: kotlin
version: 2.0.20
- run:
name: Download dependencies
command: ./bld download
- run:
name: Compile source
command: ./bld compile
- run:
name: Run tests
command: ./bld jacoco -reports-dir=<< parameters.reports-dir >>
- store_test_results:
path: << parameters.reports-dir >>
- store_artifacts:
path: build/reports/jacoco/test/html
defaults_gradle: &defaults_gradle
steps:
- checkout
- restore_cache:
keys:
- gradle-dependencies-{{ checksum "build.gradle.kts" }}
# fallback to using the latest cache if no exact match is found
- gradle-dependencies-
- run:
name: Gradle Dependencies
command: ./gradlew dependencies
- save_cache:
paths: ~/.m2
key: gradle-dependencies-{{ checksum "build.gradle.kts" }}
- run:
name: Run All Checks
command: ./gradlew check
- store_artifacts:
path: build/reports/
destination: reports
- store_test_results:
path: build/reports/
jobs: jobs:
build_gradle_jdk17: bld_jdk17:
<<: *defaults <<: *defaults
docker: docker:
- image: cimg/openjdk:17.0 - image: cimg/openjdk:17.0
<<: *defaults_gradle steps:
- build_and_test
build_gradle_jdk11: bld_jdk20:
<<: *defaults <<: *defaults
docker: docker:
- image: cimg/openjdk:11.0 - image: cimg/openjdk:20.0
<<: *defaults_gradle steps:
- build_and_test
workflows: workflows:
version: 2 bld:
gradle:
jobs: jobs:
- build_gradle_jdk11 - bld_jdk17
- build_gradle_jdk17 - bld_jdk20

51
.github/workflows/bld.yml vendored Normal file
View file

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

View file

@ -1,49 +0,0 @@
name: gradle-ci
on: [ push, pull_request, workflow_dispatch ]
jobs:
build:
runs-on: ubuntu-latest
env:
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m"
SONAR_JDK: "17"
strategy:
matrix:
java-version: [ 11, 17, 20 ]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java-version }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Cache SonarCloud packages
if: matrix.java-version == env.SONAR_JDK
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Test with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build check --stacktrace
- name: SonarCloud
if: success() && matrix.java-version == env.SONAR_JDK
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew sonar --info

135
.gitignore vendored
View file

@ -1,84 +1,57 @@
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
*.class
*.code-workspace
*.ctxt
*.iws
*.log
*.nar
*.rar
*.sublime-*
*.tar.gz
*.zip
.DS_Store
.classpath
.gradle .gradle
.history .DS_Store
.kobalt build
.mtj.tmp/ lib/bld/**
.mvn/timing.properties !lib/bld/bld-wrapper.jar
.mvn/wrapper/maven-wrapper.jar !lib/bld/bld-wrapper.properties
.nb-gradle lib/compile/
.project lib/runtime/
.scannerwork lib/standalone/
.settings lib/test/
.vscode/*
/**/.idea/$CACHE_FILE$ # IDEA ignores
/**/.idea/$PRODUCT_WORKSPACE_FILE$
/**/.idea/**/caches/build_file_checksums.ser # User-specific
/**/.idea/**/contentModel.xml .idea/**/workspace.xml
/**/.idea/**/dataSources.ids .idea/**/tasks.xml
/**/.idea/**/dataSources.local.xml .idea/**/usage.statistics.xml
/**/.idea/**/dataSources/ .idea/**/dictionaries
/**/.idea/**/dbnavigator.xml .idea/**/shelf
/**/.idea/**/dictionaries
/**/.idea/**/dynamic.xml # AWS User-specific
/**/.idea/**/gradle.xml .idea/**/aws.xml
/**/.idea/**/httpRequests
/**/.idea/**/libraries # Generated files
/**/.idea/**/mongoSettings.xml .idea/**/contentModel.xml
/**/.idea/**/replstate.xml
/**/.idea/**/shelf # Sensitive or high-churn files
/**/.idea/**/shelf/ .idea/**/dataSources/
/**/.idea/**/sqlDataSources.xml .idea/**/dataSources.ids
/**/.idea/**/tasks.xml .idea/**/dataSources.local.xml
/**/.idea/**/uiDesigner.xml .idea/**/sqlDataSources.xml
/**/.idea/**/usage.statistics.xml .idea/**/dynamic.xml
/**/.idea/**/workspace.xml .idea/**/uiDesigner.xml
/**/.idea/sonarlint* .idea/**/dbnavigator.xml
/**/.idea_modules/
Thumbs.db # Gradle
__pycache__ .idea/**/gradle.xml
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
bin/
build/ # Cursive Clojure plugin
cmake-build-*/ .idea/replstate.xml
com_crashlytics_export_strings.xml
crashlytics-build.properties # SonarLint plugin
crashlytics.properties .idea/sonarlint/
dependency-reduced-pom.xml
deploy/ # Editor-based Rest Client
dist/ .idea/httpRequests
ehthumbs.db
fabric.properties
gen/
hs_err_pid*
kobaltBuild
kobaltw*-test
lib/kotlin*
libs/
local.properties local.properties
out/
pom.xml.asc
pom.xml.next
pom.xml.releaseBackup
pom.xml.tag
pom.xml.versionsBackup
proguard-project.txt
project.properties
release.properties
target/
test-output
venv

30
.idea/app.iml generated Normal file
View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager">
<output url="file://$MODULE_DIR$/build/main" />
<output-test url="file://$MODULE_DIR$/build/test" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" relativeOutputPath="resources" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/kotlin" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library" scope="RUNTIME">
<library>
<CLASSES>
<root url="file://$MODULE_DIR$/src/main/resources/templates" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="compile" level="project" />
<orderEntry type="library" scope="RUNTIME" name="runtime" level="project" />
<orderEntry type="library" scope="TEST" name="test" level="project" />
</component>
</module>

14
.idea/bld.iml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager">
<output url="file://$MODULE_DIR$/build/bld" />
<output-test url="file://$MODULE_DIR$/build/bld" />
<exclude-output />
<content url="file://$MODULE_DIR$/src/bld">
<sourceFolder url="file://$MODULE_DIR$/src/bld/java" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="bld" level="project" />
</component>
</module>

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>

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="1.9.10" /> <option name="version" value="2.1.0" />
</component> </component>
</project> </project>

18
.idea/libraries/bld.xml generated Normal file
View file

@ -0,0 +1,18 @@
<component name="libraryTable">
<library name="bld">
<CLASSES>
<root url="file://$PROJECT_DIR$/lib/bld" />
<root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$PROJECT_DIR$/lib/bld" />
<root url="jar://$USER_HOME$/.bld/dist/bld-2.2.1-sources.jar!/" />
</SOURCES>
<excluded>
<root url="jar://$PROJECT_DIR$/lib/bld/bld-wrapper.jar!/" />
</excluded>
<jarDirectory url="file://$PROJECT_DIR$/lib/bld" recursive="false" />
<jarDirectory url="file://$PROJECT_DIR$/lib/bld" recursive="false" type="SOURCES" />
</library>
</component>

13
.idea/libraries/compile.xml generated Normal file
View file

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="compile">
<CLASSES>
<root url="file://$PROJECT_DIR$/lib/compile" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$PROJECT_DIR$/lib/compile" />
</SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/compile" recursive="true" type="SOURCES" />
</library>
</component>

14
.idea/libraries/runtime.xml generated Normal file
View file

@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="runtime">
<CLASSES>
<root url="file://$PROJECT_DIR$/lib/runtime" />
<root url="file://$PROJECT_DIR$/src/main/resources" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$PROJECT_DIR$/lib/runtime" />
</SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/runtime" recursive="true" type="SOURCES" />
</library>
</component>

14
.idea/libraries/test.xml generated Normal file
View file

@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="test">
<CLASSES>
<root url="file://$PROJECT_DIR$/lib/test" />
<root url="file://$PROJECT_DIR$/src/test/resources" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$PROJECT_DIR$/lib/test" />
</SOURCES>
<jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" />
<jarDirectory url="file://$PROJECT_DIR$/lib/test" recursive="true" type="SOURCES" />
</library>
</component>

22
.idea/misc.xml generated
View file

@ -1,9 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="EntryPointsManager">
<component name="PDMPlugin"> <entry_points version="2.0">
<option name="skipTestSources" value="false" /> <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="detekt" />
<pattern value="net.thauvin.erik.JokeApiBuild" method="detektBaseline" />
<pattern value="net.thauvin.erik.jokeapi.models.Category" />
<pattern value="net.thauvin.erik.jokeapi.models.Category" method="Category" />
<pattern value="net.thauvin.erik.jokeapi.models.Flag" method="Flag" />
<pattern value="net.thauvin.erik.jokeapi.models.Format" method="Format" />
<pattern value="net.thauvin.erik.jokeapi.models.Language" method="Language" />
<pattern value="net.thauvin.erik.jokeapi.models.Type" method="Type" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="19" 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$/out" /> <output url="file://$PROJECT_DIR$/build" />
</component> </component>
</project> </project>

9
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/app.iml" filepath="$PROJECT_DIR$/.idea/app.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/bld.iml" filepath="$PROJECT_DIR$/.idea/bld.iml" />
</modules>
</component>
</project>

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,11 +1,12 @@
[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause) [![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause)
[![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-7f52ff)](https://kotlinlang.org/) [![Kotlin](https://img.shields.io/badge/kotlin-2.1.20-7f52ff)](https://kotlinlang.org/)
[![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/) [![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/)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_jokeapi&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_jokeapi) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_jokeapi&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_jokeapi)
[![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml) [![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml)
[![CircleCI](https://circleci.com/gh/ethauvin/jokeapi/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/jokeapi/tree/master) [![CircleCI](https://circleci.com/gh/ethauvin/jokeapi/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/jokeapi/tree/master)
# JokeAPI for Kotlin, Java and Android # JokeAPI for Kotlin, Java and Android
@ -15,7 +16,7 @@ A simple library to retrieve jokes from [Sv443's JokeAPI](https://v2.jokeapi.dev
## Examples (TL;DR) ## 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)
@ -88,6 +89,19 @@ var config = new JokeConfig.Builder()
var joke = JokeApi.joke(config); var joke = JokeApi.joke(config);
joke.getJoke().forEach(System.out::println); joke.getJoke().forEach(System.out::println);
``` ```
## bld
To use with [bld](https://rife2.com/bld), include the following dependency in your build file:
```java
repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
scope(compile)
.include(dependency("net.thauvin.erik", "jokeapi", "1.0.0"));
```
Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
## Gradle, Maven, etc. ## Gradle, Maven, etc.
To use with [Gradle](https://gradle.org/), include the following dependency in your build file: To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
@ -98,7 +112,7 @@ repositories {
} }
dependencies { dependencies {
implementation("net.thauvin.erik:jokeapi:0.9.0") implementation("net.thauvin.erik:jokeapi:1.0.0")
} }
``` ```
@ -110,9 +124,10 @@ You can also retrieve one or more raw (unprocessed) jokes in all [supported form
For example for YAML: 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"
@ -128,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
@ -139,15 +154,37 @@ A generic `apiCall()` function is available to access other [JokeAPI endpoints](
For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint): 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

@ -0,0 +1,318 @@
/*
* JokeApi.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.*
import net.thauvin.erik.urlencoder.UrlEncoderUtil
import org.json.JSONObject
import java.util.logging.Logger
import java.util.stream.Collectors
/**
* Implements the [Sv443's JokeAPI](https://jokeapi.dev/).
*/
object JokeApi {
private const val API_URL = "https://v2.jokeapi.dev/"
@JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
/**
* Makes a direct API call.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
*/
@JvmStatic
@JvmOverloads
@Throws(HttpErrorException::class)
fun apiCall(
endPoint: String,
path: String = "",
params: Map<String, String> = emptyMap(),
auth: String = ""
): String {
val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) {
if (!urlBuilder.endsWith(('/'))) {
urlBuilder.append('/')
}
urlBuilder.append(path)
}
if (params.isNotEmpty()) {
urlBuilder.append('?')
val it = params.iterator()
while (it.hasNext()) {
val param = it.next()
urlBuilder.append(param.key)
if (param.value.isNotEmpty()) {
urlBuilder.append("=").append(UrlEncoderUtil.encode(param.value))
}
if (it.hasNext()) {
urlBuilder.append("&")
}
}
}
return fetchUrl(urlBuilder.toString(), auth)
}
/**
* Returns one or more jokes using a [configuration][JokeConfig].
*
* See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): String {
return rawJokes(
categories = config.categories,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
format = config.format,
contains = config.contains,
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
auth = config.auth
)
}
/**
* Retrieve a [Joke] instance using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@JvmOverloads
@Throws(HttpErrorException::class, JokeException::class)
fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return joke(
categories = config.categories,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
safe = config.safe,
auth = config.auth,
splitNewLine = config.splitNewLine
)
}
/**
* Returns an array of [Joke] instances using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
@Throws(HttpErrorException::class, JokeException::class)
fun jokes(config: JokeConfig): Array<Joke> {
return jokes(
categories = config.categories,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
auth = config.auth,
splitNewLine = config.splitNewLine
)
}
}
/**
* Returns a [Joke] instance.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun joke(
categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN,
blacklistFlags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
contains: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
auth: String = "",
splitNewLine: Boolean = false
): Joke {
val json = JSONObject(
rawJokes(
categories = categories,
lang = lang,
blacklistFlags = blacklistFlags,
type = type,
contains = contains,
idRange = idRange,
safe = safe,
auth = auth
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return parseJoke(json, splitNewLine)
}
}
/**
* Returns an array of [Joke] instances.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param amount The required amount of jokes to return.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun jokes(
amount: Int,
categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN,
blacklistFlags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
contains: String = "",
idRange: IdRange = IdRange(),
safe: Boolean = false,
auth: String = "",
splitNewLine: Boolean = false
): Array<Joke> {
val json = JSONObject(
rawJokes(
categories = categories,
lang = lang,
blacklistFlags = blacklistFlags,
type = type,
contains = contains,
idRange = idRange,
amount = amount,
safe = safe,
auth = auth
)
)
if (json.getBoolean("error")) {
throw parseError(json)
} else {
return if (json.has("amount")) {
val jokes = json.getJSONArray("jokes")
Array(jokes.length()) { i -> parseJoke(jokes.getJSONObject(i), splitNewLine) }
} else {
arrayOf(parseJoke(json, splitNewLine))
}
}
}
/**
* Returns one or more jokes.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
fun rawJokes(
categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN,
blacklistFlags: Set<Flag> = emptySet(),
type: Type = Type.ALL,
format: Format = Format.JSON,
contains: String = "",
idRange: IdRange = IdRange(),
amount: Int = 1,
safe: Boolean = false,
auth: String = ""
): String {
val params = mutableMapOf<String, String>()
// Categories
val path = if (categories.isEmpty() || categories.contains(Category.ANY)) {
Category.ANY.value
} else {
categories.stream().map(Category::value).collect(Collectors.joining(","))
}
// Language
if (lang != Language.EN) {
params[Parameter.LANG] = lang.value
}
// Flags
if (blacklistFlags.isNotEmpty()) {
if (blacklistFlags.contains(Flag.ALL)) {
params[Parameter.FLAGS] = Flag.ALL.value
} else {
params[Parameter.FLAGS] = blacklistFlags.stream().map(Flag::value).collect(Collectors.joining(","))
}
}
// Type
if (type != Type.ALL) {
params[Parameter.TYPE] = type.value
}
// Format
if (format != Format.JSON) {
params[Parameter.FORMAT] = format.value
}
// Contains
if (contains.isNotBlank()) {
params[Parameter.CONTAINS] = contains
}
// Range
if (idRange.start >= 0) {
if (idRange.end == -1 || idRange.start == idRange.end) {
params[Parameter.RANGE] = idRange.start.toString()
} else {
require(idRange.end > idRange.start) { "Invalid ID Range: ${idRange.start}, ${idRange.end}" }
params[Parameter.RANGE] = "${idRange.start}-${idRange.end}"
}
}
// Amount
require(amount > 0) { "Invalid Amount: $amount" }
if (amount > 1) {
params[Parameter.AMOUNT] = amount.toString()
}
// Safe
if (safe) {
params[Parameter.SAFE] = ""
}
return JokeApi.apiCall("joke", path, params, auth)
}

View file

@ -0,0 +1,96 @@
/*
* JokeConfig.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.JokeConfig.Builder
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
/**
* Joke Configuration.
*
* Use the [Builder] to create a new configuration.
*/
class JokeConfig private constructor(
val categories: Set<Category>,
val language: Language,
val flags: Set<Flag>,
val type: Type,
val format: Format,
val contains: String,
val idRange: IdRange,
val amount: Int,
val safe: Boolean,
val splitNewLine: Boolean,
val auth: String
) {
/**
* [Builds][build] a new configuration.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
data class Builder(
var categories: Set<Category> = setOf(Category.ANY),
var lang: Language = Language.EN,
var blacklistFlags: Set<Flag> = emptySet(),
var type: Type = Type.ALL,
var format: Format = Format.JSON,
var contains: String = "",
var idRange: IdRange = IdRange(),
var amount: Int = 1,
var safe: Boolean = false,
var splitNewLine: Boolean = false,
var auth: String = ""
) {
fun categories(categories: Set<Category>) = apply { this.categories = categories }
fun lang(language: Language) = apply { lang = language }
fun blacklistFlags(flags: Set<Flag>) = apply { blacklistFlags = flags }
fun type(type: Type) = apply { this.type = type }
fun format(format: Format) = apply { this.format = format }
fun contains(search: String) = apply { contains = search }
fun idRange(idRange: IdRange) = apply { this.idRange = idRange }
fun amount(amount: Int) = apply { this.amount = amount }
fun safe(safe: Boolean) = apply { this.safe = safe }
fun splitNewLine(splitNewLine: Boolean) = apply { this.splitNewLine = splitNewLine }
fun auth(auth: String) = apply { this.auth = auth }
fun build() = JokeConfig(
categories, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth
)
}
}

View file

@ -0,0 +1,173 @@
/*
* JokeUtil.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@file:JvmName("JokeUtil")
package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Parameter
import net.thauvin.erik.jokeapi.models.Type
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.logging.Level
internal fun fetchUrl(url: String, auth: String = ""): String {
if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(url)
}
val connection = URL(url).openConnection() as HttpURLConnection
connection.setRequestProperty(
"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
)
if (auth.isNotEmpty()) {
connection.setRequestProperty("Authentication", auth)
}
if (connection.responseCode in 200..399) {
val body = connection.inputStream.bufferedReader().use { it.readText() }
if (JokeApi.logger.isLoggable(Level.FINE)) {
JokeApi.logger.fine(body)
}
return body
} else {
throw httpError(connection.responseCode)
}
}
private fun httpError(responseCode: Int): HttpErrorException {
val httpException: HttpErrorException
when (responseCode) {
400 -> httpException = HttpErrorException(
responseCode, "Bad Request", IOException(
"The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
)
)
403 -> httpException = HttpErrorException(
responseCode, "Forbidden", IOException(
"You have been added to the blacklist due to malicious behavior and are not allowed"
+ " to send requests to JokeAPI anymore."
)
)
404 -> httpException = HttpErrorException(
responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
)
413 -> httpException = HttpErrorException(
responseCode, "URI Too Long", IOException("The URL exceeds the maximum length of 250 characters.")
)
414 -> httpException = HttpErrorException(
responseCode,
"Payload Too Large",
IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
)
429 -> httpException = HttpErrorException(
responseCode, "Too Many Requests", IOException(
"You have exceeded the limit of 120 requests per minute and have to wait a bit"
+ " until you are allowed to send requests again."
)
)
500 -> httpException = HttpErrorException(
responseCode, "Internal Server Error", IOException(
"There was a general internal error within JokeAPI. You can get more info from"
+ " the properties in the response text."
)
)
523 -> httpException = HttpErrorException(
responseCode, "Origin Unreachable", IOException(
"The server is temporarily offline due to maintenance or a dynamic IP update."
+ " Please be patient in this case."
)
)
else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
}
return httpException
}
internal fun parseError(json: JSONObject): JokeException {
val causedBy = json.getJSONArray("causedBy")
val causes = List<String>(causedBy.length()) { i -> causedBy.getString(i) }
return JokeException(
internalError = json.getBoolean("internalError"),
code = json.getInt("code"),
message = json.getString("message"),
causedBy = causes,
additionalInfo = json.getString("additionalInfo"),
timestamp = json.getLong("timestamp")
)
}
internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
val jokes = mutableListOf<String>()
if (json.has("setup")) {
jokes.add(json.getString("setup"))
jokes.add(json.getString(("delivery")))
} else {
if (splitNewLine) {
jokes.addAll(json.getString("joke").split("\n"))
} else {
jokes.add(json.getString("joke"))
}
}
val enabledFlags = mutableSetOf<Flag>()
val jsonFlags = json.getJSONObject("flags")
Flag.values().filter { it != Flag.ALL }.forEach {
if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
enabledFlags.add(it)
}
}
return Joke(
category = Category.valueOf(json.getString("category").uppercase()),
type = Type.valueOf(json.getString(Parameter.TYPE).uppercase()),
joke = jokes,
flags = enabledFlags,
safe = json.getBoolean("safe"),
id = json.getInt("id"),
lang = Language.valueOf(json.getString(Parameter.LANG).uppercase())
)
}

View file

@ -0,0 +1,49 @@
/*
* HttpErrorException.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.exceptions
import java.io.IOException
/**
* Signals that a server error has occurred.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#status-codes) for more details.
*/
class HttpErrorException @JvmOverloads constructor(
val statusCode: Int,
message: String,
cause: Throwable? = null
) : IOException(message, cause) {
companion object {
private const val serialVersionUID = 1L
}
}

View file

@ -0,0 +1,56 @@
/*
* JokeException.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.exceptions
/**
* Signals that an error has occurred.
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#errors) for more details.
*/
class JokeException @JvmOverloads constructor(
val internalError: Boolean,
val code: Int,
message: String,
val causedBy: List<String>,
val additionalInfo: String,
val timestamp: Long,
cause: Throwable? = null
) : RuntimeException(message, cause) {
companion object {
private const val serialVersionUID = 1L
}
fun debug(): String {
return "JokeException(message=$message, internalError=$internalError, code=$code," +
" causedBy=$causedBy, additionalInfo='$additionalInfo', timestamp=$timestamp)"
}
}

View file

@ -0,0 +1,45 @@
/*
* Category.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The supported [categories](https://jokeapi.dev/#categories), use [ANY] for all.
*/
enum class Category(val value: String) {
ANY("Any"),
CHRISTMAS("Christmas"),
DARK("Dark"),
MISC("Misc"),
PROGRAMMING("Programming"),
PUN("Pun"),
SPOOKY("Spooky")
}

View file

@ -0,0 +1,45 @@
/*
* Flag.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The supported [blacklist flags](https://jokeapi.dev/#flags-param), use [ALL] to prevent all.
*/
enum class Flag(val value: String) {
EXPLICIT("explicit"),
NSFW("nsfw"),
POLITICAL("political"),
RACIST("racist"),
RELIGIOUS("religious"),
SEXIST("sexist"),
ALL("${NSFW.value},${RELIGIOUS.value},${POLITICAL.value},${RACIST.value},${SEXIST.value},${EXPLICIT.value}")
}

View file

@ -0,0 +1,44 @@
/*
* Format.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The supported response [formats](https://jokeapi.dev/#format-param).
*/
enum class Format(val value: String) {
JSON("json"),
/** Plain Text */
TXT("txt"),
XML("xml"),
YAML("yaml")
}

View file

@ -0,0 +1,37 @@
/*
* IdRange.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* Specifies a joke [ID or range of IDs](https://jokeapi.dev/#idrange-param).
*/
data class IdRange(val start: Int = -1, val end: Int = -1)

View file

@ -0,0 +1,45 @@
/*
* Joke.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* Stores a joke's data.
*/
data class Joke(
val category: Category,
val type: Type,
val joke: List<String>,
val flags: Set<Flag>,
val id: Int,
val safe: Boolean,
val lang: Language
)

View file

@ -0,0 +1,55 @@
/*
* Language.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The supported [languages](https://jokeapi.dev/#lang).
*/
enum class Language(val value: String) {
/** Czech */
CS("cs"),
/** German */
DE("de"),
/** English */
EN("en"),
/** Spanish */
ES("es"),
/** French */
FR("fr"),
/** Portuguese */
PT("pt")
}

View file

@ -0,0 +1,51 @@
/*
* Parameter.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The available [URL Parameters](https://jokeapi.dev/#url-parameters).
*/
object Parameter {
const val AMOUNT = "amount"
const val CONTAINS = "contains"
const val FLAGS = "blacklistFlags"
const val FORMAT = "format"
const val RANGE = "idRange"
const val LANG = "lang"
const val SAFE = "safe-mode"
const val TYPE = "type"
const val BLACKLIST_FLAGS = FLAGS
const val ID_RANGE = RANGE
const val SAFE_MODE = SAFE
const val SEARCH = CONTAINS
}

View file

@ -0,0 +1,41 @@
/*
* Type.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi.models
/**
* The supported [types](https://jokeapi.dev/#type-param), use [ALL] for all.
*/
enum class Type(val value: String) {
SINGLE("single"),
TWOPART("twopart"),
ALL("${SINGLE.value},${TWOPART.value}")
}

View file

@ -0,0 +1,87 @@
/*
* ApiCallTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.assertThat
import assertk.assertions.isGreaterThan
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.apiCall
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Parameter
import org.json.JSONObject
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains
@ExtendWith(BeforeAllTests::class)
internal class ApiCallTest {
@Test
fun `Get Flags`() {
// See https://v2.jokeapi.dev/#flags-endpoint
val response = apiCall(endPoint = "flags")
val json = JSONObject(response)
assertAll("Validate JSON",
{ assertFalse(json.getBoolean("error"), "apiCall(flags).error") },
{ assertThat(json.getJSONArray("flags").length(), "apiCall(flags).flags").isGreaterThan(0) },
{ assertThat(json.getLong("timestamp"), "apiCall(flags).timestamp").isGreaterThan(0) })
}
@Test
fun `Get Language Code`() {
// See https://v2.jokeapi.dev/#langcode-endpoint
val lang = apiCall(
endPoint = "langcode", path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)
)
assertContains(lang, "code: \"fr\"", false, "apiCall(langcode, french, yaml)")
}
@Test
fun `Get Ping Response`() {
// See https://v2.jokeapi.dev/#ping-endpoint
val ping = apiCall(endPoint = "ping", params = mapOf(Parameter.FORMAT to Format.TXT.value))
assertThat(ping, "apiCall(ping, txt)").startsWith("Pong!")
}
@Test
fun `Get Supported Language`() {
// See https://v2.jokeapi.dev/languages
val lang = apiCall(
endPoint = "languages",
params = mapOf(Parameter.FORMAT to Format.XML.value, Parameter.LANG to Language.FR.value)
)
assertThat(lang).startsWith("<?xml version='1.0'?>")
}
}

View file

@ -0,0 +1,47 @@
/*
* BeforeAllTests.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import java.util.logging.ConsoleHandler
import java.util.logging.Level
class BeforeAllTests : BeforeAllCallback {
override fun beforeAll(context: ExtensionContext?) {
with(JokeApi.logger) {
addHandler(ConsoleHandler().apply { level = Level.FINE })
level = Level.FINE
}
}
}

View file

@ -0,0 +1,90 @@
/*
* ExceptionsTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.index
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isGreaterThan
import assertk.assertions.isNotEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.prop
import assertk.assertions.size
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
@ExtendWith(BeforeAllTests::class)
internal class ExceptionsTest {
@Test
fun `Validate Joke Exception`() {
val e = assertThrows<JokeException> {
joke(categories = setOf(Category.CHRISTMAS), contains = "foo")
}
logger.fine(e.debug())
assertThat(e, "joke(${Category.CHRISTMAS},foo)").all {
prop(JokeException::code).isEqualTo(106)
prop(JokeException::internalError).isFalse()
prop(JokeException::message).isEqualTo("No matching joke found")
prop(JokeException::causedBy).size().isEqualTo(1)
prop(JokeException::causedBy).index(0).startsWith("No jokes")
prop(JokeException::additionalInfo).isNotEmpty()
prop(JokeException::timestamp).isGreaterThan(0)
}
}
@ParameterizedTest
@ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
fun `Validate HTTP Exceptions`(code: Int) {
val e = assertThrows<HttpErrorException> {
fetchUrl("https://httpstat.us/$code")
}
assertThat(e, "fetchUrl($code)").all {
prop(HttpErrorException::statusCode).isEqualTo(code)
prop(HttpErrorException::message).isNotNull().isNotEmpty()
if (code < 600)
prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull()
else
prop(HttpErrorException::cause).isNull()
}
}
}

View file

@ -0,0 +1,211 @@
/*
* GetJokeTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.any
import assertk.assertions.contains
import assertk.assertions.containsNone
import assertk.assertions.each
import assertk.assertions.isBetween
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isIn
import assertk.assertions.isNotEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(BeforeAllTests::class)
internal class GetJokeTest {
@Test
fun `Get Joke`() {
val joke = joke()
logger.fine(joke.toString())
assertThat(joke, "joke()").all {
prop(Joke::joke).isNotEmpty()
prop(Joke::type).isIn(Type.SINGLE, Type.TWOPART)
prop(Joke::id).isGreaterThanOrEqualTo(0)
prop(Joke::lang).isEqualTo(Language.EN)
}
}
@Test
fun `Get Joke without Blacklist Flags`() {
val joke = joke(blacklistFlags = setOf(Flag.ALL))
assertThat(joke::flags).isEmpty()
}
@Test
fun `Get Joke without any Blacklist Flags`() {
val allFlags = Flag.values().filter { it != Flag.ALL }.toSet()
val joke = joke(blacklistFlags = allFlags)
assertThat(joke::flags).isEmpty()
}
@Test
fun `Get Joke with ID`() {
val id = 172
val joke = joke(idRange = IdRange(id))
logger.fine(joke.toString())
assertThat(joke, "joke($id)").all {
prop(Joke::flags).all {
contains(Flag.EXPLICIT)
contains(Flag.NSFW)
}
prop(Joke::id).isEqualTo(172)
prop(Joke::category).isEqualTo(Category.PUN)
}
}
@Test
fun `Get Joke with ID Range`() {
val idRange = IdRange(1, 100)
val joke = joke(idRange = idRange)
logger.fine(joke.toString())
assertThat(joke::id).isBetween(idRange.start, idRange.end)
}
@Test
fun `Get Joke with invalid ID Range`() {
val idRange = IdRange(100, 1)
val e = assertThrows<IllegalArgumentException> { joke(idRange = idRange, lang = Language.DE) }
assertThat(e::message).isNotNull().contains("100, 1")
}
@Test
fun `Get Joke with max ID Range`() {
val idRange = IdRange(1, 30000)
val e = assertThrows<JokeException> { joke(idRange = idRange) }
assertThat(e, "joke{${idRange})").all {
prop(JokeException::additionalInfo).contains("ID range")
}
}
@Test
fun `Get Joke with two Categories`() {
val joke = joke(categories = setOf(Category.PROGRAMMING, Category.MISC))
logger.fine(joke.toString())
assertThat(joke.category, "joke(${Category.PROGRAMMING},${Category.MISC})").isIn(
Category.PROGRAMMING,
Category.MISC
)
}
@Test
fun `Get Joke with each Categories`() {
Category.values().filter { it != Category.ANY }.forEach {
val joke = joke(categories = setOf(it))
logger.fine(joke.toString())
assertThat(joke::category, "joke($it)").prop(Category::value).isEqualTo(it.value)
}
}
@Test
fun `Get Joke with each Languages`() {
Language.values().forEach {
val joke = joke(lang = it)
logger.fine(joke.toString())
assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value)
}
}
@Test
fun `Get Joke with Split Newline`() {
val joke = joke(
categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true
)
logger.fine(joke.toString())
assertThat(joke::joke, "joke(splitNewLine=true)").all {
size().isEqualTo(2)
each {
containsNone("\n")
}
}
}
@Test
fun `Get Safe Joke`() {
val joke = joke(safe = true)
logger.fine(joke.toString())
assertThat(joke, "joke(safe)").all {
prop(Joke::safe).isTrue()
}
}
@Test
fun `Get Single Joke`() {
val joke = joke(type = Type.SINGLE)
logger.fine(joke.toString())
assertThat(joke::type).assertThat(Type.SINGLE)
}
@Test
fun `Get Two-Parts Joke`() {
val joke = joke(type = Type.TWOPART)
logger.fine(joke.toString())
assertThat(joke, "joke(${Type.TWOPART})").all {
prop(Joke::type).isEqualTo(Type.TWOPART)
prop(Joke::joke).size().isGreaterThan(1)
}
}
@Test
fun `Get Joke using Search`() {
val id = 265
val search = "his wife"
val joke =
joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true)
logger.fine(joke.toString())
assertThat(joke, "joke($search)").all {
prop(Joke::id).isEqualTo(id)
prop(Joke::joke).any {
it.contains(search)
}
}
}
}

View file

@ -0,0 +1,84 @@
/*
* GetJokesTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.each
import assertk.assertions.index
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(BeforeAllTests::class)
internal class GetJokesTest {
@Test
fun `Get Multiple Jokes`() {
val amount = 2
val jokes = jokes(amount = amount, safe = true, lang = Language.FR)
assertThat(jokes, "jokes").all {
size().isEqualTo(amount)
each {
it.prop(Joke::id).isGreaterThanOrEqualTo(0)
it.prop(Joke::safe).isTrue()
it.prop(Joke::lang).isEqualTo(Language.FR)
}
}
}
@Test
fun `Get Jokes with Invalid Amount`() {
val e = assertThrows<IllegalArgumentException> { jokes(amount = -1) }
assertThat(e::message).isNotNull().contains("-1")
}
@Test
fun `Get One Joke as Multiple`() {
val jokes = jokes(amount = 1, safe = true)
assertThat(jokes, "jokes").all {
size().isEqualTo(1)
index(0).all {
prop(Joke::id).isGreaterThanOrEqualTo(0)
prop(Joke::safe).isTrue()
}
}
}
}

View file

@ -0,0 +1,79 @@
/*
* GetRawJokesTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.doesNotContain
import assertk.assertions.isNotEmpty
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains
@ExtendWith(BeforeAllTests::class)
internal class GetRawJokesTest {
@Test
fun `Get Raw Joke with TXT`() {
val response = rawJokes(format = Format.TXT)
assertThat(response, "rawJoke(txt)").all {
isNotEmpty()
doesNotContain("Error")
}
}
@Test
fun `Get Raw Joke with XML`() {
val response = rawJokes(format = Format.XML)
assertThat(response, "rawJoke(xml)").startsWith("<?xml version='1.0'?>\n<data>\n <error>false</error>")
}
@Test
fun `Get Raw Joke with YAML`() {
val response = rawJokes(format = Format.YAML)
assertThat(response, "rawJoke(yaml)").startsWith("error: false")
}
@Test
fun `Get Raw Jokes`() {
val response = rawJokes(amount = 2)
assertContains(response, "\"amount\": 2", false, "rawJoke(2)")
}
@Test
fun `Get Raw Invalid Jokes`() {
val response = rawJokes(contains = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
}
}

View file

@ -0,0 +1,182 @@
/*
* JokeConfigTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.each
import assertk.assertions.isBetween
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.joke
import net.thauvin.erik.jokeapi.JokeApi.jokes
import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.models.Category
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains
@ExtendWith(BeforeAllTests::class)
internal class JokeConfigTest {
@Test
fun `Get Joke with Default Builder`() {
val joke = joke()
assertThat(joke, "joke").all {
prop(Joke::id).isGreaterThanOrEqualTo(0)
prop(Joke::lang).isEqualTo(Language.EN)
}
}
@Test
fun `Get Joke with Builder`() {
val id = 266
val config = JokeConfig.Builder().apply {
categories(setOf(Category.PROGRAMMING))
lang(Language.EN)
blacklistFlags(setOf(Flag.ALL))
type(Type.TWOPART)
idRange(IdRange(id - 2, id + 2))
safe(true)
}.build()
val joke = joke(config)
logger.fine(joke.toString())
assertThat(joke, "config").all {
prop(Joke::type).isEqualTo(Type.TWOPART)
prop(Joke::category).isEqualTo(Category.PROGRAMMING)
prop(Joke::joke).size().isEqualTo(2)
prop(Joke::lang).isEqualTo(Language.EN)
prop(Joke::flags).isEmpty()
prop(Joke::id).isBetween(id - 2, id + 2)
}
}
@Test
fun `Get joke with Builder and Split Newline`() {
val id = 5
val config = JokeConfig.Builder().apply {
categories(setOf(Category.PROGRAMMING))
idRange(IdRange(id))
splitNewLine(true)
}.build()
val joke = joke(config)
logger.fine(joke.toString())
assertThat(joke, "config").all {
prop(Joke::id).isEqualTo(id)
prop(Joke::joke).size().isEqualTo(2)
}
}
@Test
fun `Get Raw Joke with Builder`() {
val config = JokeConfig.Builder().apply {
categories(setOf(Category.PROGRAMMING))
format(Format.TXT)
contains("bar")
amount(2)
safe(true)
}.build()
val joke = getRawJokes(config)
assertContains(joke, "----------------------------------------------", false, "config.amount(2)")
}
@Test
fun `Get Multiple Jokes with Builder`() {
val amount = 2
val config = JokeConfig.Builder().apply {
amount(amount)
safe(true)
lang(Language.FR)
}.build()
val jokes = jokes(config)
assertThat(jokes, "jokes").all {
size().isEqualTo(amount)
each {
it.prop(Joke::id).isGreaterThanOrEqualTo(0)
it.prop(Joke::safe).isTrue()
it.prop(Joke::flags).isEmpty()
it.prop(Joke::lang).isEqualTo(Language.FR)
}
}
}
@Test
fun `Validate Config`() {
val categories = setOf(Category.ANY)
val language = Language.CS
val flags = setOf(Flag.POLITICAL, Flag.RELIGIOUS)
val type = Type.TWOPART
val format = Format.XML
val search = "foo"
val idRange = IdRange(1, 20)
val amount = 10
val safe = true
val splitNewLine = true
val auth = "token"
val config = JokeConfig.Builder().apply {
categories(categories)
lang(language)
blacklistFlags(flags)
type(type)
format(format)
contains(search)
idRange(idRange)
amount(amount)
safe(safe)
splitNewLine(splitNewLine)
auth(auth)
}.build()
assertThat(config, "config").all {
prop(JokeConfig::categories).isEqualTo(categories)
prop(JokeConfig::language).isEqualTo(language)
prop(JokeConfig::flags).isEqualTo(flags)
prop(JokeConfig::type).isEqualTo(type)
prop(JokeConfig::format).isEqualTo(format)
prop(JokeConfig::contains).isEqualTo(search)
prop(JokeConfig::idRange).isEqualTo(idRange)
prop(JokeConfig::amount).isEqualTo(amount)
prop(JokeConfig::safe).isEqualTo(safe)
prop(JokeConfig::splitNewLine).isEqualTo(splitNewLine)
prop(JokeConfig::auth).isEqualTo(auth)
}
}
}

View file

@ -0,0 +1,60 @@
/*
* UtilTest.kt
*
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.jokeapi
import assertk.assertThat
import assertk.assertions.contains
import org.json.JSONException
import org.json.JSONObject
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(BeforeAllTests::class)
internal class JokeUtilTest {
@Test
fun `Invalid JSON Error`() {
assertThrows<JSONException> { parseError(JSONObject("{}")) }
}
@Test
fun `Invalid JSON Joke`() {
assertThrows<JSONException> { parseJoke(JSONObject("{}"), false) }
}
@Test
fun `Validate Authentication Header`() {
val token = "AUTH-TOKEN"
val body = fetchUrl("https://postman-echo.com/get", token)
assertThat(body, "body").contains("\"authentication\": \"$token\"")
}
}

2
bld Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env sh
java -jar "$(dirname "$0")/lib/bld/bld-wrapper.jar" "$0" --build net.thauvin.erik.JokeApiBuild "$@"

4
bld.bat Normal file
View file

@ -0,0 +1,4 @@
@echo off
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
java -jar "%DIRNAME%/lib/bld/bld-wrapper.jar" "%0" --build net.thauvin.erik.JokeApiBuild %*

View file

@ -1,185 +0,0 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("com.github.ben-manes.versions") version "0.48.0"
id("io.gitlab.arturbosch.detekt") version "1.23.1"
id("java")
id("maven-publish")
id("org.jetbrains.dokka") version "1.9.0"
id("org.jetbrains.kotlinx.kover") version "0.7.3"
id("org.sonarqube") version "4.3.1.3277"
id("signing")
kotlin("jvm") version "1.9.10"
}
description = "Retrieve jokes from Sv443's JokeAPI"
group = "net.thauvin.erik"
version = "0.9.0"
val deployDir = "deploy"
val gitHub = "ethauvin/$name"
val mavenUrl = "https://github.com/$gitHub"
val publicationName = "mavenJava"
repositories {
mavenCentral()
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
}
dependencies {
implementation(platform(kotlin("bom")))
implementation("net.thauvin.erik.urlencoder:urlencoder-lib:1.4.0")
implementation("org.json:json:20230618")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.27.0")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
withSourcesJar()
}
koverReport {
defaults {
xml {
onCheck = true
}
html {
onCheck = true
}
}
}
sonarqube {
properties {
property("sonar.projectKey", "ethauvin_$name")
property("sonar.organization", "ethauvin-github")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.sourceEncoding", "UTF-8")
property("sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/kover/report.xml")
}
}
val javadocJar by tasks.creating(Jar::class) {
dependsOn(tasks.dokkaJavadoc)
from(tasks.dokkaJavadoc)
archiveClassifier.set("javadoc")
}
tasks {
test {
useJUnitPlatform()
}
withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = java.targetCompatibility.toString()
}
withType<Test> {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
this.jvmTarget = java.targetCompatibility.toString()
}
withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configureEach {
this.jvmTarget = java.targetCompatibility.toString()
}
withType<GenerateMavenPom> {
destination = file("$projectDir/pom.xml")
}
clean {
doLast {
project.delete(fileTree(deployDir))
}
}
withType<DokkaTask>().configureEach {
dokkaSourceSets {
named("main") {
moduleName.set("Joke API")
}
}
}
val copyToDeploy by registering(Copy::class) {
from(configurations.runtimeClasspath) {
exclude("annotations-*.jar")
}
from(jar)
into(deployDir)
}
register("deploy") {
description = "Copies all needed files to the $deployDir directory."
group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn(clean, build, jar)
outputs.dir(deployDir)
inputs.files(copyToDeploy)
mustRunAfter(clean)
}
}
publishing {
publications {
create<MavenPublication>(publicationName) {
from(components["java"])
artifact(javadocJar)
pom {
name.set(project.name)
description.set(project.description)
url.set(mavenUrl)
licenses {
license {
name.set("BSD 3-Clause")
url.set("https://opensource.org/licenses/BSD-3-Clause")
}
}
developers {
developer {
id.set("ethauvin")
name.set("Erik C. Thauvin")
email.set("erik@thauvin.net")
url.set("https://erik.thauvin.net/")
}
}
scm {
connection.set("scm:git:https://github.com/$gitHub.git")
developerConnection.set("scm:git:git@github.com:$gitHub.git")
url.set(mavenUrl)
}
issueManagement {
system.set("GitHub")
url.set("$mavenUrl/issues")
}
}
}
}
repositories {
maven {
name = "ossrh"
url = if (project.version.toString().contains("SNAPSHOT"))
uri("https://oss.sonatype.org/content/repositories/snapshots/") else
uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials(PasswordCredentials::class)
}
}
}
signing {
useGpgCmd()
sign(publishing.publications[publicationName])
}

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>
@ -18,6 +18,15 @@
<ID>MagicNumber:JokeUtil.kt$500</ID> <ID>MagicNumber:JokeUtil.kt$500</ID>
<ID>MagicNumber:JokeUtil.kt$523</ID> <ID>MagicNumber:JokeUtil.kt$523</ID>
<ID>TooManyFunctions:JokeConfig.kt$JokeConfig$Builder</ID> <ID>TooManyFunctions:JokeConfig.kt$JokeConfig$Builder</ID>
<ID>WildcardImport:ExceptionsTest.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: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:JokeConfigTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:JokeConfigTest.kt$import net.thauvin.erik.jokeapi.models.*</ID>
<ID>WildcardImport:JokeUtil.kt$import net.thauvin.erik.jokeapi.models.*</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View file

@ -1 +0,0 @@
kotlin.code.style=official

Binary file not shown.

View file

@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored
View file

@ -1,249 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored
View file

@ -1,92 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
lib/bld/bld-wrapper.jar Normal file

Binary file not shown.

View file

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

67
pom.xml
View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
<!-- This module was also published with a richer model, Gradle metadata, --> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<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.0</version> <version>1.0.1-SNAPSHOT</version>
<name>jokeapi</name> <name>jokeapi</name>
<description>Retrieve jokes from Sv443'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>
<licenses> <licenses>
<license> <license>
@ -18,6 +14,26 @@
<url>https://opensource.org/licenses/BSD-3-Clause</url> <url>https://opensource.org/licenses/BSD-3-Clause</url>
</license> </license>
</licenses> </licenses>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.1.20</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250107</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.thauvin.erik.urlencoder</groupId>
<artifactId>urlencoder-lib-jvm</artifactId>
<version>1.6.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<developers> <developers>
<developer> <developer>
<id>ethauvin</id> <id>ethauvin</id>
@ -31,39 +47,4 @@
<developerConnection>scm:git:git@github.com:ethauvin/jokeapi.git</developerConnection> <developerConnection>scm:git:git@github.com:ethauvin/jokeapi.git</developerConnection>
<url>https://github.com/ethauvin/jokeapi</url> <url>https://github.com/ethauvin/jokeapi</url>
</scm> </scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/ethauvin/jokeapi/issues</url>
</issueManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>1.9.10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.9.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.thauvin.erik.urlencoder</groupId>
<artifactId>urlencoder-lib-jvm</artifactId>
<version>1.4.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230618</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project> </project>

View file

@ -1,3 +0,0 @@
rootProject.name = "jokeapi"

7
sonar-project.properties Normal file
View file

@ -0,0 +1,7 @@
sonar.organization=ethauvin-github
sonar.projectKey=ethauvin_jokeapi
sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
sonar.sources=src/main/kotlin/
sonar.tests=src/test/kotlin/
sonar.java.binaries=build/main,build/test
sonar.java.libraries=lib/compile/*.jar

View file

@ -0,0 +1,197 @@
/*
* JokeApiBuild.java
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik;
import rife.bld.BuildCommand;
import rife.bld.Project;
import rife.bld.extension.CompileKotlinOperation;
import rife.bld.extension.DetektOperation;
import rife.bld.extension.DokkaOperation;
import rife.bld.extension.JacocoReportOperation;
import rife.bld.extension.dokka.LoggingLevel;
import rife.bld.extension.dokka.OutputFormat;
import rife.bld.extension.kotlin.CompileOptions;
import rife.bld.operations.exceptions.ExitStatusException;
import rife.bld.publish.PomBuilder;
import rife.bld.publish.PublishDeveloper;
import rife.bld.publish.PublishLicense;
import rife.bld.publish.PublishScm;
import rife.tools.exceptions.FileUtilsErrorException;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static rife.bld.dependencies.Repository.*;
import static rife.bld.dependencies.Scope.compile;
import static rife.bld.dependencies.Scope.test;
public class JokeApiBuild extends Project {
final File srcMainKotlin = new File(srcMainDirectory(), "kotlin");
public JokeApiBuild() {
pkg = "net.thauvin.erik";
name = "jokeapi";
version = version(1, 0, 1, "SNAPSHOT");
javaRelease = 11;
downloadSources = true;
autoDownloadPurge = true;
repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL);
final var kotlin = version(2, 1, 20);
scope(compile)
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
.include(dependency("org.json", "json", "20250107"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", version(1, 6, 0)));
scope(test)
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1)))
.include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 1)))
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)));
publishOperation()
.repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location())
.withCredentials(property("sonatype.user"), property("sonatype.password"))
: repository(SONATYPE_RELEASES_LEGACY.location())
.withCredentials(property("sonatype.user"), property("sonatype.password")))
.repository(repository("github"))
.info()
.groupId(pkg)
.artifactId(name)
.description("Retrieve jokes from Sv443's JokeAPI")
.url("https://github.com/ethauvin/" + name)
.developer(
new PublishDeveloper()
.id("ethauvin")
.name("Erik C. Thauvin")
.email("erik@thauvin.net")
.url("https://erik.thauvin.net/")
)
.license(
new PublishLicense()
.name("BSD 3-Clause")
.url("https://opensource.org/licenses/BSD-3-Clause")
)
.scm(
new PublishScm()
.connection("scm:git:https://github.com/ethauvin/" + name + ".git")
.developerConnection("scm:git:git@github.com:ethauvin/" + name + ".git")
.url("https://github.com/ethauvin/" + name)
)
.signKey(property("sign.key"))
.signPassphrase(property("sign.passphrase"));
jarSourcesOperation().sourceDirectories(srcMainKotlin);
}
public static void main(String[] args) {
// Enable detailed logging for the extensions
var level = Level.ALL;
var logger = Logger.getLogger("rife.bld.extension");
var consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(level);
logger.addHandler(consoleHandler);
logger.setLevel(level);
logger.setUseParentHandlers(false);
new JokeApiBuild().start(args);
}
@BuildCommand(summary = "Compiles the Kotlin project")
@Override
public void compile() throws Exception {
new CompileKotlinOperation()
.fromProject(this)
.compileOptions(new CompileOptions().verbose(true))
.execute();
}
@BuildCommand(summary = "Checks source with Detekt")
public void detekt() throws ExitStatusException, IOException, InterruptedException {
new DetektOperation()
.fromProject(this)
.execute();
}
@BuildCommand(value = "detekt-baseline", summary = "Creates the Detekt baseline")
public void detektBaseline() throws ExitStatusException, IOException, InterruptedException {
new DetektOperation()
.fromProject(this)
.baseline("detekt-baseline.xml")
.createBaseline(true)
.execute();
}
@BuildCommand(summary = "Generates JaCoCo Reports")
public void jacoco() throws Exception {
new JacocoReportOperation()
.fromProject(this)
.sourceFiles(srcMainKotlin)
.execute();
}
@Override
public void javadoc() throws ExitStatusException, IOException, InterruptedException {
new DokkaOperation()
.fromProject(this)
.loggingLevel(LoggingLevel.INFO)
.moduleName("JokeApi")
.moduleVersion(version.toString())
.outputDir(new File(buildDirectory(), "javadoc"))
.outputFormat(OutputFormat.JAVADOC)
.execute();
}
@Override
public void publish() throws Exception {
super.publish();
pomRoot();
}
@Override
public void publishLocal() throws Exception {
super.publishLocal();
pomRoot();
}
@BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory")
public void pomRoot() throws FileUtilsErrorException {
PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(),
new File(workDirectory, "pom.xml"));
}
}

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:
@ -32,31 +32,26 @@
package net.thauvin.erik.jokeapi package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.JokeConfig.Builder import net.thauvin.erik.jokeapi.JokeConfig.Builder
import net.thauvin.erik.jokeapi.models.Category import net.thauvin.erik.jokeapi.models.*
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
/** /**
* Joke Configuration. * Joke Configuration.
* *
* 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.
* *
@ -77,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:
@ -35,39 +35,41 @@ package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category import net.thauvin.erik.jokeapi.models.*
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Parameter
import net.thauvin.erik.jokeapi.models.Type
import org.json.JSONObject import 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()
} }
} }
@ -128,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) }
@ -141,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")) {
@ -155,7 +163,7 @@ internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
} }
val enabledFlags = mutableSetOf<Flag>() val enabledFlags = mutableSetOf<Flag>()
val jsonFlags = json.getJSONObject("flags") val jsonFlags = json.getJSONObject("flags")
Flag.values().filter { it != Flag.ALL }.forEach { Flag.entries.filter { it != Flag.ALL }.forEach {
if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) { if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
enabledFlags.add(it) enabledFlags.add(it)
} }

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:

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:
@ -29,6 +29,8 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
@file:Suppress("ConstPropertyName")
package net.thauvin.erik.jokeapi.exceptions package net.thauvin.erik.jokeapi.exceptions
/** /**

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:
@ -33,16 +33,7 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.index import assertk.assertions.*
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isGreaterThan
import assertk.assertions.isNotEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.prop
import assertk.assertions.size
import assertk.assertions.startsWith
import net.thauvin.erik.jokeapi.JokeApi.logger import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException import net.thauvin.erik.jokeapi.exceptions.JokeException
@ -50,8 +41,6 @@ import net.thauvin.erik.jokeapi.models.Category
import org.junit.jupiter.api.Test import org.junit.jupiter.api.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 {
@ -72,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:
@ -33,29 +33,10 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.any import assertk.assertions.*
import assertk.assertions.contains
import assertk.assertions.containsNone
import assertk.assertions.each
import assertk.assertions.isBetween
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isIn
import assertk.assertions.isNotEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.logger import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.exceptions.JokeException import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category import net.thauvin.erik.jokeapi.models.*
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.junit.jupiter.api.Test import org.junit.jupiter.api.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
@ -82,22 +63,19 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke without any Blacklist Flags`() { fun `Get Joke without any Blacklist Flags`() {
val allFlags = Flag.values().filter { it != Flag.ALL }.toSet() val allFlags = Flag.entries.filter { it != Flag.ALL }.toSet()
val joke = joke(blacklistFlags = allFlags) val joke = joke(blacklistFlags = allFlags)
assertThat(joke::flags).isEmpty() assertThat(joke::flags).isEmpty()
} }
@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)
} }
} }
@ -138,7 +116,7 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with each Categories`() { fun `Get Joke with each Categories`() {
Category.values().filter { it != Category.ANY }.forEach { Category.entries.filter { it != Category.ANY }.forEach {
val joke = joke(categories = setOf(it)) val joke = joke(categories = setOf(it))
logger.fine(joke.toString()) logger.fine(joke.toString())
assertThat(joke::category, "joke($it)").prop(Category::value).isEqualTo(it.value) assertThat(joke::category, "joke($it)").prop(Category::value).isEqualTo(it.value)
@ -147,7 +125,7 @@ internal class GetJokeTest {
@Test @Test
fun `Get Joke with each Languages`() { fun `Get Joke with each Languages`() {
Language.values().forEach { Language.entries.forEach {
val joke = joke(lang = it) val joke = joke(lang = it)
logger.fine(joke.toString()) logger.fine(joke.toString())
assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value) assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value)
@ -156,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")
} }
@ -196,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:
@ -33,15 +33,7 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.*
import assertk.assertions.each
import assertk.assertions.index
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.models.Joke import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language import net.thauvin.erik.jokeapi.models.Language
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

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:
@ -33,25 +33,12 @@ package net.thauvin.erik.jokeapi
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.each import assertk.assertions.*
import assertk.assertions.isBetween import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThanOrEqualTo
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import net.thauvin.erik.jokeapi.JokeApi.joke import net.thauvin.erik.jokeapi.JokeApi.joke
import net.thauvin.erik.jokeapi.JokeApi.jokes import net.thauvin.erik.jokeapi.JokeApi.jokes
import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.logger import net.thauvin.erik.jokeapi.JokeApi.logger
import net.thauvin.erik.jokeapi.models.Category import net.thauvin.erik.jokeapi.models.*
import net.thauvin.erik.jokeapi.models.Flag
import net.thauvin.erik.jokeapi.models.Format
import net.thauvin.erik.jokeapi.models.IdRange
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Type
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertContains import kotlin.test.assertContains
@ -115,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
@ -167,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 @@
/* /*
* UtilTest.kt * JokeUtilTest.kt
* *
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net) * Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* *
* Redistribution and use in source and binary forms, with or without * 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\"")
} }
} }