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
11 changes: 6 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ jobs:
if: always()
with:
report_paths: |
reactive/api/build/test-results/test/TEST-*.xml
reactive/core/build/test-results/test/TEST-*.xml
reactive/java-flow/build/test-results/test/TEST-*.xml
reactive/kotlin/build/test-results/test/TEST-*.xml
reactive/reactivestreams/build/test-results/test/TEST-*.xml
reactive/datasource/api/build/test-results/test/TEST-*.xml
reactive/datasource/core/build/test-results/test/TEST-*.xml
reactive/datasource/java-flow/build/test-results/test/TEST-*.xml
reactive/datasource/kotlin/build/test-results/test/TEST-*.xml
reactive/datasource/reactivestreams/build/test-results/test/TEST-*.xml
reactive/streamlink/kotlin/build/test-results/test/TEST-*.xml
spring/build/test-results/test/TEST-*.xml
util/build/test-results/test/TEST-*.xml

Expand Down
1 change: 1 addition & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 22 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,44 @@ The repo is typically checked out on Windows. PowerShell uses `.\gradlew.bat`; B
### Module layout and dependency direction

```
reactive/api ← config DSL types (ActiveConfig, ChannelConfig, ContainerEvent, …)
reactive/core ← Binder + IFlowAdapter — does the real DataSource SDK plumbing
Everything funnels through here as kotlinx.coroutines Flow
reactive/kotlin ← Bind DSL for Flow } each contributes a Bind facade
reactive/java-flow ← Bind DSL for java.util.concurrent.Flow.Publisher
reactive/reactivestreams ← Bind DSL for org.reactivestreams.Publisher

spring ← spring-boot-starter-datasource (depends on reactive/kotlin)
reactive/datasource/api ← config DSL types (ActiveConfig, ChannelConfig, ContainerEvent, …)
reactive/datasource/core ← Binder + IFlowAdapter — does the real DataSource SDK plumbing
Everything funnels through here as kotlinx.coroutines Flow
reactive/datasource/kotlin ← Bind DSL for Flow } each contributes a Bind facade
reactive/datasource/java-flow ← Bind DSL for java.util.concurrent.Flow.Publisher
reactive/datasource/reactivestreams ← Bind DSL for org.reactivestreams.Publisher

reactive/streamlink/kotlin ← sl4jx-kotlin — Flow-based StreamLink client wrapper
(StreamLink + KeyMaster). Depends on datasourcex-util for Subject.

kotest ← datasourcex-kotest — Liberator-in-a-container Kotest harness
(LiberatorContainerExtension) for StreamLink client tests. Client-side
only: api-depends on sl4jx-kotlin + testcontainers, no datasourcex
runtime dependency. Its docker-backed integrationTest is not in `check`.
spring ← spring-boot-starter-datasource (depends on reactive/datasource/kotlin)
util ← datasourcex-util — FlowMap, custom Flow operators, AntPatternNamespace,
Fory serialization helpers. No DataSource SDK dependency.
Subject, Fory serialization helpers. `api`-depends on the DataSource
server SDK (`api(libs.datasource)`).
examples/ ← spring-java, spring-kotlin, spring-kotlin-chat — manual smoke tests
api-docs/ ← aggregated Dokka site (published to GitHub Pages from main)
```

The published Maven coordinates use the module's renamed name (`datasourcex-kotlin`, `spring-boot-starter-datasource`, etc.), not the Gradle path — set up in `settings.gradle.kts`.
The five datasource reactive modules sit under `reactive/datasource/` on disk so the StreamLink SDK family can sit beside them under `reactive/streamlink/`. A `projectDir` override in `settings.gradle.kts` relocates only the directory; the `include("reactive:kotlin")` coordinate, the resulting project path (`:reactive:datasourcex-kotlin`, `:reactive:datasourcex-reactive-core`, `:reactive:sl4jx-kotlin`, etc. — set by the `.name` overrides), and the published Maven coordinates are all unchanged. Published coordinates use the renamed name (`datasourcex-kotlin`, `sl4jx-kotlin`, `spring-boot-starter-datasource`, etc.), not the include path.

### Code generation (read this before editing reactive modules)

