Skip to content

Commit 2aa946c

Browse files
committed
build: add sdk-example, a runnable end-to-end usage sample
Until now nothing exercised the assembled toolkit as a whole, and there was no executable reference showing how the pluggable seams fit together. Add `sdk-example`, an `application`-plugin module that wires the four pluggable pieces and issues a real HTTP exchange through the public API: - OkioIoProvider installed into the Io seam, - the OkHttp transport as the terminal HttpClient, - an HttpPipeline carrying one step per user-installable pillar (REDIRECT, RETRY, AUTH, LOGGING), - JacksonSerde for typed request/response bodies. The request runs against an embedded mockwebserver3 driven from `main()`, so the sample is deterministic and needs no network: `:sdk-example:run` serializes a typed request, POSTs it, and deserializes the typed response. The AUTH pillar refuses to stamp credentials over plaintext, so the embedded server speaks HTTPS with a self-signed certificate (okhttp-tls) and the transport is configured to trust it — the same shape a production caller would use. A smoke test drives the identical wiring under `build`. The module is intentionally not a published library, so it opts out of the gates that only make sense for the public ABI: - no `maven-publish`/`signing` — it is a sample, never released; - excluded from binary-compatibility checks via `apiValidation.ignoredProjects`, since application code has no stable ABI to snapshot; - left out of the Kover aggregate (it does not apply the plugin), so `main()`-centric sample code does not drag the 80% line-coverage floor down. Its smoke test still runs and proves the sample works. It keeps explicit-API strict mode, ktlint, detekt, and allWarningsAsErrors (inherited from the root build) and stays on the Java 8 toolchain.
1 parent ea0cc81 commit 2aa946c

6 files changed

Lines changed: 388 additions & 1 deletion

File tree

