Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/client-configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ factory hook forwarded by generated clients:
| --- | --- | --- |
| Kotlin | `2.0.0-beta.16` | Maven Central |
| Swift | `2.0.0-beta.16` | Swift Package Manager Git tag |
| TypeScript | `2.0.0-beta.11` | npm (`@outfoxx/sunday`) |
| Python | `2.0.0-beta.10` | Git tag installed with pip; PyPI publication remains disabled |
| TypeScript | `2.0.0-beta.12` | npm (`@outfoxx/sunday`) |
| Python | `2.0.0-beta.11` | Git tag installed with pip; PyPI publication remains disabled |

Kotlin/Swift beta.13, TypeScript beta.10 and Python beta.9 do not provide this hook. Consumers generating
configuration factories must update their runtime dependencies to the compatible releases above or later.
Expand Down
29 changes: 27 additions & 2 deletions docs/server-resource-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ JSON `format: byte` values and binary fields inside structured models continue t
This output requires Sunday Python `2.0.0-beta.2` or later, which provides `request_bytes`. Until PyPI publication, install the Litestar extra from the released Git tag:

```sh
python -m pip install 'sunday-python[litestar] @ git+https://github.com/outfoxx/sunday-python.git@2.0.0-beta.10'
python -m pip install 'sunday-python[litestar] @ git+https://github.com/outfoxx/sunday-python.git@2.0.0-beta.11'
```

Compiler-backed tests use `2.0.0-beta.10` by default, including directional model validation and merge-patch support. To verify a local runtime change, set `SUNDAY_PYTHON_PATH` to its checkout:
Compiler-backed tests use `2.0.0-beta.11` by default, including directional model validation and merge-patch support. To verify a local runtime change, set `SUNDAY_PYTHON_PATH` to its checkout:

```sh
SUNDAY_PYTHON_PATH=/path/to/sunday-python ./gradlew :generator:test --tests '*PythonContentTypeTest'
Expand Down Expand Up @@ -108,3 +108,28 @@ The runtime fixtures generate source from OpenAPI and RAML contracts during the
Jersey exercises the standard JAX-RS request pipeline. Quarkus runs HTTP requests against the CDI-managed aggregate and verifies public access, anonymous 401 responses, and authenticated delegation. Litestar tests verify middleware exclusions, protected access, and a fail-closed guard when no authentication middleware supplies a user.

Content-Type fixtures additionally verify raw binary delegation, exact and wildcard media restrictions, strict and tolerant enum conversion, constants, defaults and missing headers. Compiler-backed AsyncAPI and composed cases reuse an enum for header binding and an `allOf` child restriction, preserving the behavior fixed by #217.

### Whole-body binary client responses

Python clients emit `ResponseSpec(binary_body=True)` for whole-body `file`/`binary` schemas, including named scalar aliases and unions whose branches are all binary.
Sunday Python `2.0.0-beta.11` or later is required; beta.10 and earlier do not support this flag. It bypasses media decoding so
JSON/text assets and empty downloads retain their bytes, while successful status/media checks and typed headers remain
active. Structured models and encoded byte properties retain normal decoding.

Compiler-backed tests install the published `2.0.0-beta.11` Git tag by default. Run without `SUNDAY_PYTHON_PATH` to verify
the released runtime rather than a local checkout. PyPI publication remains disabled; install from the Git tag as shown
above. Regenerate consuming clients after the generator release; a runtime update alone cannot identify binary bodies
in previously generated clients.

TypeScript clients likewise emit `RequestSpec.binaryResponse: true` for whole-body binary schemas, aliases, and
all-binary unions. Sunday TypeScript `2.0.0-beta.12` or later is required; earlier versions do not support this metadata.
The compiler fixture pins the published npm package `@outfoxx/sunday@2.0.0-beta.12` and its integrity-checked tarball.
Run without `SUNDAY_TYPESCRIPT_PATH` to verify the released runtime rather than a local checkout. Regenerate consuming
clients after the generator release; updating the runtime alone cannot add metadata to older clients.

Compiler-backed OpenAPI and RAML download fixtures execute the generated Kotlin, Swift, and TypeScript clients.
They verify exact bytes for direct binary schemas, aliases, unions, and nested unions across JSON, text, image, and
custom media types through both operation result APIs. Mixed unions retain media decoding. Kotlin and Swift beta.16
preserve nonempty binary downloads by their native `ByteArray`/`Data` return types, but deliberately reject empty typed
responses with their no-data errors. TypeScript and Python accept empty successful 200 binary bodies. These empty-body
contracts are tested explicitly; byte-preservation parity does not imply identical empty-body behavior.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.outfoxx.sunday.generator.ir.GeneratedStreaming
import io.outfoxx.sunday.generator.ir.GeneratedTypeRef
import io.outfoxx.sunday.generator.ir.emit.GeneratedClientSecurity
import io.outfoxx.sunday.generator.ir.emit.GeneratedModelProperties
import io.outfoxx.sunday.generator.ir.emit.GeneratedNominalTypes
import io.outfoxx.sunday.generator.ir.emit.credentialTransport
import io.outfoxx.sunday.generator.ir.emit.defaultMediaSelection
Expand All @@ -50,6 +51,7 @@

private val modelIndex = models.associateBy { model -> model.name }
private val nominalTypes = GeneratedNominalTypes { modelIndex[it.name] }
private val modelProperties = GeneratedModelProperties { modelIndex[it.name] }

private var adapterNames = emptyMap<Pair<String, String>, String>()

Expand Down Expand Up @@ -631,6 +633,9 @@
}
if (hasBody()) {
add(PythonCodeBlock.of("decoder=%L.validate_python", checkNotNull(type).adapterName()))
if (checkNotNull(type).isBinaryBody()) {

Check warning on line 636 in generator/src/main/kotlin/io/outfoxx/sunday/generator/python/PythonClientRenderer.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless non-null check checkNotNull, it always succeeds.

See more on https://sonarcloud.io/project/issues?id=outfoxx_sunday-generator&issues=AaFCMzwgeuMi53AKCTpR&open=AaFCMzwgeuMi53AKCTpR&pullRequest=275
add(PythonCodeBlock.of("binary_body=True"))
}
} else {
add(PythonCodeBlock.of("body_expected=False"))
}
Expand All @@ -655,6 +660,23 @@
}
}

private fun GeneratedTypeRef.isBinaryBody(visited: Set<GeneratedTypeRef> = emptySet()): Boolean {
if (this in visited) return false
val declaration = modelProperties.declarationType(this)
val model = modelProperties.declarationModel(declaration)
val alternatives =
when {
declaration.kind == GeneratedTypeRef.Kind.UNION -> declaration.arguments
model?.kind == GeneratedModel.Kind.UNION -> model.aliases
else -> emptyList()
}
if (alternatives.isNotEmpty()) {
return alternatives.all { it.isBinaryBody(visited + this) }
}
return declaration.kind == GeneratedTypeRef.Kind.SCALAR &&
(declaration.name in setOf("file", "binary") || declaration.format.equals("binary", ignoreCase = true))
}

private fun GeneratedResponse.renderResponseHeaderSpecs(): PythonCodeBlock {
if (headers.isEmpty()) {
return PythonCodeBlock.of("()")
Expand Down Expand Up @@ -709,7 +731,7 @@
PythonCodeBlock.of("()")
} else {
PythonCodeBlock.of(
"(%C,)",
if (mediaTypes.size == 1) "(%C,)" else "(%C)",
PythonCodeBlock.join(
mediaTypes.map { mediaType ->
PythonCodeBlock.of("%T(%S)", PythonSymbol("sunday", "MediaType"), mediaType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4038,7 +4038,24 @@
?: (typeName as? TypeName.Standard)?.simpleName()
?: typeName.toString()

private fun GeneratedTypeRef.isBinaryBody(visited: Set<GeneratedTypeRef> = emptySet()): Boolean {
if (this in visited) return false
val declaration = modelProperties.declarationType(this)
val model = modelProperties.declarationModel(declaration)
val alternatives =
when {
declaration.kind == GeneratedTypeRef.Kind.UNION -> declaration.arguments
model?.kind == GeneratedModel.Kind.UNION -> model.aliases
else -> emptyList()
}
if (alternatives.isNotEmpty()) {
return alternatives.all { it.isBinaryBody(visited + this) }
}
return declaration.kind == GeneratedTypeRef.Kind.SCALAR &&
(declaration.name in setOf("file", "binary") || declaration.format.equals("binary", ignoreCase = true))
}

private fun GeneratedOperation.spec(

Check failure on line 4058 in generator/src/main/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptSundayIrGenerator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=outfoxx_sunday-generator&issues=AaFCcEDJvRfLn9isiZRk&open=AaFCcEDJvRfLn9isiZRk&pullRequest=275
serviceTypeName: TypeName.Standard,
returnType: TypeName,
requestBodyTypeProperty: String?,
Expand Down Expand Up @@ -4083,6 +4100,10 @@
builder.add("\ncontentTypes: %L,", requestBody.contentTypes(contentTypeParameter))
}

if (response?.type?.isBinaryBody() == true && returnType != VOID) {
builder.add("\nbinaryResponse: true,")
}

val acceptTypes = response?.mediaTypes?.ifEmpty { defaultMediaTypes } ?: emptyList()
if (acceptTypes.isNotEmpty() && returnType != VOID) {
builder.add("\nacceptTypes: %L,", acceptTypes(acceptTypes))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2026 Outfox, Inc.
*
* 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
*
* http://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.
*/

package io.outfoxx.sunday.generator.kotlin

import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import io.outfoxx.sunday.generator.GenerationMode
import io.outfoxx.sunday.generator.kotlin.tools.compileTypesResult
import io.outfoxx.sunday.generator.kotlin.utils.KotlinProblemLibrary
import io.outfoxx.sunday.generator.tools.binaryResponseApi
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.nio.file.Path

@KotlinTest
@OptIn(ExperimentalCompilerApi::class)
class KotlinBinaryResponseTest {
@ParameterizedTest
@ValueSource(strings = ["openapi", "raml"])
fun `binary responses and unions preserve download bytes`(frontend: String) {
val registry = KotlinTypeRegistry("example", null, GenerationMode.Client, setOf(), KotlinProblemLibrary.SUNDAY)
KotlinSundayIrGenerator(
binaryResponseApi(frontend),
registry,
KotlinSundayOptions("example", "https://example.com/", listOf("application/json"), "API"),
).generateServiceTypes()
val result =
compileTypesResult(
registry.buildTypes(),
listOf(
SourceFile.kotlin(
"Check.kt",
java.nio.file.Files
.readString(Path.of("src/test/resources/binary-responses/Check.kt")),
),
),
)
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages)
result.classLoader
.loadClass("example.BinaryCheck")
.getMethod("verify")
.invoke(null)
}
}
Loading
Loading