The three `reactive/{kotlin,java-flow,reactivestreams}` modules are NOT three parallel hand-written implementations. Each runs a `generateApi` Gradle task (defined in `buildSrc/src/main/kotlin/GenerateApi.kt`, applied via `common-reactive-library`) that emits Kotlin source for the `Bind`, `BindActive`, `BindActiveContainer`, `BindChannel`, `BindBroadcast` DSL classes plus their `Mapping`/`Json`/`Record` flavours.
The three `reactive/datasource/{kotlin,java-flow,reactivestreams}` modules are NOT three parallel hand-written implementations. Each runs a `generateApi` Gradle task (defined in `buildSrc/src/main/kotlin/GenerateApi.kt`, applied via `common-reactive-library`) that emits Kotlin source for the `Bind`, `BindActive`, `BindActiveContainer`, `BindChannel`, `BindBroadcast` DSL classes plus their `Mapping`/`Json`/`Record` flavours.

Each module's `build.gradle.kts` sets the variant: `tasks.generateApi { publisherType = "kotlin" | "java" | "reactivestreams" }`.

Implications:
- If you grep for `BindActiveJson` and find nothing, that's because it's generated. Run `./gradlew :reactive:datasourcex-kotlin:generateApi` and look in `reactive/kotlin/build/generated/sources/generateApi/main/kotlin/`.
- If you grep for `BindActiveJson` and find nothing, that's because it's generated. Run `./gradlew :reactive:datasourcex-kotlin:generateApi` and look in `reactive/datasource/kotlin/build/generated/sources/generateApi/main/kotlin/`.
- The shape of the generated DSL lives in `buildSrc/src/main/kotlin/{Active,ActiveContainer,Channel,Broadcast,Functions}.kt`. Edit those to change the generated API, not the build output.
- All three published variants share the runtime path through `reactive/core/Binder`. `IFlowAdapter` is the small bridge that converts each library's publisher type to/from `Flow<Any>`.
- All three published variants share the runtime path through `reactive/datasource/core/Binder`. `IFlowAdapter` is the small bridge that converts each library's publisher type to/from `Flow<Any>`.

### Spring starter

`spring` depends on `reactive/kotlin` and exposes endpoints via custom annotations on top of Spring Messaging:
`spring` depends on `reactive/datasource/kotlin` and exposes endpoints via custom annotations on top of Spring Messaging:
- `@DataService` marks a controller. `@DataMessageMapping("/subject/{var}")` maps a subject pattern to a method (analogous to `@MessageMapping`).
- `@IngressDestinationVariable` extracts path variables.
- Methods return `Flow<T>` / `Flux<T>` / `Publisher<T>` for active subjects, or accept/return `Flow` pairs for channels.
Expand Down
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ Then refer to the documentation:
* [datasourcex-kotlin](https://caplin.github.io/DataSource-Extensions/reactive/datasourcex-kotlin)
* [datasourcex-java-flow](https://caplin.github.io/DataSource-Extensions/reactive/datasourcex-java-flow)

## StreamLink

`sl4jx-kotlin` is a [Kotlin Flow](https://kotlinlang.org/docs/flow.html)-based client wrapper over
Caplin's StreamLink and KeyMaster SDKs, for consuming data from a Liberator.

### Usage

Add the following dependency to your project:

```kotlin
dependencies {
implementation("com.caplin.integration.datasourcex:sl4jx-kotlin:<version>")
}
```

Then refer to
the [documentation](https://caplin.github.io/DataSource-Extensions/reactive/sl4jx-kotlin).

## Spring

This module provides a starter for integrating Caplin DataSource with your
Expand All @@ -61,4 +79,22 @@ dependencies {

Then refer to
the [documentation](https://caplin.github.io/DataSource-Extensions/spring-boot-starter-datasource),
[hands-on tutorial](./spring/docs/GUIDE.md) and [examples](./examples).
[hands-on tutorial](./spring/docs/GUIDE.md) and [examples](./examples).

## Testing

`datasourcex-kotest` is a [Kotest](https://kotest.io/) test-support harness that runs a Liberator in
a [Testcontainers](https://testcontainers.com/) container for StreamLink client tests.

### Usage

Add the following dependency to your project:

```kotlin
dependencies {
testImplementation("com.caplin.integration.datasourcex:datasourcex-kotest:<version>")
}
```

Then refer to
the [documentation](https://caplin.github.io/DataSource-Extensions/datasourcex-kotest).
2 changes: 2 additions & 0 deletions api-docs/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ dependencies {
dokka(project(":reactive:datasourcex-java-flow"))
dokka(project(":reactive:datasourcex-kotlin"))
dokka(project(":reactive:datasourcex-reactivestreams"))
dokka(project(":reactive:sl4jx-kotlin"))
dokka(project(":datasourcex-kotest"))
dokka(project(":spring-boot-starter-datasource"))
dokka(project(":datasourcex-util"))
}
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies {
kover(project(":reactive:datasourcex-java-flow"))
kover(project(":reactive:datasourcex-kotlin"))
kover(project(":reactive:datasourcex-reactivestreams"))
kover(project(":reactive:sl4jx-kotlin"))
kover(project(":spring-boot-starter-datasource"))
kover(project(":datasourcex-util"))
}
Expand Down
16 changes: 16 additions & 0 deletions docs/adr/0002-subject-path-naming-convention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# A subject's string form is named `path`; a `Subject` object is named `subject`

Across both product lines (`datasourcex` and `sl4jx`), a variable, parameter, or property is named
by the type it holds: `path` when it is the `String` rendering of a subject, `subject` when it is a
`Subject`. The convention applies to public and internal code alike and to the `buildSrc` code
generation, which emits `path: String` DSL parameters (`pathParameter`). Method names describe an
operation rather than a variable — `getSubject(subject: Subject)`, `extractPathParameters(path: String)` —
and are governed by what they do, not by this rule.

## Consequences

New and renamed members follow the convention directly. A rename that would break binary
compatibility — a public `subject: String` property, whose getter would change from `getSubject()` to
`getPath()` — is introduced by **deprecate-and-add**: the `path` member is added and the old name is
kept as a `@Deprecated` alias delegating to it. This keeps the convention shippable within a minor
(3.x) release. The deprecated aliases are removed in a later major release.
24 changes: 24 additions & 0 deletions docs/adr/0003-no-data-classes-in-public-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Public value types are plain classes, not `data class`es

Public API value types across both product lines (`datasourcex`, `sl4jx`) are declared as ordinary
classes with hand-written `equals`/`hashCode`/`toString` where value semantics are wanted, never
`data class`. A `data class` bakes `copy(...)` and `componentN()` into the binary-compatible surface,
so adding or reordering a constructor property later is a breaking change — the `copy`/`componentN`
signatures shift, and `@JvmOverloads` does not extend to the generated `copy`. Hand-writing the value
methods keeps the constructor free to evolve within a minor release. See
[Public API challenges in Kotlin](https://jakewharton.com/public-api-challenges-in-kotlin/).

## Consequences

- New public value types follow this directly — e.g. the sl4jx `UpdateEvent`/`StatusEvent`/
`ErrorEvent` and `ContainerChange.*`, matching the existing `reactive.api` events (`ContainerEvent`,
`BroadcastEvent`), which expose only the `componentN` they choose to hand-write.
- Losing `data` does not mean losing `copy`: hand-write it when it is wanted. Because you own the
signature, a later parameter is added as an overload rather than silently shifting the generated
one. `AntPatternNamespace` is the in-repo example — a plain class with a hand-written `copy`.
- A type may support destructuring by hand-writing `componentN`; it need not expose `copy`.
- A type that holds non-value fields (streams, containers, connections) is a plain class with no
`equals`/`hashCode` at all, rather than a `data class` whose generated equality would be meaningless.
- Removing a shipped `data class`'s generated `copy`/`componentN` is itself binary-breaking, so a
released type is converted by hand-writing whichever of those members callers already use; only a
genuinely unused member is dropped outright.
23 changes: 23 additions & 0 deletions examples/spring-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,26 @@ dependencies {
implementation(project(":spring-boot-starter-datasource"))
implementation("org.springframework.boot:spring-boot-starter")
}

// Docker-backed end-to-end test: starts a Liberator Testcontainer, runs this adapter against it and
// subscribes as a StreamLink client. Not wired into `check`; run on demand with
// `./gradlew :examples:spring-kotlin:integrationTest`.
testing {
suites {
register<JvmTestSuite>("integrationTest") {
dependencies {
implementation(project())
implementation(project(":datasourcex-kotest"))
implementation(libs.kotest.runner)
implementation(libs.kotest.assertions)
implementation(libs.turbine)
}
targets.configureEach { testTask.configure { useJUnitPlatform() } }
}
}
}

configurations {
named("integrationTestImplementation") { extendsFrom(configurations.implementation.get()) }
named("integrationTestRuntimeOnly") { extendsFrom(configurations.runtimeOnly.get()) }
}
Loading
Loading