build.gradle.kts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,20 @@ plugins {
4141
group = "org.dexpace"
4242
version = "0.0.1-alpha.1"
4343

44-
// Coverage: aggregate every Kover-enabled subproject through this root project's reports.
44+
// `sdk-example` is a runnable usage sample (an `application` module), not a published library,
45+
// so it has no committed `.api` baseline and must not be subject to binary-compatibility checks.
46+
// Without this, `apiCheck` would demand an `api/sdk-example.api` snapshot for application code
47+
// that has no stable ABI. Every library module stays validated.
48+
apiValidation {
49+
ignoredProjects.add("sdk-example")
50+
}
51+
52+
// Coverage: aggregate every Kover-enabled *library* subproject through this root project's
53+
// reports. `sdk-example` is deliberately absent: it is sample code built around a `main()`, and
54+
// folding it into the aggregate would drag the 80% line-coverage floor down for code that exists
55+
// to be read and run, not unit-tested to the library standard. The example does not apply the
56+
// Kover plugin, so it contributes nothing to these reports; its own smoke test still runs under
57+
// `build` and proves the sample assembles and executes end-to-end.
4558
dependencies {
4659
kover(project(":sdk-core"))
4760
kover(project(":sdk-io-okio3"))

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-
2424
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
2525
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
2626
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
27+
okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp" }
2728
okhttp-mockwebserver-junit5 = { module = "com.squareup.okhttp3:mockwebserver3-junit5", version.ref = "mockwebserver" }
2829
reactor-core = { module = "io.projectreactor:reactor-core", version.ref = "reactor" }
2930
reactor-test = { module = "io.projectreactor:reactor-test", version.ref = "reactor" }

sdk-example/build.gradle.kts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
plugins {
9+
kotlin("jvm")
10+
application
11+
}
12+
13+
group = "org.dexpace"
14+
version = "0.0.1-alpha.1"
15+
16+
// Java 8 bytecode and explicit-API strict mode are inherited from the root build script
17+
// (jvmToolchain(8), jvmTarget=1.8, allWarningsAsErrors, explicitApi=Strict). The sample wires
18+
// the Java-8 OkHttp transport, so it stays on the default toolchain — no override needed.
19+
//
20+
// Unlike the library modules this module applies NEITHER `maven-publish`/`signing` (it is a
21+
// usage sample, never released) NOR the Kover plugin (it is intentionally outside the aggregate
22+
// coverage floor — see the root build.gradle.kts rationale). The binary-compatibility validator
23+
// also skips it via `apiValidation.ignoredProjects` in the root build.
24+
25+
application {
26+
mainClass.set("org.dexpace.sdk.example.ExampleAppKt")
27+
}
28+
29+
dependencies {
30+
// Public contracts: HTTP models, the pipeline runtime + pillar steps, the I/O seam.
31+
implementation(project(":sdk-core"))
32+
// I/O adapter — the single `IoProvider` the sample installs at startup.
33+
implementation(project(":sdk-io-okio3"))
34+
// Transport adapter — the terminal `HttpClient` the pipeline dispatches to.
35+
implementation(project(":sdk-transport-okhttp"))
36+
// Serde adapter — typed request/response (de)serialization.
37+
implementation(project(":sdk-serde-jackson"))
38+
39+
// MockWebServer ships in the OkHttp project as a generic embedded HTTP server. The sample
40+
// drives it from `main()` so the end-to-end exchange runs deterministically with no network.
41+
implementation(libs.okhttp.mockwebserver.junit5)
42+
// okhttp-tls mints a self-signed certificate so the embedded server can speak HTTPS — the
43+
// AUTH pillar step refuses to stamp credentials over plaintext, so the sample uses TLS exactly
44+
// as a production caller would. `OkHttpClient` is configured directly here, hence the explicit
45+
// dependency on OkHttp itself.
46+
implementation(libs.okhttp)
47+
implementation(libs.okhttp.tls)
48+
49+
// SLF4J is `compileOnly` on every Kotlin module (added by the root build); the sample needs a
50+
// real binding at runtime so the pipeline's instrumentation logging has somewhere to go. NOP
51+
// keeps the console output limited to what the sample prints itself.
52+
runtimeOnly(libs.slf4j.nop)
53+
54+
testImplementation(kotlin("test"))
55+
testImplementation(libs.junit.jupiter)
56+
testRuntimeOnly(libs.slf4j.nop)
57+
}
58+
59+
tasks.test {
60+
useJUnitPlatform()
61+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright (c) 2026 dexpace and Omar Aljarrah
3+
*
4+
* Licensed under the MIT License. See LICENSE in the project root.
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package org.dexpace.sdk.example
9+
10+
import mockwebserver3.MockResponse
11+
import mockwebserver3.MockWebServer
12+
import okhttp3.OkHttpClient
13+
import okhttp3.tls.HandshakeCertificates
14+
import okhttp3.tls.HeldCertificate
15+
import org.dexpace.sdk.core.client.HttpClient
16+
import org.dexpace.sdk.core.http.auth.KeyCredential
17+
import org.dexpace.sdk.core.http.common.CommonMediaTypes
18+
import org.dexpace.sdk.core.http.common.HttpHeaderName
19+
import org.dexpace.sdk.core.http.pipeline.HttpPipeline
20+
import org.dexpace.sdk.core.http.pipeline.HttpPipelineBuilder
21+
import org.dexpace.sdk.core.http.pipeline.steps.DefaultInstrumentationStep
22+
import org.dexpace.sdk.core.http.pipeline.steps.DefaultRedirectStep
23+
import org.dexpace.sdk.core.http.pipeline.steps.DefaultRetryStep
24+
import org.dexpace.sdk.core.http.pipeline.steps.HttpInstrumentationOptions
25+
import org.dexpace.sdk.core.http.pipeline.steps.HttpLogLevel
26+
import org.dexpace.sdk.core.http.pipeline.steps.KeyCredentialAuthStep
27+
import org.dexpace.sdk.core.http.request.Method
28+
import org.dexpace.sdk.core.http.request.Request
29+
import org.dexpace.sdk.core.http.request.RequestBody
30+
import org.dexpace.sdk.core.io.Io
31+
import org.dexpace.sdk.core.serde.deserialize
32+
import org.dexpace.sdk.io.OkioIoProvider
33+
import org.dexpace.sdk.serde.jackson.JacksonSerde
34+
import org.dexpace.sdk.transport.okhttp.OkHttpTransport
35+
import java.net.URL
36+
37+
/*
38+
* End-to-end usage sample for the dexpace SDK.
39+
*
40+
* This module exists as an executable smoke test of the assembled toolkit: it wires the four
41+
* pluggable seams together and proves they cooperate over a real HTTP exchange —
42+
*
43+
* - an OkioIoProvider installed into the Io seam,
44+
* - the OkHttpTransport as the terminal HttpClient,
45+
* - an HttpPipeline carrying one step per user-installable pillar (REDIRECT, RETRY, AUTH,
46+
* LOGGING),
47+
* - and JacksonSerde for typed request/response bodies.
48+
*
49+
* The request targets an embedded MockWebServer, so the sample is fully deterministic and needs
50+
* no network access — `./gradlew :sdk-example:run` produces the same output everywhere.
51+
*
52+
* The AUTH pillar refuses to stamp credentials over plaintext HTTP, so the embedded server speaks
53+
* HTTPS with a self-signed certificate (newTlsServer) and the transport is configured to trust it
54+
* (TlsServer.newTransportTrusting) — the sample uses TLS exactly as a production caller would.
55+
*
56+
* The wiring is deliberately split out of main() into small functions so the smoke test can
57+
* exercise the exact same code paths the sample runs.
58+
*/
59+
60+
/** HTTP 201 Created — the status the embedded server returns for the sample POST. */
61+
private const val HTTP_CREATED = 201
62+
63+
/** A typed request payload, serialized to JSON by the [JacksonSerde]. */
64+
public data class CreateUserRequest(
65+
val name: String,
66+
val email: String,
67+
)
68+
69+
/** A typed response payload, deserialized from JSON by the [JacksonSerde]. */
70+
public data class User(
71+
val id: Long,
72+
val name: String,
73+
val email: String,
74+
)
75+
76+
/**
77+
* Installs the Okio-backed [Io] provider. Install is idempotent for the same provider, so calling
78+
* this from both [main] and the smoke test is safe.
79+
*/
80+
public fun installIoProvider() {
81+
Io.installProvider(OkioIoProvider)
82+
}
83+
84+
/**
85+
* Mints a self-signed certificate for `localhost` and starts an HTTPS [MockWebServer] serving it.
86+
* The matching client trust material is returned alongside so the caller can build a transport
87+
* that trusts this exact certificate — see [newTransportTrusting].
88+
*/
89+
public fun newTlsServer(): TlsServer {
90+
val certificate =
91+
HeldCertificate.Builder()
92+
.addSubjectAlternativeName("localhost")
93+
.build()
94+
val serverCertificates =
95+
HandshakeCertificates.Builder()
96+
.heldCertificate(certificate)
97+
.build()
98+
val clientCertificates =
99+
HandshakeCertificates.Builder()
100+
.addTrustedCertificate(certificate.certificate)
101+
.build()
102+
103+
val server = MockWebServer()
104+
server.useHttps(serverCertificates.sslSocketFactory())
105+
return TlsServer(server, clientCertificates)
106+
}
107+
108+
/** An embedded HTTPS [MockWebServer] paired with the client trust material that accepts it. */
109+
public class TlsServer internal constructor(
110+
public val server: MockWebServer,
111+
private val clientCertificates: HandshakeCertificates,
112+
) {
113+
/**
114+
* Builds an [OkHttpTransport] over a BYO [OkHttpClient] that trusts this server's self-signed
115+
* certificate. The transport is SDK-managed, so closing it shuts the underlying client down.
116+
*/
117+
public fun newTransportTrusting(): OkHttpTransport {
118+
val client =
119+
OkHttpClient.Builder()
120+
.sslSocketFactory(
121+
clientCertificates.sslSocketFactory(),
122+
clientCertificates.trustManager,
123+
)
124+
.build()
125+
return OkHttpTransport.create(client)
126+
}
127+
}
128+
129+
/**
130+
* Assembles an [HttpPipeline] over [transport] with exactly one step on each user-installable
131+
* pillar stage. The SERDE pillar is reserved by the runtime and carries no user step — typed
132+
* (de)serialization happens explicitly at the call site via [JacksonSerde], as shown in
133+
* [createUser].
134+
*/
135+
public fun buildPipeline(transport: HttpClient): HttpPipeline =
136+
HttpPipelineBuilder(transport)
137+
// REDIRECT pillar — follow 3xx responses within a hop budget.
138+
.append(DefaultRedirectStep())
139+
// RETRY pillar — exponential backoff that honours `Retry-After`.
140+
.append(DefaultRetryStep())
141+
// AUTH pillar — stamp a static API key into the `Authorization` header.
142+
.append(
143+
KeyCredentialAuthStep(
144+
KeyCredential(
145+
apiKey = "example-api-key",
146+
headerName = HttpHeaderName.AUTHORIZATION,
147+
prefix = "Bearer",
148+
),
149+
),
150+
)
151+
// LOGGING pillar — emit request/response diagnostics at header granularity.
152+
.append(
153+
DefaultInstrumentationStep(
154+
HttpInstrumentationOptions(logLevel = HttpLogLevel.HEADERS),
155+
),
156+
)
157+
.build()
158+
159+
/**
160+
* Serializes [request] to a JSON body, POSTs it through [pipeline] to [endpoint], and deserializes
161+
* the JSON response into a typed [User]. Throws if the server does not answer with a 2xx status.
162+
*
163+
* The returned [User] is fully materialized before the response is closed, so the caller does not
164+
* own any streaming resource.
165+
*/
166+
public fun createUser(
167+
pipeline: HttpPipeline,
168+
serde: JacksonSerde,
169+
endpoint: URL,
170+
request: CreateUserRequest,
171+
): User {
172+
val json = serde.serializer.serialize(request)
173+
val httpRequest =
174+
Request.builder()
175+
.method(Method.POST)
176+
.url(endpoint)
177+
.addHeader(HttpHeaderName.ACCEPT.toString(), CommonMediaTypes.APPLICATION_JSON.toString())
178+
.body(RequestBody.create(json, CommonMediaTypes.APPLICATION_JSON))
179+
.build()
180+
181+
pipeline.send(httpRequest).use { response ->
182+
val status = response.status
183+
val payload = response.body?.source()?.readUtf8().orEmpty()
184+
check(status.isSuccess) { "Unexpected status $status — body: $payload" }
185+
return serde.deserializer.deserialize<User>(payload)
186+
}
187+
}
188+
189+
/**
190+
* Runs the full sample against an embedded HTTPS [MockWebServer] and prints the typed round-trip.
191+
*
192+
* No arguments are read; nothing touches the network. The single canned response makes the output
193+
* stable across runs and machines.
194+
*/
195+
public fun main() {
196+
installIoProvider()
197+
val serde = JacksonSerde.withDefaults()
198+
199+
val tls = newTlsServer()
200+
tls.server.use { server ->
201+
// Canned JSON the SDK will deserialize back into a typed `User`.
202+
server.enqueue(
203+
MockResponse.Builder()
204+
.code(HTTP_CREATED)
205+
.addHeader(
206+
HttpHeaderName.CONTENT_TYPE.toString(),
207+
CommonMediaTypes.APPLICATION_JSON.toString(),
208+
)
209+
.body("""{"id":1,"name":"Ada Lovelace","email":"ada@example.org"}""")
210+
.build(),
211+
)
212+
server.start()
213+
214+
tls.newTransportTrusting().use { transport ->
215+
val pipeline = buildPipeline(transport)
216+
val endpoint = server.url("/v1/users").toUrl()
217+
val payload = CreateUserRequest(name = "Ada Lovelace", email = "ada@example.org")
218+
219+
println("POST $endpoint")
220+
println(" request : $payload")
221+
val user = createUser(pipeline, serde, endpoint, payload)
222+
println(" response: $user")
223+
println("Created user #${user.id} (${user.name}).")
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)