Skip to content

Declare the navigation graph with a Kotlin DSL (no codegen) - #18

Merged
rinekri merged 2 commits into
masterfrom
codex/way-dsl-dual-support
Nov 9, 2026
Merged

rinekri merged 2 commits into
masterfrom
codex/way-dsl-dual-support

Conversation

@rinekri

@rinekri rinekri commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What

ru.kode.way.dsl.Graph declares a navigation graph in Kotlin, as an alternative to .dot files and the Gradle plugin. A Graph gives the runtime a Schema and a NodeBuilder, just as generated code does. That means NavigationService, extension points and way-compose work the same with either one, and DSL and generated schemas can include each other.

It ships as a separate artifact, ru.kode:way-dsl (module :way-dsl, package ru.kode.way.dsl), with the same targets as :way: JVM, Android and iOS. It depends on ru.kode:way via api, so one dependency is enough:

implementation("ru.kode:way-dsl:<version>")

Nothing in :way had to become public for it; it uses only the existing public API.

Usage

object LoginGraph : Graph("Login") {
  val login = parameterizedFlow<String, Int>("login") // built with a String, Finish(result: Int)
  val credentials = screen("credentials")
  val otp = parameterizedScreen<String>("otp")
  init { login to credentials to otp }
}

object AppGraph : Graph("App") {
  val app = flow<Unit>("app")
  val intro = screen("intro")
  val login = include("login", LoginGraph.login) // `type = schema`, typed by the included root
  val lastStep = deepHistory("lastStep")
  init {
    app to intro
    app to login
    app to lastStep
  }
}

NavigationService<Unit>(
  AppGraph.nodeBuilder {
    flow(AppGraph.app) { AppFlowNode() }
    screen(AppGraph.intro) { IntroScreen() }
    include(AppGraph.login) { userName: String -> LoginGraph.nodeBuilder { /* ... */ } }
  },
  onFinishRequest = { Finish(Unit) },
)

Supported:

  • Typed parameters and results.
    • Declarations: flow<R>, parameterizedFlow<P, R>, parallel<R>, parameterizedParallel<P, R>, parameterizedScreen<P>.
    • include(id, OtherGraph.root) takes the payload and result types of the included root.
    • Targets and factories are typed, so no Any? reaches user code.
    • A nullable P works as in codegen (NullPayload; a nullable root starts with null).
  • Child flow results: event.childFlowResult(AppGraph.login).
  • History nodes: history(id) and deepHistory(id), with the same placement rules as Parser.kt. Their target is the runtime HistoryTarget, and they need no factory.
  • Nested parallel flows: a parallel can be the root or sit under a flow, a screen or another parallel. Regions can be local flows, parallel flows or included graphs.
  • Fan-in: an included graph can have several parents, as long as none is a parallel and all are in the same region. Schema.targets returns every path, and target(payload, via = parent) is the counterpart of the generated <id>Via<Parent>.
  • Validation on first use mirrors Parser.kt:
    • one root, no cycles, children under the root flow and under every parallel;
    • several parents only for an include, and no children under an include;
    • history nodes placed correctly, and a factory for every non-history node.

Not supported, compared with .dot:

  • screens as direct regions of a parallel (wrap them in a flow);
  • full targets with ancestor payloads (use AbsoluteTarget);
  • typed region accessors (use Schema.regionByName);
  • a separate node builder per local flow (include a graph instead).

A DSL target under a parallel starts at its closest flow, not at the flow above the parallel; both resolve to the same node.

The README has a new "Kotlin DSL" section, and the CHANGELOG has an Unreleased entry. :way-dsl:jvmTest is added to the CI task list, and :way-dsl to the release workflow's test and publish steps. No version bump.

Testing

  • GraphTest (13 tests) covers:
    • validation errors and relative targets;
    • typed and nullable payloads, MissingPayloadException and missing factories;
    • child flow results, history placement, nested parallels and fan-in.
  • DslParityTest (6 tests) runs the same event sequences on generated graphs (dsl-parity-*.dot in way-dsl/src/commonTest/way) and on equivalent DSL graphs. The active path, alive paths and node payloads must be identical after every step. It covers:
    • a parameterized screen and a local child flow;
    • an included graph with payloads and results;
    • a parallel root;
    • typed params with a null and a non-null root payload;
    • shallow and deep history;
    • nested parallels;
    • fan-in.
  • Test helpers from :way (TestNodeBuilder, TestFlowNode, ...) are copied into way-dsl/src/commonTest, because KMP has no test fixtures.
  • These pass locally:
    • ./gradlew spotlessCheck;
    • the CI task list;
    • ./gradlew build, excluding the six iosSimulatorArm64Test / iosX64Test tasks, since no simulator runtime is installed on the machine used.
  • The iOS test binaries compile and link.

Codegen bugs found on master (not fixed here)

@rinekri
rinekri force-pushed the codex/way-dsl-dual-support branch from d4979ad to 15a3089 Compare November 8, 2026 21:24
@rinekri rinekri changed the title WIP: declare the navigation graph with a runtime DSL Declare the navigation graph with a Kotlin DSL (no codegen) Nov 8, 2026
@rinekri
rinekri force-pushed the codex/way-dsl-dual-support branch from 15a3089 to 15851f9 Compare November 8, 2026 22:33
ru.kode.way.dsl.Graph declares a graph in Kotlin as an alternative to .dot
files. It ships as a separate artifact, ru.kode:way-dsl (module :way-dsl,
same targets as :way, api dependency on :way). It implements the runtime
Schema and NodeBuilder, so it works on every platform of :way and mixes
with generated schemas.

Supported: flows, parameterized screens and flows, included graphs (the
counterpart of type = schema), and a parallel root whose regions are included
graphs. A finished child flow is reported with ChildFlowFinishedEvent.

Tests cover graph validation, payloads and navigation parity with graphs
generated from equivalent .dot files.
Bring the Kotlin DSL to parity with .dot codegen:
- typed flow, parallel and include parameters (parameterizedFlow<P, R>,
  parameterizedParallel<P, R>, typed target(payload) and factories);
  a nullable root is built with null when started without a payload
- history(id) and deepHistory(id) nodes with a HistoryTarget, with the
  placement rules of the .dot parser
- parallel flows anywhere, regions may be local flows and parallels
- an included graph may have several parents in one region;
  Schema.targets returns every path, target(payload, via) picks one

Validation mirrors Parser.kt. GraphTest covers each feature and its
errors, DslParityTest runs each against a generated counterpart.
@rinekri
rinekri force-pushed the codex/way-dsl-dual-support branch from 15851f9 to fe2ab52 Compare November 9, 2026 10:02
@rinekri
rinekri marked this pull request as ready for review November 9, 2026 11:43
@rinekri
rinekri merged commit cdd1828 into master Nov 9, 2026
1 check passed
@rinekri
rinekri deleted the codex/way-dsl-dual-support branch November 9, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant