Compare commits

..

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

75 changed files with 969 additions and 3059 deletions

View file

@ -1,62 +1,53 @@
version: 2.1
orbs:
sdkman: joshdholtz/sdkman@0.2.0
version: 2
defaults: &defaults
working_directory: ~/repo
environment:
JVM_OPTS: -Xmx3200m
TERM: dumb
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
CI: true
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:
bld_jdk17:
build_gradle_jdk17:
<<: *defaults
docker:
- image: cimg/openjdk:17.0
steps:
- build_and_test
<<: *defaults_gradle
bld_jdk20:
build_gradle_jdk11:
<<: *defaults
docker:
- image: cimg/openjdk:20.0
- image: cimg/openjdk:11.0
steps:
- build_and_test
<<: *defaults_gradle
workflows:
bld:
version: 2
gradle:
jobs:
- bld_jdk17
- bld_jdk20
- build_gradle_jdk11
- build_gradle_jdk17

View file

@ -1,51 +0,0 @@
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 }}

49
.github/workflows/gradle.yml vendored Normal file
View file

@ -0,0 +1,49 @@
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,57 +1,84 @@
.gradle
!.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
build
lib/bld/**
!lib/bld/bld-wrapper.jar
!lib/bld/bld-wrapper.properties
lib/compile/
lib/runtime/
lib/standalone/
lib/test/
# IDEA ignores
# User-specific
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
.classpath
.gradle
.history
.kobalt
.mtj.tmp/
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.nb-gradle
.project
.scannerwork
.settings
.vscode/*
/**/.idea/$CACHE_FILE$
/**/.idea/$PRODUCT_WORKSPACE_FILE$
/**/.idea/**/caches/build_file_checksums.ser
/**/.idea/**/contentModel.xml
/**/.idea/**/dataSources.ids
/**/.idea/**/dataSources.local.xml
/**/.idea/**/dataSources/
/**/.idea/**/dbnavigator.xml
/**/.idea/**/dictionaries
/**/.idea/**/dynamic.xml
/**/.idea/**/gradle.xml
/**/.idea/**/httpRequests
/**/.idea/**/libraries
/**/.idea/**/mongoSettings.xml
/**/.idea/**/replstate.xml
/**/.idea/**/shelf
/**/.idea/**/shelf/
/**/.idea/**/sqlDataSources.xml
/**/.idea/**/tasks.xml
/**/.idea/**/uiDesigner.xml
/**/.idea/**/usage.statistics.xml
/**/.idea/**/workspace.xml
/**/.idea/sonarlint*
/**/.idea_modules/
Thumbs.db
__pycache__
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Editor-based Rest Client
.idea/httpRequests
bin/
build/
cmake-build-*/
com_crashlytics_export_strings.xml
crashlytics-build.properties
crashlytics.properties
dependency-reduced-pom.xml
deploy/
dist/
ehthumbs.db
fabric.properties
gen/
hs_err_pid*
kobaltBuild
kobaltw*-test
lib/kotlin*
libs/
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
View file

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

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

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

View file

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

4
.idea/kotlinc.xml generated
View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +0,0 @@
<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,21 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0">
<entry_point TYPE="field" FQNAME="net.thauvin.erik.jokeapi.models.Category SPOOKY" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.jokeapi.JokeApi logger$delegate" />
</entry_points>
<pattern value="net.thauvin.erik.JokeApiBuild" method="jacoco" />
<pattern value="net.thauvin.erik.JokeApiBuild" method="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 name="ExternalStorageConfigurationManager" enabled="true" />
<component name="PDMPlugin">
<option name="skipTestSources" value="false" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

9
.idea/modules.xml generated
View file

@ -1,9 +0,0 @@
<?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-2025 Erik C. Thauvin (erik@thauvin.net)
Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

View file

@ -1,12 +1,11 @@
[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause)
[![Kotlin](https://img.shields.io/badge/kotlin-2.1.20-7f52ff)](https://kotlinlang.org/)
[![bld](https://img.shields.io/badge/2.2.1-FA9052?label=bld&labelColor=2392FF)](https://rife2.com/bld)
[![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-7f52ff)](https://kotlinlang.org/)
[![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/)
[![Release](https://img.shields.io/github/release/ethauvin/jokeapi.svg)](https://github.com/ethauvin/jokeapi/releases/latest)
[![Maven Central](https://img.shields.io/maven-central/v/net.thauvin.erik/jokeapi?color=blue)](https://central.sonatype.com/artifact/net.thauvin.erik/jokeapi)
[![Nexus Snapshot](https://img.shields.io/nexus/s/net.thauvin.erik/jokeapi?label=snapshot&server=https%3A%2F%2Foss.sonatype.org%2F)](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/jokeapi/)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_jokeapi&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_jokeapi)
[![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/bld.yml)
[![GitHub CI](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/jokeapi/actions/workflows/gradle.yml)
[![CircleCI](https://circleci.com/gh/ethauvin/jokeapi/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/jokeapi/tree/master)
# JokeAPI for Kotlin, Java and Android
@ -16,7 +15,7 @@ A simple library to retrieve jokes from [Sv443's JokeAPI](https://v2.jokeapi.dev
## Examples (TL;DR)
```kotlin
import net.thauvin.erik.jokeapi.joke
import net.thauvin.erik.jokeapi.getJoke
val joke = joke()
val safe = joke(safe = true)
@ -89,19 +88,6 @@ var config = new JokeConfig.Builder()
var joke = JokeApi.joke(config);
joke.getJoke().forEach(System.out::println);
```
## bld
To use with [bld](https://rife2.com/bld), include the following dependency in your build file:
```java
repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
scope(compile)
.include(dependency("net.thauvin.erik", "jokeapi", "1.0.0"));
```
Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
## Gradle, Maven, etc.
To use with [Gradle](https://gradle.org/), include the following dependency in your build file:
@ -112,7 +98,7 @@ repositories {
}
dependencies {
implementation("net.thauvin.erik:jokeapi:1.0.0")
implementation("net.thauvin.erik:jokeapi:0.9.0")
}
```
@ -124,10 +110,9 @@ You can also retrieve one or more raw (unprocessed) jokes in all [supported form
For example for YAML:
```kotlin
var jokes = getRawJokes(format = Format.YAML, idRange = IdRange(22))
println(jokes.data)
var joke = getRawJokes(format = Format.YAML, idRange = IdRange(22))
println(joke)
```
```yaml
error: false
category: "Programming"
@ -143,8 +128,8 @@ flags:
id: 22
safe: true
lang: "en"
```
```
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt#L46)...
## Extending
@ -154,37 +139,15 @@ A generic `apiCall()` function is available to access other [JokeAPI endpoints](
For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint):
```kotlin
val response = JokeApi.apiCall(
val lang = JokeApi.apiCall(
endPoint = "langcode",
path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)
)
if (response.statusCode == 200) {
println(response.data)
}
println(lang)
```
```yaml
error: false
code: "fr"
```
- View more [examples](https://github.com/ethauvin/jokeapi/blob/master/src/test/kotlin/net/thauvin/erik/jokeapi/ApiCallTest.kt#L48)...
## Contributing
If you want to contribute to this project, all you have to do is clone the GitHub
repository:
```console
git clone git@github.com:ethauvin/jokeapi.git
```
Then use [bld](https://rife2.com/bld) to build:
```console
cd jokeapi
./bld compile
```
The project has an [IntelliJ IDEA](https://www.jetbrains.com/idea/) project structure. You can just open it after all
the dependencies were downloaded and peruse the code.

View file

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,60 +0,0 @@
/*
* 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
View file

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

View file

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

185
build.gradle.kts Normal file
View file

@ -0,0 +1,185 @@
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" ?>
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>LongParameterList:JokeApi.kt$( amount: Int, categories: Set&lt;Category&gt; = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag&gt; = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeApi.kt$( 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&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&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&gt;, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID>
<ID>LongParameterList:JokeApi.kt$( amount: Int, categories: Set&lt;Category> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, contains: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, auth: String = "", splitNewLine: Boolean = false )</ID>
<ID>LongParameterList:JokeApi.kt$( categories: Set&lt;Category> = 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> = setOf(Category.ANY), lang: Language = Language.EN, blacklistFlags: Set&lt;Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, contains: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )</ID>
<ID>LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set&lt;Category>, val language: Language, val flags: Set&lt;Flag>, val type: Type, val format: Format, val contains: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )</ID>
<ID>LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List&lt;String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )</ID>
<ID>MagicNumber:JokeUtil.kt$200</ID>
<ID>MagicNumber:JokeUtil.kt$399</ID>
<ID>MagicNumber:JokeUtil.kt$400</ID>
@ -18,15 +18,6 @@
<ID>MagicNumber:JokeUtil.kt$500</ID>
<ID>MagicNumber:JokeUtil.kt$523</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: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>
</SmellBaseline>

1
gradle.properties Normal file
View file

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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

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

@ -0,0 +1,249 @@
#!/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 Normal file
View file

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

Binary file not shown.

View file

@ -1,10 +0,0 @@
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,12 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<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"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<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">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- 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>
<groupId>net.thauvin.erik</groupId>
<artifactId>jokeapi</artifactId>
<version>1.0.1-SNAPSHOT</version>
<version>0.9.0</version>
<name>jokeapi</name>
<description>Retrieve jokes from Sv443&apos;s JokeAPI</description>
<description>Retrieve jokes from Sv443's JokeAPI</description>
<url>https://github.com/ethauvin/jokeapi</url>
<licenses>
<license>
@ -14,26 +18,6 @@
<url>https://opensource.org/licenses/BSD-3-Clause</url>
</license>
</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>
<developer>
<id>ethauvin</id>
@ -47,4 +31,39 @@
<developerConnection>scm:git:git@github.com:ethauvin/jokeapi.git</developerConnection>
<url>https://github.com/ethauvin/jokeapi</url>
</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>

3
settings.gradle.kts Normal file
View file

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

View file

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

@ -1,197 +0,0 @@
/*
* 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
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -45,16 +45,13 @@ import java.util.stream.Collectors
object JokeApi {
private const val API_URL = "https://v2.jokeapi.dev/"
/**
* The logger instance.
*/
@JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
/**
* Makes a direct API call.
*
* See the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#endpoints) for more details.
*/
@JvmStatic
@JvmOverloads
@ -64,7 +61,7 @@ object JokeApi {
path: String = "",
params: Map<String, String> = emptyMap(),
auth: String = ""
): JokeResponse {
): String {
val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) {
@ -98,11 +95,11 @@ object JokeApi {
*/
@JvmStatic
@Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): JokeResponse {
fun getRawJokes(config: JokeConfig): String {
return rawJokes(
categories = config.categories,
lang = config.lang,
blacklistFlags = config.blacklistFlags,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
format = config.format,
contains = config.contains,
@ -124,8 +121,8 @@ object JokeApi {
fun joke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return joke(
categories = config.categories,
lang = config.lang,
blacklistFlags = config.blacklistFlags,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
@ -145,8 +142,8 @@ object JokeApi {
fun jokes(config: JokeConfig): Array<Joke> {
return jokes(
categories = config.categories,
lang = config.lang,
blacklistFlags = config.blacklistFlags,
lang = config.language,
blacklistFlags = config.flags,
type = config.type,
contains = config.contains,
idRange = config.idRange,
@ -164,32 +161,6 @@ object JokeApi {
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param categories JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
* about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING] category, dark
* humor will be in the [Category.DARK] category and so on. If you want jokes from all categories, you can instead use
* [Category.ANY], which will make JokeAPI randomly choose a category.
* @param lang There are two types of languages; system languages and joke languages. Both are separate from each other.
* All system messages like errors can have a certain system language, while jokes can only have a joke language.
* It is possible, that system languages don't yet exist for your language while jokes already do.
* If no suitable system language is found, JokeAPI will default to English.
* @param blacklistFlags Blacklist Flags (or just "Flags") are a more fine layer of filtering. Multiple flags can be
* set on each joke, and they tell you something about the offensiveness of each joke.
* @param type Each joke comes with one of two types: [Type.SINGLE] or [Type.TWOPART]. If a joke is of type
* [Type.TWOPART], it has a setup string and a delivery string, which are both part of the joke. They are separated
* because you might want to present the users the delivery after a timeout or in a different section of the UI.
* A joke of type [Type.SINGLE] only has a single string, which is the entire joke.
* @param contains If the search string filter is used, only jokes that contain the specified string will be returned.
* @param idRange If this filter is used, you will only get jokes that are within the provided range of IDs.
* You don't necessarily need to provide an ID range though, a single ID will work just fine as well.
* For example, an ID range of 0-9 will mean you will only get one of the first 10 jokes, while an ID range of 5 will
* mean you will only get the 6th joke.
* @param safe Safe Mode. If enabled, JokeAPI will try its best to serve only jokes that are considered safe for
* everyone. Unsafe jokes are those who can be considered explicit in any way, either through the used language, its
* references or its [flags][blacklistFlags]. Jokes from the category [Category.DARK] are also generally marked as
* unsafe.
* @param auth JokeAPI has a way of whitelisting certain clients. This is achieved through an API token.
* At the moment, you will only receive one of these tokens temporarily if something breaks or if you are a business
* and need more than 120 requests per minute.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun joke(
@ -213,7 +184,7 @@ fun joke(
idRange = idRange,
safe = safe,
auth = auth
).data
)
)
if (json.getBoolean("error")) {
throw parseError(json)
@ -227,35 +198,7 @@ fun joke(
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*
* @param amount This filter allows you to set a certain amount of jokes to receive in a single call. Setting the
* 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 amount The required amount of jokes to return.
* @param splitNewLine Split newline within [Type.SINGLE] joke.
*/
fun jokes(
@ -281,7 +224,7 @@ fun jokes(
amount = amount,
safe = safe,
auth = auth
).data
)
)
if (json.getBoolean("error")) {
throw parseError(json)
@ -298,42 +241,8 @@ fun jokes(
/**
* Returns one or more jokes.
*
* 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.
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@Throws(HttpErrorException::class)
fun rawJokes(
categories: Set<Category> = setOf(Category.ANY),
lang: Language = Language.EN,
@ -345,7 +254,7 @@ fun rawJokes(
amount: Int = 1,
safe: Boolean = false,
auth: String = ""
): JokeResponse {
): String {
val params = mutableMapOf<String, String>()
// Categories

View file

@ -1,7 +1,7 @@
/*
* JokeConfig.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -32,26 +32,31 @@
package net.thauvin.erik.jokeapi
import net.thauvin.erik.jokeapi.JokeConfig.Builder
import net.thauvin.erik.jokeapi.models.*
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(builder: Builder) {
val categories = builder.categories
val lang = builder.lang
val blacklistFlags = builder.blacklistFlags
val type = builder.type
val format = builder.format
val contains = builder.contains
val idRange = builder.idRange
val amount = builder.amount
val safe = builder.safe
val splitNewLine = builder.splitNewLine
val auth = builder.auth
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.
*
@ -72,86 +77,20 @@ class JokeConfig private constructor(builder: Builder) {
var splitNewLine: Boolean = false,
var auth: String = ""
) {
/**
* JokeAPI has a first, coarse filter that just categorizes the jokes depending on what the joke is
* about or who the joke is directed at. A joke about programming will be in the [Category.PROGRAMMING]
* category, dark humor will be in the [Category.DARK] category and so on. If you want jokes from all
* categories, you can instead use [Category.ANY], which will make JokeAPI randomly choose a category.
*/
fun categories(categories: Set<Category>): Builder = apply { this.categories = categories }
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 }
/**
* 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)
fun build() = JokeConfig(
categories, lang, blacklistFlags, type, format, contains, idRange, amount, safe, splitNewLine, auth
)
}
}

View file

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

View file

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

View file

@ -1,7 +1,7 @@
/*
* JokeException.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -29,8 +29,6 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@file:Suppress("ConstPropertyName")
package net.thauvin.erik.jokeapi.exceptions
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
/*
* ExceptionsTest.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -33,7 +33,16 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.*
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
@ -41,6 +50,8 @@ 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 {
@ -61,20 +72,19 @@ internal class ExceptionsTest {
}
}
@Test
fun `Validate HTTP Exceptions`() {
val locs = ArrayList<Pair<String, Int>>()
locs.add(Pair("https://apichallenges.herokuapp.com/secret/note", 401))
locs.add(Pair("https://apichallenges.herokuapp.com/todo", 404))
for ((url, code) in locs) {
val e = assertThrows<HttpErrorException> {
fetchUrl(url)
}
assertThat(e, "fetchUrl($code)").all {
prop(HttpErrorException::statusCode).isEqualTo(code)
prop(HttpErrorException::message).isNotNull().isNotEmpty()
}
@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

@ -1,7 +1,7 @@
/*
* GetJokeTest.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -33,10 +33,29 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.*
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.*
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
@ -63,19 +82,22 @@ internal class GetJokeTest {
@Test
fun `Get Joke without any Blacklist Flags`() {
val allFlags = Flag.entries.filter { it != Flag.ALL }.toSet()
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 = 201
val id = 172
val joke = joke(idRange = IdRange(id))
logger.fine(joke.toString())
assertThat(joke, "joke($id)").all {
prop(Joke::flags).contains(Flag.RELIGIOUS);
prop(Joke::id).isEqualTo(id)
prop(Joke::flags).all {
contains(Flag.EXPLICIT)
contains(Flag.NSFW)
}
prop(Joke::id).isEqualTo(172)
prop(Joke::category).isEqualTo(Category.PUN)
}
}
@ -116,7 +138,7 @@ internal class GetJokeTest {
@Test
fun `Get Joke with each Categories`() {
Category.entries.filter { it != Category.ANY }.forEach {
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)
@ -125,7 +147,7 @@ internal class GetJokeTest {
@Test
fun `Get Joke with each Languages`() {
Language.entries.forEach {
Language.values().forEach {
val joke = joke(lang = it)
logger.fine(joke.toString())
assertThat(joke::lang, "joke($it)").prop(Language::value).isEqualTo(it.value)
@ -134,10 +156,12 @@ internal class GetJokeTest {
@Test
fun `Get Joke with Split Newline`() {
val joke = joke(type = Type.SINGLE, idRange = IdRange(18), splitNewLine = true)
val joke = joke(
categories = setOf(Category.DARK), type = Type.SINGLE, idRange = IdRange(178), splitNewLine = true
)
logger.fine(joke.toString())
assertThat(joke::joke, "joke(splitNewLine=true)").all {
size().isGreaterThanOrEqualTo(2)
size().isEqualTo(2)
each {
containsNone("\n")
}
@ -172,12 +196,13 @@ internal class GetJokeTest {
@Test
fun `Get Joke using Search`() {
val search = "UDP joke"
val id = 265
val search = "his wife"
val joke =
joke(contains = search, categories = setOf(Category.PROGRAMMING), safe = true)
joke(contains = search, categories = setOf(Category.PROGRAMMING), idRange = IdRange(id), safe = true)
logger.fine(joke.toString())
assertThat(joke, "joke($search)").all {
prop(Joke::id).isEqualTo(0)
prop(Joke::id).isEqualTo(id)
prop(Joke::joke).any {
it.contains(search)
}

View file

@ -1,7 +1,7 @@
/*
* GetJokesTest.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -33,7 +33,15 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.*
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

View file

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

View file

@ -1,7 +1,7 @@
/*
* JokeConfigTest.kt
*
* Copyright 2022-2025 Erik C. Thauvin (erik@thauvin.net)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@ -33,12 +33,25 @@ package net.thauvin.erik.jokeapi
import assertk.all
import assertk.assertThat
import assertk.assertions.*
import net.thauvin.erik.jokeapi.JokeApi.getRawJokes
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.*
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
@ -102,9 +115,8 @@ internal class JokeConfigTest {
amount(2)
safe(true)
}.build()
val jokes = getRawJokes(config)
assertThat(jokes.statusCode).isEqualTo(200)
assertContains(jokes.data, "----------------------------------------------", false, "config.amount(2)")
val joke = getRawJokes(config)
assertContains(joke, "----------------------------------------------", false, "config.amount(2)")
}
@Test
@ -155,8 +167,8 @@ internal class JokeConfigTest {
}.build()
assertThat(config, "config").all {
prop(JokeConfig::categories).isEqualTo(categories)
prop(JokeConfig::lang).isEqualTo(language)
prop(JokeConfig::blacklistFlags).isEqualTo(flags)
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)

View file

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