From 94c0f45234a48119dc65189556db93ee60b681fb Mon Sep 17 00:00:00 2001 From: Konstantin Pavlov <1517853+kpavlov@users.noreply.github.com> Date: Sun, 21 Sep 2025 08:03:41 +0300 Subject: [PATCH] Add ShadowJar support and new OpenAI sample tests - Introduced `shadow-convention` in build scripts for multiple modules. - Enabled `shadow` publication in Maven configuration. - Added sample OpenAI `pom.xml` and example tests utilizing MockOpenai. - Added `httpStatusCode` with default value `200` in response classes and builders. --- .editorconfig | 15 +-- .github/workflows/gradle.yml | 11 ++- Makefile | 6 +- RELEASE.md | 2 +- ai-mocks-a2a/build.gradle.kts | 16 ++++ ai-mocks-anthropic/build.gradle.kts | 16 ++++ ai-mocks-gemini/build.gradle.kts | 16 ++++ ai-mocks-ollama/build.gradle.kts | 16 ++++ ai-mocks-openai/build.gradle.kts | 16 ++++ .../samples/OpenaiLc4jSample.ipynb | 75 +++++++-------- ai-mocks-openai/samples/shadow/pom.xml | 72 ++++++++++++++ .../src/test/java/com/example/OpenAITest.java | 94 +++++++++++++++++++ .../me/kpavlov/aimocks/openai/MockOpenai.kt | 1 + buildSrc/build.gradle.kts | 1 + .../main/kotlin/dokka-convention.gradle.kts | 2 +- .../main/kotlin/publish-convention.gradle.kts | 23 +++-- .../main/kotlin/shadow-convention.gradle.kts | 25 +++++ mokksy/build.gradle.kts | 16 ++++ .../response/AbstractResponseDefinition.kt | 4 +- .../mokksy/response/ResponseDefinition.kt | 15 +-- .../response/ResponseDefinitionBuilders.kt | 16 +++- .../response/StreamResponseDefinition.kt | 16 ++-- 22 files changed, 400 insertions(+), 74 deletions(-) create mode 100644 ai-mocks-openai/samples/shadow/pom.xml create mode 100644 ai-mocks-openai/samples/shadow/src/test/java/com/example/OpenAITest.java create mode 100644 buildSrc/src/main/kotlin/shadow-convention.gradle.kts diff --git a/.editorconfig b/.editorconfig index a6051bd4..d42d12d2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,13 +18,15 @@ insert_final_newline = true trim_trailing_whitespace = false [*.java] -indent_size = 4 +indent_size = tab +tab_width = 4 ij_java_names_count_to_use_import_on_demand = 2147483647 ij_java_packages_to_use_import_on_demand = unset # noinspection EditorConfigKeyCorrectness [*.{kt,kts}] -indent_size = 4 +indent_size = tab +tab_width = 4 max_line_length = 100 ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 @@ -36,8 +38,9 @@ ktlint_function_naming_ignore_when_annotated_with = "Test" max_line_length = off ij_kotlin_name_count_to_use_star_import = unset ij_kotlin_name_count_to_use_star_import_for_members = unset -ij_kotlin_packages_to_use_import_on_demand = kotlinx.serialization,kotlinx.serialization.descriptors,kotlinx.serialization.encoding +ij_kotlin_packages_to_use_import_on_demand = kotlinx.serialization, kotlinx.serialization.descriptors, kotlinx.serialization.encoding insert_final_newline = unset -# no-trailing-spaces: -# in -# standard:no-multi-spaces: 2 + +[*.xml] +tab_width = 4 +indent_size = tab diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 64c8e0ba..6704f3d9 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -54,7 +54,15 @@ jobs: caches/keyrings - name: Build with Gradle - run: gradle clean build dokkaGenerate dokkaJavadocJar sourcesJar koverXmlReport + run: | + ./gradlew \ + --rerun-tasks --no-daemon \ + -Dorg.gradle.maven.repo.local=.m2-local \ + clean build publishToMavenLocal koverXmlReport + + - name: OpenAI Maven Integration Tests + working-directory: ai-mocks-openai/samples/shadow + run: mvn --batch-mode test - name: Publish Test Report uses: mikepenz/action-junit-report@v5 @@ -81,6 +89,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} slug: mokksy/ai-mocks + dependency-submission: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 316e5569..03a5e4c9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ .PHONY: build build: - ./gradlew clean build dokkaJavadocJar sourcesJar koverHtmlReport + rm -rf ~/.m2/repository/me/kpavlov/aimocks ~/.m2/repository/me/kpavlov/mokksy && \ + ./gradlew --rerun-tasks clean build publishToMavenLocal koverHtmlReport && \ + (cd ai-mocks-openai/samples/shadow && mvn test) .PHONY: test test: @@ -43,7 +45,7 @@ pom: .PHONY: publish publish: rm -rf ~/.m2/repository/me/kpavlov/aimocks ~/.m2/repository/me/kpavlov/mokksy - ./gradlew clean build check sourcesJar publishToMavenLocal + ./gradlew --rerun-tasks clean build check sourcesJar publishToMavenLocal echo "Publishing 📢" ## https://vanniktech.github.io/gradle-maven-publish-plugin/central/#configuring-maven-central # ./gradlew publishToMavenCentral \ diff --git a/RELEASE.md b/RELEASE.md index 1b765c88..08ac2b21 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,7 +13,7 @@ SONATYPE_PASSWORD=... ./gradlew clean build sourcesJar check publishToMavenCentral \ - --stacktrace --warning-mode=all \ + --stacktrace --rerun-tasks --warning-mode=all \ -PmavenCentralUsername="$SONATYPE_USERNAME" \ -PmavenCentralPassword="$SONATYPE_PASSWORD" ``` diff --git a/ai-mocks-a2a/build.gradle.kts b/ai-mocks-a2a/build.gradle.kts index 5d519b08..380444bb 100644 --- a/ai-mocks-a2a/build.gradle.kts +++ b/ai-mocks-a2a/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `dokka-convention` `publish-convention` `netty-convention` + `shadow-convention` } dokka { @@ -63,3 +64,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/ai-mocks-anthropic/build.gradle.kts b/ai-mocks-anthropic/build.gradle.kts index c4c554bf..d8e00817 100644 --- a/ai-mocks-anthropic/build.gradle.kts +++ b/ai-mocks-anthropic/build.gradle.kts @@ -4,6 +4,7 @@ plugins { `kotlin-convention` `dokka-convention` `publish-convention` + `shadow-convention` } dokka { @@ -57,3 +58,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/ai-mocks-gemini/build.gradle.kts b/ai-mocks-gemini/build.gradle.kts index fd8793f3..dbb90024 100644 --- a/ai-mocks-gemini/build.gradle.kts +++ b/ai-mocks-gemini/build.gradle.kts @@ -4,6 +4,7 @@ plugins { `kotlin-convention` `dokka-convention` `publish-convention` + `shadow-convention` } dokka { @@ -56,3 +57,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/ai-mocks-ollama/build.gradle.kts b/ai-mocks-ollama/build.gradle.kts index 5f3392e2..2dbf511c 100644 --- a/ai-mocks-ollama/build.gradle.kts +++ b/ai-mocks-ollama/build.gradle.kts @@ -4,6 +4,7 @@ plugins { `kotlin-convention` `dokka-convention` `publish-convention` + `shadow-convention` } kotlin { @@ -62,3 +63,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/ai-mocks-openai/build.gradle.kts b/ai-mocks-openai/build.gradle.kts index 987806ed..59e8d76f 100644 --- a/ai-mocks-openai/build.gradle.kts +++ b/ai-mocks-openai/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `dokka-convention` `publish-convention` `netty-convention` + `shadow-convention` // id("org.openapi.generator") version "7.12.0" } @@ -59,3 +60,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/ai-mocks-openai/samples/OpenaiLc4jSample.ipynb b/ai-mocks-openai/samples/OpenaiLc4jSample.ipynb index 733ef23d..f40d89d1 100644 --- a/ai-mocks-openai/samples/OpenaiLc4jSample.ipynb +++ b/ai-mocks-openai/samples/OpenaiLc4jSample.ipynb @@ -10,8 +10,8 @@ "metadata": { "collapsed": true, "ExecuteTime": { - "end_time": "2025-09-19T19:49:09.868118Z", - "start_time": "2025-09-19T19:49:05.225077Z" + "end_time": "2025-09-21T08:33:23.068468Z", + "start_time": "2025-09-21T08:33:22.912180Z" } }, "source": [ @@ -23,21 +23,21 @@ " mavenCentral()\n", " }\n", " dependencies(\n", - " \"me.kpavlov.aimocks:ai-mocks-openai-jvm:0.5.0-SNAPSHOT\",\n", + " \"me.kpavlov.aimocks:ai-mocks-openai-standalone:0.5.0-SNAPSHOT\",\n", " \"io.kotest:kotest-assertions-core:5.9.1\",\n", - " \"io.kotest:kotest-assertions-json:5.9.1\",\n", " \"dev.langchain4j:langchain4j-open-ai:1.5.0\",\n", + " \"dev.langchain4j:langchain4j-kotlin:1.5.0-beta11\",\n", " )\n", "}" ], "outputs": [], - "execution_count": 10 + "execution_count": 3 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-09-19T19:49:09.960056Z", - "start_time": "2025-09-19T19:49:09.877363Z" + "end_time": "2025-09-21T08:33:27.746433Z", + "start_time": "2025-09-21T08:33:27.667009Z" } }, "cell_type": "code", @@ -51,7 +51,7 @@ "import io.kotest.matchers.shouldBe\n", "import io.kotest.matchers.shouldNotBe\n", "\n", - "val openai = MockOpenai(verbose = true)\n", + "val openai = MockOpenai(verbose = true, port = 0)\n", "\n", "val model: OpenAiChatModel =\n", " OpenAiChatModel\n", @@ -61,8 +61,8 @@ " .build()\n", "\n", "openai.completion {\n", - " temperature = 0.42\n", - " seed = 100500\n", + "// temperature(0.42) = 0.42\n", + "// seed = 100500\n", " model = \"4o\"\n", " maxTokens = 120\n", "} responds {\n", @@ -96,31 +96,34 @@ ], "outputs": [ { - "ename": "java.lang.NoClassDefFoundError", - "evalue": "io/ktor/server/plugins/contentnegotiation/ContentNegotiationConfig", + "ename": "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException", + "evalue": "at Cell In[4], line 3, column 35: Unresolved reference: chat\nat Cell In[4], line 10, column 14: None of the following functions can be called with the arguments supplied: \npublic constructor MockOpenai() defined in me.kpavlov.aimocks.openai.MockOpenai\npublic constructor MockOpenai(port: Int, verbose: Boolean) defined in me.kpavlov.aimocks.openai.MockOpenai\nat Cell In[4], line 20, column 3: Unresolved reference: temperature\nat Cell In[4], line 21, column 3: Unresolved reference: seed\nat Cell In[4], line 22, column 3: Val cannot be reassigned\nat Cell In[4], line 22, column 11: Type mismatch: inferred type is String but OpenAiChatModel was expected\nat Cell In[4], line 23, column 3: Unresolved reference: maxTokens\nat Cell In[4], line 25, column 3: Unresolved reference: assistantContent\nat Cell In[4], line 26, column 3: Unresolved reference: finishReason\nat Cell In[4], line 32, column 11: None of the following functions can be called with the arguments supplied: \npublic open fun chat(p0: ChatRequest!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel\npublic open fun chat(vararg p0: ChatMessage!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel\npublic open fun chat(p0: String!): String! defined in dev.langchain4j.model.openai.OpenAiChatModel\npublic open fun chat(p0: (Mutable)List!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel\nat Cell In[4], line 33, column 7: Unresolved reference: parameters\nat Cell In[4], line 41, column 7: Unresolved reference: messages\nat Cell In[4], line 41, column 16: Unresolved reference: +=\nat Cell In[4], line 45, column 5: Unresolved reference: finishReason\nat Cell In[4], line 46, column 5: Unresolved reference: tokenUsage\nat Cell In[4], line 47, column 5: Unresolved reference: aiMessage", "output_type": "error", "traceback": [ - "java.lang.NoClassDefFoundError: io/ktor/server/plugins/contentnegotiation/ContentNegotiationConfig", - "\tat me.kpavlov.aimocks.openai.MockOpenai.(MockOpenai.kt:33)", - "\tat me.kpavlov.aimocks.openai.MockOpenai.(MockOpenai.kt:27)", - "\tat Line_24_jupyter.(Line_24.jupyter.kts:10) at Cell In[11], line 10", - "\tat java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)", - "\tat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)", - "\tat java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:483)", - "\tat kotlin.script.experimental.jvm.BasicJvmScriptEvaluator.evalWithConfigAndOtherScriptsResults(BasicJvmScriptEvaluator.kt:122)", - "\tat kotlin.script.experimental.jvm.BasicJvmScriptEvaluator.invoke$suspendImpl(BasicJvmScriptEvaluator.kt:48)", - "\tat kotlin.script.experimental.jvm.BasicJvmScriptEvaluator.invoke(BasicJvmScriptEvaluator.kt)", - "\tat kotlin.script.experimental.jvm.BasicJvmReplEvaluator.eval(BasicJvmReplEvaluator.kt:49)", - "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl$eval$resultWithDiagnostics$1.invokeSuspend(InternalEvaluatorImpl.kt:137)", - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)", - "\tat kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)", - "\tat kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:263)", - "\tat kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)", - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)", - "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)", - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:47)", - "\tat kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)", - "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:137)", + "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[4], line 3, column 35: Unresolved reference: chat", + "at Cell In[4], line 10, column 14: None of the following functions can be called with the arguments supplied: ", + "public constructor MockOpenai() defined in me.kpavlov.aimocks.openai.MockOpenai", + "public constructor MockOpenai(port: Int, verbose: Boolean) defined in me.kpavlov.aimocks.openai.MockOpenai", + "at Cell In[4], line 20, column 3: Unresolved reference: temperature", + "at Cell In[4], line 21, column 3: Unresolved reference: seed", + "at Cell In[4], line 22, column 3: Val cannot be reassigned", + "at Cell In[4], line 22, column 11: Type mismatch: inferred type is String but OpenAiChatModel was expected", + "at Cell In[4], line 23, column 3: Unresolved reference: maxTokens", + "at Cell In[4], line 25, column 3: Unresolved reference: assistantContent", + "at Cell In[4], line 26, column 3: Unresolved reference: finishReason", + "at Cell In[4], line 32, column 11: None of the following functions can be called with the arguments supplied: ", + "public open fun chat(p0: ChatRequest!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel", + "public open fun chat(vararg p0: ChatMessage!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel", + "public open fun chat(p0: String!): String! defined in dev.langchain4j.model.openai.OpenAiChatModel", + "public open fun chat(p0: (Mutable)List!): ChatResponse! defined in dev.langchain4j.model.openai.OpenAiChatModel", + "at Cell In[4], line 33, column 7: Unresolved reference: parameters", + "at Cell In[4], line 41, column 7: Unresolved reference: messages", + "at Cell In[4], line 41, column 16: Unresolved reference: +=", + "at Cell In[4], line 45, column 5: Unresolved reference: finishReason", + "at Cell In[4], line 46, column 5: Unresolved reference: tokenUsage", + "at Cell In[4], line 47, column 5: Unresolved reference: aiMessage", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.JupyterCompilerImpl.compileSync(JupyterCompilerImpl.kt:151)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:126)", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl.execute_L4Nmkdk$lambda$0$0(CellExecutorImpl.kt:80)", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withHost(ReplForJupyterImpl.kt:791)", "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl.execute-L4Nmkdk(CellExecutorImpl.kt:78)", @@ -147,13 +150,11 @@ "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$Task.execute(JupyterExecutorImpl.kt:41)", "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl.executorThread$lambda$0(JupyterExecutorImpl.kt:81)", "\tat kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)", - "", - "java.lang.NoClassDefFoundError: io/ktor/server/plugins/contentnegotiation/ContentNegotiationConfig", - "at Cell In[11], line 10" + "" ] } ], - "execution_count": 11 + "execution_count": 4 } ], "metadata": { diff --git a/ai-mocks-openai/samples/shadow/pom.xml b/ai-mocks-openai/samples/shadow/pom.xml new file mode 100644 index 00000000..2d82c318 --- /dev/null +++ b/ai-mocks-openai/samples/shadow/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + com.example + mokksy-openai-shadowed + 1.0.0-SNAPSHOT + + + 17 + ${java.version} + + + + + org.jetbrains.kotlin + kotlin-bom + 1.9.25 + pom + import + + + + + + dev.langchain4j + langchain4j-open-ai + 1.5.0 + + + com.openai + openai-java-client-okhttp + 3.1.2 + + + org.slf4j + slf4j-simple + 2.0.17 + runtime + + + org.assertj + assertj-core + 3.27.4 + test + + + me.kpavlov.aimocks + ai-mocks-openai-standalone + 0.5.0-SNAPSHOT + test + + + org.junit.jupiter + junit-jupiter-api + 5.13.4 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + + diff --git a/ai-mocks-openai/samples/shadow/src/test/java/com/example/OpenAITest.java b/ai-mocks-openai/samples/shadow/src/test/java/com/example/OpenAITest.java new file mode 100644 index 00000000..e758f8b4 --- /dev/null +++ b/ai-mocks-openai/samples/shadow/src/test/java/com/example/OpenAITest.java @@ -0,0 +1,94 @@ +package com.example; + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.core.JsonValue; +import com.openai.errors.OpenAIInvalidDataException; +import com.openai.models.ChatModel; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +import com.openai.models.chat.completions.ChatCompletionUserMessageParam; +import me.kpavlov.aimocks.openai.MockOpenai; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class OpenAITest { + + private static final MockOpenai MOCK_OPENAI = new MockOpenai(); + + private static final Random RANDOM = new Random(); + + private static final OpenAIClient CLIENT = OpenAIOkHttpClient.builder() + .apiKey("demo") + .baseUrl(MOCK_OPENAI.baseUrl()) + .build(); + + private double temperature; + private long maxTokens; + + @BeforeEach + void beforeEach() { + temperature = RANDOM.nextDouble(0.0, 1.0); + maxTokens = RANDOM.nextLong(100, 500); + } + + @Test + void shouldRespondToChatCompletion() { + MOCK_OPENAI.completion(req -> { + req.temperature(temperature); + req.model("gpt-4o-mini"); + req.maxTokens(maxTokens); + req.requestBodyContains("say 'Hey!'"); + }).responds(response -> { + response.assistantContent("Hey!"); + response.finishReason("stop"); + response.delayMillis(42); + }); + + final ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() + .temperature(temperature) + .maxCompletionTokens(maxTokens) + .messages( + List.of(ChatCompletionMessageParam.ofUser( + ChatCompletionUserMessageParam.builder() + .role(JsonValue.from("user")) + .content("Just say 'Hey!'").build()))) + .model(ChatModel.GPT_4O_MINI) + .build(); + + final var result = CLIENT.chat().completions().create(params); + + assertThat(result.choices().get(0).message().content()).hasValue("Hey!"); + } + + @Test + void shouldRespondToChatCompletionWithError() { + MOCK_OPENAI.completion(req -> { + req.temperature(temperature); + req.maxTokens(maxTokens); + }).respondsError(response -> { + response.setBody("Ahh, ohh!"); + response.setHttpStatusCode(500); + }); + + final ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() + .temperature(temperature) + .maxCompletionTokens(maxTokens) + .messages( + List.of(ChatCompletionMessageParam.ofUser( + ChatCompletionUserMessageParam.builder() + .role(JsonValue.from("user")) + .content("Just say 'Hello!'").build()))) + .model(ChatModel.GPT_4O_MINI) + .build(); + + assertThatExceptionOfType(OpenAIInvalidDataException.class) + .isThrownBy(() -> CLIENT.chat().completions().create(params)); + } +} diff --git a/ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/MockOpenai.kt b/ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/MockOpenai.kt index 65578d8d..e21618cb 100644 --- a/ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/MockOpenai.kt +++ b/ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/MockOpenai.kt @@ -31,6 +31,7 @@ public open class MockOpenai( port = port, configuration = ServerConfiguration( + name = "MockOpenai", verbose = verbose, ) { config -> config.json( diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index d32ad4ca..ecd51ad4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21") implementation("com.vanniktech:gradle-maven-publish-plugin:0.34.0") implementation("org.jetbrains.dokka:dokka-gradle-plugin:2.0.0") + implementation("com.gradleup.shadow:shadow-gradle-plugin:9.1.0") } diff --git a/buildSrc/src/main/kotlin/dokka-convention.gradle.kts b/buildSrc/src/main/kotlin/dokka-convention.gradle.kts index 332a54a1..9b9ead6b 100644 --- a/buildSrc/src/main/kotlin/dokka-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/dokka-convention.gradle.kts @@ -20,7 +20,7 @@ dokka { url("https://api.ktor.io/ktor-client/") packageListUrl("https://api.ktor.io/package-list") } - + externalDocumentationLinks.register("kotlinx-coroutines") { url("https://kotlinlang.org/api/kotlinx.coroutines/") packageListUrl("https://kotlinlang.org/api/kotlinx.coroutines/package-list") diff --git a/buildSrc/src/main/kotlin/publish-convention.gradle.kts b/buildSrc/src/main/kotlin/publish-convention.gradle.kts index 9e6216ee..a9f1258e 100644 --- a/buildSrc/src/main/kotlin/publish-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/publish-convention.gradle.kts @@ -43,15 +43,15 @@ configure { ) pom { - name.set(project.name) - description.set(project.description) - url.set("https://github.com/mokksy/ai-mocks") - inceptionYear.set("2025") + name = project.name + description = project.description + url = "https://mokksy.dev" + inceptionYear = "2025" licenses { license { - name.set("MIT License") - url.set("https://opensource.org/licenses/MIT") + name = "MIT License" + url = "https://opensource.org/licenses/MIT" } } @@ -65,9 +65,14 @@ configure { } scm { - connection.set("scm:git:git://github.com/mokksy/ai-mocks.git") - developerConnection.set("scm:git:ssh://github.com/mokksy/ai-mocks.git") - url.set("https://github.com/mokksy/ai-mocks") + connection = "scm:git:git://github.com/mokksy/ai-mocks.git" + developerConnection = "scm:git:ssh://github.com/mokksy/ai-mocks.git" + url = "https://github.com/mokksy/ai-mocks" + } + + issueManagement { + url = "https://github.com/mokksy/ai-mocks/issues" + system = "GitHub" } } } diff --git a/buildSrc/src/main/kotlin/shadow-convention.gradle.kts b/buildSrc/src/main/kotlin/shadow-convention.gradle.kts new file mode 100644 index 00000000..ec85a55e --- /dev/null +++ b/buildSrc/src/main/kotlin/shadow-convention.gradle.kts @@ -0,0 +1,25 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar + +plugins { + `maven-publish` + id("org.gradle.base") + id("com.gradleup.shadow") // https://gradleup.com/shadow +} + +tasks.shadowJar { + + minimize { + exclude(dependency("kotlin:.*:.*")) + exclude(dependency("org.jetbrains.kotlin:.*:.*")) + exclude(dependency("org.jetbrains.kotlinx:kotlinx-coroutines-.*:.*")) + } + + dependencies { + relocate("io.ktor", "dev.mokksy.relocated.io.ktor") + relocate("kotlinx", "dev.mokksy.relocated.kotlinx") + } +} + +tasks.assemble { + dependsOn(tasks.shadowJar) +} diff --git a/mokksy/build.gradle.kts b/mokksy/build.gradle.kts index ceea1945..41ee9447 100644 --- a/mokksy/build.gradle.kts +++ b/mokksy/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `dokka-convention` `publish-convention` `netty-convention` + `shadow-convention` } dokka { @@ -62,3 +63,18 @@ kotlin { } } } + +publishing { + publications { + create("shadow") { + artifactId = "${project.name}-standalone" + artifact(tasks.named("shadowJar")) { + classifier = "" + extension = "jar" + } + artifact(tasks["jvmSourcesJar"]) { + classifier = "sources" + } + } + } +} diff --git a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/AbstractResponseDefinition.kt b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/AbstractResponseDefinition.kt index 58a0dd84..fd2ea903 100644 --- a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/AbstractResponseDefinition.kt +++ b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/AbstractResponseDefinition.kt @@ -17,13 +17,15 @@ internal typealias ResponseDefinitionSupplier = ( * * @param T The type of the response data. * @property contentType The MIME type of the response content. Defaults to `null`. + * @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200. * @property httpStatus The HTTP status code of the response. Defaults to [HttpStatusCode.OK]. * @property headers A lambda function for configuring the response headers. Defaults to `null`. * @property headerList A list of header key-value pairs to populate the response headers. Defaults to an empty list. */ public abstract class AbstractResponseDefinition( public val contentType: ContentType? = null, - public val httpStatus: HttpStatusCode = HttpStatusCode.OK, + public val httpStatusCode: Int = 200, + public val httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode), public val headers: (ResponseHeaders.() -> Unit)? = null, public val headerList: List> = emptyList(), public open val delay: Duration = Duration.ZERO, diff --git a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinition.kt b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinition.kt index e1087d88..edbeafc6 100644 --- a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinition.kt +++ b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinition.kt @@ -18,6 +18,7 @@ import kotlin.time.Duration * @param T The type of the response body. * @property contentType The MIME type of the response content with a default to [ContentType.Application.Json]. * @property body The body of the response, which can be null. + * @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200. * @property httpStatus The HTTP status code of the response, defaulting to [HttpStatusCode.OK]. * @property headers A lambda function for configuring additional response headers using [ResponseHeaders]. * Defaults to null. @@ -27,16 +28,18 @@ import kotlin.time.Duration public open class ResponseDefinition( contentType: ContentType = ContentType.Application.Json, public val body: T? = null, - httpStatus: HttpStatusCode = HttpStatusCode.OK, + httpStatusCode: Int = 200, + httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode), headers: (ResponseHeaders.() -> Unit)? = null, headerList: List> = emptyList>(), delay: Duration, ) : AbstractResponseDefinition( - contentType, - httpStatus, - headers, - headerList, - delay, + contentType = contentType, + httpStatusCode = httpStatusCode, + httpStatus = httpStatus, + headers = headers, + headerList = headerList, + delay = delay, ) { override suspend fun writeResponse( call: ApplicationCall, diff --git a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinitionBuilders.kt b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinitionBuilders.kt index f21cad06..e125b460 100644 --- a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinitionBuilders.kt +++ b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/ResponseDefinitionBuilders.kt @@ -20,7 +20,8 @@ import kotlin.time.Duration.Companion.milliseconds * @property headers A mutable list of header key-value pairs to be included in the response. */ public abstract class AbstractResponseDefinitionBuilder( - public var httpStatus: HttpStatusCode, + public var httpStatusCode: Int = 200, + public var httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode), public val headers: MutableList>, public var delay: Duration = Duration.ZERO, ) { @@ -68,7 +69,8 @@ public abstract class AbstractResponseDefinitionBuilder( * @property contentType Optional MIME type of the response. * Defaults to `ContentType.Application.Json` if not specified. * @property body The body of the response. Can be null. - * @param httpStatus The HTTP status code of the response. Defaults to `HttpStatusCode.OK`. + * @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200. + * @property httpStatus The HTTP status code of the response, defaulting to [HttpStatusCode.OK]. * @param headers A mutable list of additional custom headers for the response. * * Inherits functionality from [AbstractResponseDefinitionBuilder] to allow additional header manipulations @@ -78,13 +80,19 @@ public open class ResponseDefinitionBuilder

( public val request: CapturedRequest

, public var contentType: ContentType? = null, public var body: T? = null, - httpStatus: HttpStatusCode = HttpStatusCode.OK, + httpStatusCode: Int = 200, + httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode), headers: MutableList> = mutableListOf(), -) : AbstractResponseDefinitionBuilder(httpStatus = httpStatus, headers = headers) { +) : AbstractResponseDefinitionBuilder( + httpStatusCode = httpStatusCode, + httpStatus = httpStatus, + headers = headers, + ) { public override fun build(): ResponseDefinition = ResponseDefinition( body = body, contentType = contentType ?: ContentType.Application.Json, + httpStatusCode = httpStatusCode, httpStatus = httpStatus, headers = headersLambda, headerList = Collections.unmodifiableList(headers), diff --git a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/StreamResponseDefinition.kt b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/StreamResponseDefinition.kt index 22d0efb6..a75cdbcb 100644 --- a/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/StreamResponseDefinition.kt +++ b/mokksy/src/commonMain/kotlin/me/kpavlov/mokksy/response/StreamResponseDefinition.kt @@ -34,6 +34,8 @@ internal const val SEND_BUFFER_CAPACITY = 256 * @property chunkFlow A `Flow` of chunks to be streamed as part of the response. * @property chunks A list of chunks representing the response data to be sent. * @property delayBetweenChunks Delay between the transmission of each chunk. + * @property httpStatusCode The HTTP status code of the response as Int, defaulting to 200. + * @property httpStatus The HTTP status code of the response, defaulting to [HttpStatusCode.OK]. * @constructor Initializes a streaming response definition with the specified flow, chunk list, content type, * HTTP status code, and headers. * @@ -46,16 +48,18 @@ public open class StreamResponseDefinition( public val chunks: List? = null, public val delayBetweenChunks: Duration = Duration.ZERO, contentType: ContentType = ContentType.Text.EventStream.withCharset(Charsets.UTF_8), - httpStatus: HttpStatusCode = HttpStatusCode.OK, + httpStatusCode: Int = 200, + httpStatus: HttpStatusCode = HttpStatusCode.fromValue(httpStatusCode), headers: (ResponseHeaders.() -> Unit)? = null, headerList: List> = emptyList>(), delay: Duration, ) : AbstractResponseDefinition( - contentType, - httpStatus, - headers, - headerList, - delay, + contentType = contentType, + httpStatusCode = httpStatusCode, + httpStatus = httpStatus, + headers = headers, + headerList = headerList, + delay = delay, ) { internal suspend fun writeChunksFromFlow( writer: ByteWriteChannel,