From fa04c6dc25c332fa2857a1a88dff75e3fe729e82 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 22:48:04 +0200 Subject: [PATCH 01/28] hack/designs: spec unified client generation Generate the same Java client for a module whether the caller is another module or a plain Maven project, and make a plain Java program able to open a session and have the engine load the module it calls. Signed-off-by: Yves Brissaud --- .../2026-09-13-unified-client-generation.md | 872 ++++++++++++++++++ 1 file changed, 872 insertions(+) create mode 100644 hack/designs/2026-09-13-unified-client-generation.md diff --git a/hack/designs/2026-09-13-unified-client-generation.md b/hack/designs/2026-09-13-unified-client-generation.md new file mode 100644 index 0000000..4be175d --- /dev/null +++ b/hack/designs/2026-09-13-unified-client-generation.md @@ -0,0 +1,872 @@ +# Unified client generation + +Status: proposed +Date: 2026-09-13 + +## Terms + +This document uses one word per concept. + +| Term | Meaning | +| --- | --- | +| **target** | A Dagger module that Java code calls. | +| **scope** | A directory the SDK generates into. A *module scope* holds a Dagger module. A *standalone scope* is an ordinary Maven project that only calls targets. | +| **bindings** | The generated Java code. | +| **client package** | `io.dagger.client.modules.`, holding one target's bindings. | +| **target descriptor** | The generated record of one target's name, reference and pin, used to serve it. | + +The engine and its configuration use "client" for what this document calls a +target: `dagger module client add java ` records a target, and +`[sdks.java.scopes.""].clients` holds them. Where this document quotes an +engine name it keeps the engine's spelling. + +## Reviewed baselines + +Every claim below was checked against these exact revisions. + +| What | Revision | +| --- | --- | +| This repository (`dagger/java-sdk`), base of the change | `24f430a529a5aa07b0d3ca64417d8f460394f004` | +| The engine (`dagger/dagger`), `main` | `7c35e6274737acff0f6bd76614abb5e04efa7d12` | +| The released engine and CLI the checks run against | `v1.0.0-beta.13` (`6bf59d50`) | +| The engine the `engine-e2e` module builds today | `8fd9b22b5416f8dc7cb420ba37769adef6e874d2`, a pre-merge commit of `dagger/dagger#13992`, **not** an ancestor of `7c35e627` | +| The SDK-module interface, `dagger/dagger#13992` | merged 2026-09-09 | +| The client-codegen schema primitives, `dagger/dagger#13646` | merged 2026-07-16 | +| Manifest v2 entrypoints, `dagger/dagger#14038` | **open, unmerged** — nothing here depends on it | +| The manifest-v2 prototype in this repository, `dagger/java-sdk#19` | open, unmerged — nothing here depends on it | +| This work's own pull request, `dagger/java-sdk#17` | the unified-clients redesign named as a non-goal by the design at `hack/designs/2026-09-04-sdk-module-interface.md` | +| The reference standalone-client implementation, `dagger/typescript-sdk#42` | merged | +| The reference per-target emitter, `dagger/python-sdk#22` | open draft, built on an SDK interface the engine no longer has | + +An earlier attempt at this feature in this repository was abandoned. Its tip is +`848fc622b4c83dc16e226e802f17c76f66c2cf3b`, on the branch `module-max` of the +fork `github.com/eunomie/java-sdk`. Parts of it are reused. The section +**What was taken from the abandoned attempt** lists every commit by full hash. + +## Problem + +A Dagger module written in Java can call another module. This repository already +generates the Java bindings for that: `dagger module client add java ` +records the target, `dagger generate` writes the bindings into the calling +module's vendored SDK, and the module's code calls `theTarget()` after one +import. + +A plain Java program cannot do the same thing. `dagger module client add java` +in a Maven project that is not a Dagger module is refused. `main.dang` raises +`java-sdk does not generate standalone module clients yet`. Two separate things +are missing. + +1. **Generation.** Nothing produces bindings for a scope that has no module. +2. **Runtime.** `io.dagger.client.engineconn.Connection` reads + `DAGGER_SESSION_PORT` and `DAGGER_SESSION_TOKEN` and throws when they are + absent. It cannot start a session. It also accepts a `loadWorkspaceModules` + parameter and ignores it. So even with bindings in hand, a plain + `java -jar app.jar` cannot reach an engine, and nothing asks the engine to + load the target. + +There is a second problem in the case that does work. Every type in the schema — +the engine core API and every target's types — is generated into one flat Java +package, `io.dagger.client`. The content of that package therefore depends on +the whole target set. A module with targets `{alpha}` and a program with targets +`{alpha, beta}` get different files for `alpha`. The bindings for a target are +not a stable artifact. They are a by-product of whoever is calling. + +## Goals + +- One Java program, module or not, reaches a target through the same bindings. +- A target's client package is byte-identical wherever it is generated, and does + not change when an unrelated target is added or removed. +- A plain Java program can open an engine session and reach its targets with no + Dagger CLI wrapper command. +- One code path generates a client package. The module scope and the standalone + scope differ only in the schema they hand that path and in what they emit + alongside it. + +## Non-goals + +- **Downloading the Dagger CLI.** The runtime will start a session from a + `dagger` binary it finds. It will not fetch one. Provisioning is a separate + concern with its own release, checksum and mirror questions. +- **Publishing the Java SDK to a Maven repository.** The SDK stays vendored as + source, as it is for modules today. +- **Generating a client package for the module's own types.** See + **Alternatives considered**. +- **Manifest v2.** `dagger/dagger#14038` and `dagger/java-sdk#19` are unmerged. + Nothing here uses either. +- **Build tools other than Maven.** A Java scope is a directory with a + `pom.xml`; `findClientRoot` in `main.dang` already says so. Gradle is out of + scope. +- **Choosing a target at run time.** Bindings are typed, so the set of targets + is fixed when the code is generated. The engine loads a recorded target when + the program first needs it. The program cannot invent a reference at run time. + +## What the engine already provides + +These facts decide the design. Each was read in the engine source at +`7c35e6274737acff0f6bd76614abb5e04efa7d12`. + +**`ModuleSource.clientSchemaIntrospectionJSON` is the unit of client +generation.** It returns an introspection schema holding the client-facing core +API plus exactly one module, installed under its own name so the module is +reached as `dag.`. The module's own dependencies are excluded. The +engine's own comment states the intent: "a client is generated for a single +module plus core, not for its whole dependency graph" +(`core/schema/modulesource.go`). This is the same call for a module's dependency +and for a standalone target, because in both cases the SDK holds a +`ModuleSource`. + +Two qualifications. First, the field installs the module only when the source +has an SDK whose implementation resolves as a runtime +(`clientSchemaIntrospectionJSONFile`). A target with no runtime SDK yields core +alone, silently. Second, "client-facing core" means core rendered through the +*target's* declared `engineVersion` view, not the running engine's: the helper +rewrites the core module with `WithView(...NormalizeVersion(src.EngineVersion))` +before building the schema. Two targets on different engine versions therefore +produce two different core schemas. + +**`Module.serve` may be called more than once in a session.** The engine adds +each served module to the client's served set and deduplicates by name +(`Server.serveModule` in `engine/server/session.go`). It rejects only a second +module claiming a name already taken by a *different* source, where sameness is +canonical source identity — absolute local path, or clone reference plus subpath +plus pin — not the raw reference string. The doc string on the `serve` field +still says "this can only be called once per session"; the implementation +contradicts it. + +**The engine namespaces a module's type names, which reduces collisions without +removing them.** `NamespaceObject` in `core/gqlformat.go` renames a module's +local `Result` to `Result`. Two ordinary modules do not collide. But the +namespacing runs through `strcase.ToCamel`, so `foo-bar`, `foo_bar` and `FooBar` +converge, and a module's main object takes the module's own final name, which +can equal a core type name. The SDK must validate names itself. + +**A recorded target is not a workspace module.** `dagger module client add` +appends to `[sdks.java.scopes.""].clients` and writes nothing under +`[modules]` (`withSDKModuleClient` in `core/schema/workspace_sdk_module.go`). +The two sets are configured independently and are not guaranteed to match, so +`dagger session --load-workspace-modules` does not serve a recorded target. A +generated client must serve its own targets. + +**Measured, not assumed.** Against `v1.0.0-beta.13`, for the fixture module +`client-dep` at `.dagger/modules/e2e/fixtures/clients/dep`, +`clientSchemaIntrospectionJSON` returns 123 unowned types plus exactly one owned +type, `ClientDep`, and `Query` carries 37 fields of which one, `clientDep`, is +attributed to `client-dep`. For this repository's own root module, +`introspectionSchemaJSON` returns 123 types in total, of which three are +attributed to its one dependency `sdk-helpers`, and `Query` carries 32 fields of +which one is. The module-facing schema is the smaller of the two, which is the +core the engine hides from module code. The module's own types appear in +neither. + +One detail matters for the implementation: the `module` argument of the +`@sourceMap` directive arrives JSON-encoded, so its value includes the quotation +marks. The generator strips them. + +**The released engine already has all of this.** `v1.0.0-beta.13` postdates the +merge of `dagger/dagger#13992`. Its schema carries +`ModuleSource.clientSchemaIntrospectionJSON`, `Workspace.withClient`, +`Workspace.withoutClient` and `Workspace.withUpdatedClients`, and the existing +check `e-2-e:generate-scope-clients-check` passes against it in 1m41s with no +engine built from source. The README's statement that this SDK needs an engine +built from `dagger/dagger#13992` is out of date. + +## Proposed approach + +### The generated layout + +``` +io.dagger.client core API and the hand-written runtime +io.dagger.client.modules. one package per target +``` + +`io.dagger.client` keeps its present meaning and contents: the hand-written +runtime (`Dagger`, `QueryBuilder`, `engineconn`, `exception`, `graphql`, +`telemetry`) and the generated core types, including the generated `Client` +class that binds the GraphQL `Query` root. `Dagger.dag()` still returns that +`Client`, and core is still reached as `dag().container()`. No module written +against this SDK changes the way it calls core. + +Each target gets one package under `io.dagger.client.modules`, named from the +target's final name. The target's own types live there and nowhere else. Nesting +the targets one level down is what makes that safe: a target named `graphql` or +`exception` becomes `io.dagger.client.modules.graphql`, which cannot collide +with the runtime's own `io.dagger.client.graphql`. Only names Java itself +reserves are refused, and two names that normalize to one package segment. + +The way into a target moves there too, as a static method on the target's own +root type: + +```java +import static io.dagger.client.modules.sdkhelpers.SdkHelpers.sdkHelpers; + +sdkHelpers().moduleManifest() +``` + +Core is not extended with an accessor. One import is the whole of the +integration, and a caller that never names a session gets the ambient one; a +caller that has one passes it, as `sdkHelpers(dag)`. + +### What belongs to core and what belongs to a client package + +Java has no partial classes, so a type is generated once, in one package. The +split follows from that. + +- **Core** is every type the engine's `@sourceMap` directive does not attribute + to a module, with **every module-owned field stripped**. `Query.sdkHelpers()` + is such a field, and so is `Binding.asSdkHelpers()`; neither appears on its + core class. +- **A client package** is every type `@sourceMap` attributes to that target, + plus the fields that target contributes to core types, re-homed. + +A contributed field has no class of its own — `Query` and `Binding` belong to +core, and Java has no partial classes — so each becomes a static method on the +target's root type, named after the field and taking the core receiver it was +reached through as its first argument. `Binding.asSdkHelpers()` becomes +`SdkHelpers.asSdkHelpers(binding)`. `Query`'s receiver is the session, so it +stays implicit and the method is offered both with and without it. The +`SdkHelpersArguments` holder moves with its method, onto the target's root type. + +Core is therefore independent of the target set: no core source names a client +package, and the byte-identity guarantee covers core as well as +`io.dagger.client.modules.`. + +### The generation path + +Generation takes a **plan**: a list of entries, each with a schema, a target +Java package, and the name of the module that owns it, or none for core. The +Maven codegen plugin reads the whole plan in one invocation and emits every +package. Cross-package type references resolve through a registry populated from +every entry, so a client package referring to `Directory` gets +`io.dagger.client.Directory` rather than a second copy. + +The two scope kinds differ in five places and share everything else. + +| | module scope | standalone scope | +| --- | --- | --- | +| core entry schema | `moduleSource(scope).introspectionSchemaJSON` | the merged core of every target's `clientSchemaIntrospectionJSON` | +| client entries | one per recorded target | one per recorded target | +| output root | `/sdk` | `/dagger` | +| also emitted | the annotation-processor entry point | nothing; a client package carries its own descriptor | +| build integration | the module's own generated `pom.xml` | one profile inserted into the user's `pom.xml` | +| source roots | three, split by role: the runtime, the annotation processor, the bindings | one, because the profile adds exactly one directory to a pom the SDK does not own | +| the Maven coordinate the SDK jars build under | the module's name | derived from the scope's path | + +The core entry uses the module-facing schema for a module scope on purpose. The +engine hides part of the core API from module code, and generating the wider +client-facing core into a module would offer module authors calls the engine +will refuse. + +### Merging core for a standalone scope, and refusing a skewed one + +A standalone scope has no module-facing schema, so its core comes from the +targets. Every target's schema carries a full copy of core, rendered through +that target's declared engine version. The merge is defined and guarded: + +1. Take each target's schema and remove every type `@sourceMap` attributes to a + module. What remains is that target's view of core, including the fields the + target contributes to core types. +2. Remove each target's own contributed fields from each view, giving its + *bare* core. +3. Require every bare core to be identical. If two differ, refuse generation and + name both targets, both declared engine versions, and the first type that + differs. +4. The merged core is the bare core plus, for every target in sorted order, + the fields that target contributes to core types. + +Step 3 is the guard against version skew. It also makes the result independent +of target order, which "take the first target's core" would not be. + +A module scope applies the same rule between the scope and its targets: a target +whose declared engine version has a different base version from the module's is +refused, because the module-facing core is rendered through the module's version +and the client package through the target's. + +### Reaching the target at run time + +A client package carries a **target descriptor**: the target's final name, and +either a workspace-root-absolute path or a git reference with the commit it +resolved to at generation time. It is data, written by the generator into the +package it belongs to: + +```java +private static final ModuleTarget TARGET = + ModuleTarget.inWorkspace("sdk-helpers", "/dagger/modules/sdk-helpers"); +``` + +Every entry point in that package serves it before selecting anything, because +until the module is served the field being selected does not exist: + +```java +ModuleTargets.serve(dag.queryBuilder(), TARGET); +``` + +`ModuleTargets` is hand-written and lives in `io.dagger.client`. The session is a +parameter rather than a global, so a client from `Dagger.connect()` serves into +its own session instead of into whichever one `Dagger.dag()` happens to hold. +The first time a target is asked for in a session, it sends: + +``` +moduleSource(ref, refPin: pin) or currentWorkspace.moduleSource(path) + .withName(finalName) + .asModule() + .serve() +``` + +`withName` pins the name the schema was generated under, so a later change in +how the engine resolves a target's name cannot silently produce a root field the +bindings do not have. + +The descriptor is passed in, not looked up. A package owns the target it was +generated against, which is what lets a package be the whole of how a target is +reached: there is no registry to consult, no service file to ship, and no +question of what happens when two of them disagree. + +Whether a target carries a descriptor is decided when the plan is written, and +per target: + +| target | in a module | standalone | +| --- | --- | --- | +| git | descriptor | descriptor | +| workspace path | none — the engine serves it | descriptor | + +A module runtime has no filesystem session attachable, so it cannot resolve a +workspace path; the engine serves such a target from the module's manifest +instead, and the generated package asks for nothing. A git module is reachable +from anywhere, so a client loads it for itself in either scope — which is what +makes a git target's package come out byte for byte identical on both sides, and +is what `clientsAreOneArtifactCheck` pins. + +The remaining asymmetry is an engine limitation, not an SDK one. When a module +runtime can serve a workspace-local module, `modulePlan` passes +`servesWorkspacePaths: true`, the manifest stops carrying dependencies, and the +two columns above become one. + +Three properties follow. Serving is lazy, so an unusable target that no code +calls costs nothing. Serving is per target, so one broken target does not stop +the others. And a package that does not serve carries no trace of serving at +all, rather than a call that returns at once. + +### Opening a session outside a module + +`Connection.get(workingDir, loadWorkspaceModules)` gains a fallback. When +`DAGGER_SESSION_PORT` and `DAGGER_SESSION_TOKEN` are set, it attaches to that +session, as it does today. When they are not, it starts one: + +1. Find the CLI: `_EXPERIMENTAL_DAGGER_CLI_BIN`, else `dagger` on `PATH`. When + neither resolves, fail with a message naming both. +2. Run `dagger session`, passing `--version` with the SDK's engine version, and + `--load-workspace-modules` when the caller asked for it, with the working + directory as the process working directory. +3. Read one line of JSON from its standard output within a bounded timeout: + `{"port":…,"session_token":…}`. On timeout, malformed JSON, an out-of-range + port, an empty token or early exit, fail with the process's captured standard + error included. +4. Forward the rest of its standard error to the SDK logger, so engine progress + reaches the user. +5. On close, shut the process down the way the CLI expects — close its standard + input, wait, then destroy forcibly — and register the same shutdown on a JVM + hook. + +`Dagger.dag()` becomes synchronized, because two threads racing the first call +would otherwise start two engines. + +This makes the existing `loadWorkspaceModules` parameter mean something. A +generated client does not use it: it serves its own targets, which is narrower, +works for a target that is not a workspace module, and keeps the run-time schema +aligned with the one the code was generated against. + +### Fitting a standalone client into a Maven project + +The scope's `pom.xml` belongs to the user. The generated files go under +`/dagger/`, entirely SDK-owned: + +``` +/dagger/src/main/java/io/dagger/client/** runtime and core +/dagger/src/main/java/io/dagger/client/modules/** one package per target +/dagger/src/main/resources/META-INF/services/** the descriptor provider +``` + +For the user's build to see them, the SDK inserts exactly one element into the +scope's `pom.xml`: a `` with the id `dagger-clients`, carrying a +generated-by marker comment and activated by the presence of +`dagger/src/main/java`. The profile + +- adds `dagger/src/main/java` as a source root and + `dagger/src/main/resources` as a resource root, through + `build-helper-maven-plugin`; +- declares the SDK's run-time dependencies at explicit versions, because the + user's project does not inherit this repository's dependency management: + `jakarta.json-api`, `jakarta.json.bind-api`, `slf4j-api`, the OpenTelemetry + API, SDK and OTLP exporter, and `yasson` as the JSON-B implementation at + runtime. It does **not** add a logging implementation; the user's project + chooses one. + +Generation refuses to write when the scope's `pom.xml` already contains a +profile with the id `dagger-clients` that does not carry the marker, and says +so. The insertion is otherwise idempotent, and deleting the profile removes the +integration. + +The generated code needs Java 17. A scope whose `maven.compiler.release` is +lower will not compile it, and the README says so. + +This repository already generates a self-activating profile of the same shape: +`dagger-vendored-sdk-jar` in `templates/default/pom.xml`. That precedent is in +an SDK-owned template rather than a user-owned file, which is why the marker, +the same-id refusal and the enumerated dependencies are all part of this design +rather than assumed. + +The TypeScript SDK deliberately does not edit the user's `package.json` and asks +for a one-line file dependency instead. Maven has no equivalent of a file +dependency on a source directory, and every alternative — a parent pom, a build +extension, a locally installed artifact, a generated child module — needs either +more project configuration or a changed `mvn` invocation. Writing one marked, +removable element is the smaller imposition. + +The standalone path roots the workspace at the workspace root to do its work and +restores the scope as the cwd on the way out, for the same reason the module +path does: the engine resolves a target's local path relative to +`Workspace.cwd`, and it rejects a `generateScope` result whose cwd is not the +scope. + +A standalone scope gets no `dagger-module.toml`. Its target set lives in +`dagger.toml` and in the generated descriptors. + +## Alternatives considered + +**Keep one flat generated package and merge the targets' schemas into it.** This +is what the repository does today for a module's dependencies, and what the +TypeScript SDK does for a standalone package. It is less code. It fails the +central goal: the contents of `io.dagger.client` would still depend on the whole +target set, so the bindings for one target would still differ between a module +that has one target and a program that has three. + +**Generate a self-contained package per target, core included.** This is what +the Go SDK does for standalone clients. It removes the need for a shared core +and for a cross-package type registry. It is wrong for Java for the same reason +it is a known weakness in Go: two targets would carry two unrelated `Directory` +classes, and a `Directory` returned by one target could not be passed to the +other. Fully qualifying the names makes the code compile and does not make the +values interchangeable. + +**Keep the accessor on core instead of emitting an entry point per target.** +This is what an earlier draft of this design proposed: `Query.target()` is an +ordinary field the existing visitor already generates, and only its return type +is new, so it costs nothing and leaves every `dag().target()` call site alone. +It was rejected once the goal was stated as a client being autonomous. An +accessor on core makes core depend on the target set, which means the core a +module gets and the core a standalone project gets are different files whenever +their target sets differ — the very thing this design is trying to remove. It +also makes a client something you reach *through* the global session rather than +something you import. Moving the field onto the target's own root type costs the +call-site change and a way of re-homing the receiver, and buys a core that is +the same bytes everywhere and a client package that is the whole of its own +integration. + +**Generate a client package for the module's own types.** A module would then +reach itself the way it reaches anything else. The engine produces a module's +own client-facing schema only by loading the module, which means building it, +which is what generation is producing — a circle. The abandoned attempt broke it +with a second full generation pass, at the cost of a second Maven invocation for +every module generation. It is not needed here: a module's own types are its own +hand-written Java, and the current SDK does not generate them either, because +the module-facing schema holds core and dependencies only. + +**Split the session from the core client.** The abandoned attempt moved the +hand-written runtime to `io.dagger.sdk`, the generated core to `io.dagger.core`, +made `Dagger.dag()` return a new `Session` handle, and made core reachable as +`core(dag())` so that core would be "a target like any other". The symmetry is +real. The cost is that `dag().container()`, the most common expression in every +Java module, becomes `core(dag()).container()`, every existing module must be +rewritten, and the annotation processor's many references to core types move +with it. The design here gets package separation without that. A `Session` type +that owns the connection instead of a generated class remains a reasonable +tidy-up on its own; it is not part of this change. + +**Serve every target eagerly when the session opens.** Simpler than serving from +the entry point: one bootstrap, run once. Rejected because it makes an unusable +target that no code calls break core and every other target, and because it pays +for every target on every run. + +**Serve every target from the client package in both scope kinds.** Adopted for +git targets, where it holds: a git reference plus its pin reproduces the +canonical source identity, `Module.serve` deduplicates, and the package comes out +identical on both sides. Not adopted for a workspace path, because a module +runtime has no filesystem session attachable and cannot resolve one at all — so +this is blocked on the engine rather than on a judgement about reliability. Until +it lands, a local target inside a module is served by the engine from the +manifest, and that is the one place the two scope kinds still differ. + +**Have the standalone client rely on `dagger session +--load-workspace-modules`.** The engine supports this and the flag is already +plumbed to a parameter Java ignores. It does not solve the problem: +`dagger module client add` does not install the target as a workspace module, so +the flag serves whatever `[modules]` happens to list, which is configured +independently of the target set the bindings were generated from. It would also +serve unrelated modules, and would not work for a client distributed outside the +workspace it was generated in. The flag is wired up because it is cheap and the +parameter already exists, but generated clients do not use it. + +## Affected components + +| Component | Change | +| --- | --- | +| `sdk/dagger-codegen-maven-plugin` | Read `@sourceMap` attribution; partition a schema into core and one target; validate names; resolve type references through a registry so more than one output package is possible; take a generation plan instead of a single schema; emit a client's entry points and the descriptor they serve; a goal that inserts the Maven profile. | +| `sdk/dagger-java-sdk` | Public query transport so generated code outside `io.dagger.client` can build queries; `ModuleTarget` and `ModuleTargets`; `CLISession`; the `Connection` fallback and the `--load-workspace-modules` flag; a synchronized `Dagger.dag()`. | +| `codegen.dang` (new) | Build a plan, run the plugin, vendor the result. Shared by both scope kinds. | +| `mod.dang` | Build a module scope's plan: core from the module-facing schema, one entry per recorded target. | +| `client.dang` (new) | Build a standalone scope's plan, merge core, emit the descriptors, insert the Maven profile. | +| `main.dang` | Route `generateScope` with `isModule: false` to the standalone path instead of raising. | +| `templates/*/pom.xml` | No change: a module's layout is unchanged. | +| `.dagger/modules/e2e` | Checks for both scope kinds, including the byte-identity check. | +| `dagger.toml` | Install the `e2e` module, so its checks run against the released engine. | + +## Testing + +**Unit, in the codegen plugin.** It already has a JUnit suite and a helper that +compiles generated output; these extend it. + +- Partitioning by `@sourceMap`: a target's owned types go to its package; a + target's contributed fields on `Query` and on `Binding` stay on the core + class; a core-only schema partitions to itself. +- The type registry resolves a core type referenced from a client package to + `io.dagger.client`. +- Plan execution: a two-target plan emits three packages and one `Client`. +- Core merge: two targets contributing to `Binding` merge; two targets whose + bare cores differ are refused, and the message names both targets and both + engine versions; reversing the target order changes nothing. +- Name validation: a target whose name normalizes to something Java reserves, or + to the same package segment as another target, is refused with both names in + the message. Two targets that own a type of the same name are refused too, as + is a target owning a name core already has. +- A target that yields core alone, because it has no runtime SDK, is refused + with the target named. +- Pom insertion: into a minimal pom; twice, changing nothing the second time; + into a pom with an unrelated profile; refused for a same-id profile with no + marker; over a namespaced pom, a pom with comments, and CRLF line endings. + +**Unit, in the SDK library.** `CLISessionTest` against a fake CLI script: +success, malformed JSON, out-of-range port, empty token, no output before the +timeout, early exit, and double close. `ModuleTargetsTest` against a fake +engine: the expected serve query, no second query for the same name, no query +at all when no descriptor is registered, and one failing target not blocking +another. + +**Behaviour preserved by the registry refactor.** Generate from a real schema +before and after, and compare the normalized output with an explicit allowlist +for known-equivalent spellings. Compile both. + +**End to end, in `.dagger/modules/e2e`.** These run inside an engine that has +the SDK-module interface, driven by `engine-e-2-e:sdk-contract-check`. They also +run against the released `v1.0.0-beta.13` directly, which is how they were +developed: the whole suite passes there in about ninety seconds with no engine +built from source. + +- A module scope with one target generates `io.dagger.client.modules.`, + the client package carries its own entry point, and core names the package + nowhere. This replaces the present `generateScopeClientsCheck` assertion that + looks for the target's types in the flat package. +- `clientOptionalArgsCheck` is updated, and it is the one that proves the call + shape rather than describing it: its fixture is module source that gets + compiled, and it calls `clientDefaults()` after a single static import. Both + forms are checked, with the session named and over the ambient one, and so is + the arguments holder now nested in the target's root type. +- A standalone scope with the same target generates a client package whose files + are **byte-identical** to the module scope's, compared as two subtrees rooted + at the client package, since the two sit under different roots. This is the + check that makes the artifact claim testable. It compares a git target, + because that is the kind both scopes load for themselves today. +- A standalone scope with two targets generates two client packages and one + core, and that core names neither of them. +- The standalone scope's `pom.xml` gains the `dagger-clients` profile; a second + generation does not add it twice; the scope's cwd is unchanged. +- Removing a target drops its package, and core is unchanged by its going. +- A git target records the commit it resolved to in the descriptor its package + holds; a local target inside a module holds none. +- The generated standalone project compiles with a plain `mvn package` in a + container with Maven and no engine — the only check that builds a standalone + scope rather than reading it. +- A Dagger module that declares a git client loads it at run time: the module is + generated, then called with `dagger call`, and its function reaches the client. + This lives in `engine-e2e` rather than `e2e`, because it needs a CLI and an + engine rather than a Workspace. It is the only check that runs a generated + module, so without it the serve a client package performs on first use is + unproven — every other check stops at generating or compiling. + +To hold the cost down, checks that need only *a* module reuse one module name, +as the existing checks do, because the SDK jars are installed under a +per-module-name Maven version and a new name pays for a whole vendored SDK +build. The two scopes in the byte-identity check must differ, so that check pays +for one extra build. + +**Manual, once, with the procedure recorded in the README.** A standalone Maven +project run with plain `java -jar`, calling a target, against a real engine. +The end-to-end checks cannot cover it: they run inside a session that already +exists, and the one container that does drive the CLI, `engine-e2e`'s +playground, carries neither a JDK nor Maven, so an in-engine version of this +check would install a toolchain and run a full `mvn package` inside a nested +engine. + +## Risks + +**A target that moves is served silently against stale bindings.** The engine +compares identity only when a module of that name is *already* served +(`serveModule`), and a standalone program serves into an empty set. So a branch +that advanced, or a local target that was edited, is served without complaint +and the generated bindings no longer match it. The descriptor records the +resolved commit for a git target to close this, independent of the manifest +`lock` setting, because manifest pinning is about how the engine resolves a +dependency and descriptor pinning is about what the generated code must talk to. +A local target cannot be pinned this way, and regeneration after editing it is +required. + +**A local target needs a workspace at run time.** `currentWorkspace.moduleSource(path)` +resolves from the session's working directory. A jar run outside the workspace +it was generated in can serve git targets and cannot serve local ones. Git +targets are the distributable form. + +**The inserted Maven profile can conflict with the user's build.** It adds a +source root, a resource root, `build-helper-maven-plugin`, and a fixed set of +dependency versions. A project that already compiles `dagger/src/main/java`, +that forbids `build-helper-maven-plugin`, or that pins one of those dependencies +to a different version, will conflict. The profile is one marked element and is +removable, generation refuses to overwrite an unmarked profile of the same id, +and the generated tree is inert without the profile. + +**Breaking change for existing modules that use targets.** A target's types move +from `io.dagger.client.` to +`io.dagger.client.modules..`. Call sites are unchanged, imports +are not, and the nested arguments class stays where it is. Targets are recent +and the change is mechanical, so no compatibility shim is proposed. An +end-to-end check compiles a module written against the old layout after +migration, and the README documents the move. + +**Size.** This changes the code generator, the runtime library, the generation +driver and the test suite together. See **On shipping this as one change**. + +## Generation, end to end + +```mermaid +graph TD + E["engine: generateScope(ws, isModule, name, clients)"] --> R{isModule} + + R -->|true| MS["mod.dang
core: moduleSource(scope).introspectionSchemaJSON"] + R -->|false| CS["client.dang
core: merge of every target's client schema"] + + MS --> CE + CS --> CE + + CE["one entry per target:
target.clientSchemaIntrospectionJSON,
owned types only"] --> P["codegen.dang: the plan"] + P --> G["dagger-codegen-maven-plugin
one Maven invocation, every package"] + + G --> CORE["io.dagger.client
core types, all their fields"] + G --> CLI["io.dagger.client.modules.<target>
one package per target"] + + CORE --> OUT + CLI --> OUT + OUT["vendored into the scope"] --> M2{isModule} + M2 -->|true| EP["annotation processor: the module entry point"] + M2 -->|false| BS["target descriptors, service file,
Maven profile"] +``` + +## Serving, at run time + +```mermaid +sequenceDiagram + participant App as Java program + participant SDK as io.dagger.client.Dagger + participant CLI as dagger session + participant Eng as engine + + App->>SDK: dag() + alt DAGGER_SESSION_PORT is set + SDK->>Eng: attach to the existing session + else no session + SDK->>CLI: start dagger session + CLI-->>SDK: {"port", "session_token"} + SDK->>Eng: attach + end + SDK-->>App: Client + App->>SDK: TheTarget.theTarget(dag) + alt the package carries a descriptor + SDK->>Eng: moduleSource(ref).withName(name).asModule().serve() + else it carries none (a local target inside a module) + SDK->>SDK: nothing emitted, the engine already served it + end + App->>Eng: someFunction() +``` + +## On shipping this as one change + +The series below is one pull request. It could be two, and the cut is exact: +patches 1 to 12 are the code generator and the runtime library, they build and +test with `mvn` alone, and they change no generated output; patch 13 rebuilds +the committed plugin and 14 onwards are the generation driver, the standalone +scope, the checks and the documentation. + +It is proposed as one because the first half on its own is a package move that +breaks every module using a target and delivers no new ability in exchange. A +reviewer who wants the halves separately can take the cut at patch 12 as given. + +## The patch series + +Built with Stacked Git on `24f430a529a5aa07b0d3ca64417d8f460394f004`. Every +patch carries `Signed-off-by: Yves Brissaud `. Patches 1 to 12 +are the code generator and the runtime library, and build and test with `mvn` +alone; 13 onwards are the generation driver, the standalone scope, the checks +and the documentation. + +1. **`hack/designs: spec unified client generation`** — this document. +2. **`sdk: apply the formatter`** — five files had drifted from what + `fmt-maven-plugin` produces, so every build rewrote them and every patch had + to be checked for the noise. Unrelated to the feature, and done first so it + stops recurring. +3. **`codegen: read @sourceMap module attribution`** — `Directive`, `Type` and + `Field` learn which module the engine attributes a type or field to. The + argument arrives JSON-encoded, so the quotation marks are part of the value. +4. **`codegen: partition a schema into core and one module`** — `SchemaPartition`. + Core keeps every unowned type with all its fields; a client keeps only the + types its module owns. An empty client partition is refused, which is also + how a target with no runtime SDK is caught. +5. **`codegen: map a module name to a Java package, and refuse a set it cannot + separate`** — `ModulePackage`. The comparison is case-insensitive, because a + case-sensitive filesystem is not the only kind these packages are written to. +6. **`codegen: resolve type references through a registry`** — `TypeRegistry`, + threaded through every visitor and `CodeWriter`. Behaviour-preserving, and + measured: generating from a real `v1.0.0-beta.13` schema before and after + differs in exactly one way across 114 files, `executeQuery(java.lang.String.class)` + becoming `executeQuery(String.class)`, because a `ClassName` lets javapoet + elide the implicit `java.lang` import. +7. **`sdk: make the query transport public API`** — generated code outside + `io.dagger.client` has to be able to build a query. +8. **`sdk: serve a target on first use`** — `ModuleTarget` and + `ModuleTargets.serve`, which takes the descriptor its caller holds rather + than looking one up. +9. **`sdk: open a session when there is none`** — `CLISession`, the `Connection` + fallback, `--load-workspace-modules` wired through, and a `Dagger.dag()` that + two threads cannot race into starting two engines. +10. **`codegen: generate every package a plan names in one pass`** — + `GenerationPlan`, `Generator`, the `-Ddagger.plan` parameter, and the + descriptor a plan entry carries emitted as a constant its entry points + serve. Generated constructors become public here: package-private was + correct only while everything was one package. +11. **`codegen: merge core from the targets when a scope has none`** — + `SchemaMerge`, and the refusal when targets disagree. +12. **`codegen: add a client-pom goal to register generated clients`** — the + goal that writes one marked profile into a pom the SDK does not own. It + splices text rather than re-serializing, so nothing else in the file moves. +13. **`prebuilt: rebuild the codegen plugin`** — before the first patch that + generates with it. Generation seeds the local Maven repository from + `prebuilt/m2` whenever it exists and never compiles the plugin sources in + that case, so a driver change without this would run the old generator. +14. **`java-sdk: generate one package per target`** — `codegen.dang`, and + `mod.dang` driving it. The generated layout changes here, and the checks + that cover the move land with it. +15. **`java-sdk: generate standalone client scopes`** — `client.dang`, the + routing in `main.dang`, the descriptors, the pom registration, and the two + checks that matter most: that a standalone scope generates, and that the + package it generates has the same digest as the module scope's. +16. **`README: document standalone clients`**. + +Three things differ from what this document first planned, and the reasons are +worth keeping. The formatter patch was not planned; it was added because the +drift made every other patch noisy. `codegen.dang` and the per-target layout +landed as one patch rather than two, because the intermediate — a driver +refactor that changes no output — does not exist once the plan format itself is +what changes. And the plan's last patch, installing the `e2e` module in +`dagger.toml` so its checks run against the released engine, was dropped: the +finding behind it is real and recorded below, but acting on it changes what CI +runs, which is not this feature's business. + +## What was taken from the abandoned attempt, and what was not + +The attempt at `848fc622b4c83dc16e226e802f17c76f66c2cf3b`, on the branch +`module-max` of the fork `github.com/eunomie/java-sdk`, was made while the +SDK-module interface was still unmerged and being force-pushed. Its 26 commits +divide cleanly. + +**Superseded by this repository's `main`.** Its SDK provider interface +(`clientScope` / `generateModule` / `generateClient`) is an earlier shape of +`dagger/dagger#13992` that the merged engine does not accept. Its hand-written +manifest writing is replaced by `github.com/dagger/sdk-helpers`, which also +handles the pre-1.0 `dagger.json` migration and the `lock` pin. Its Maven cache +locking and its engine version pin both landed independently. Its +defaulted-argument fix landed as `f4af6598639abd2682a26e2a84098fa45a542084`, in +a better place — on `InputObject`, where the default value lives, rather than as +a static helper on `Field`. That last one is the single file both sides changed, +`introspection/Field.java`, and it is why the abandoned series does not rebase +cleanly as a whole. + +**Taken.** The generator and the runtime: `@sourceMap` attribution, schema +partitioning, the type registry, the plan, the serve preamble and the CLI +session. `main` has changed `sdk/dagger-codegen-maven-plugin` once since the two +diverged, in `Field.java`, and has not changed `sdk/dagger-java-sdk` at all. + +**Reused with changes.** The serve preamble and the CLI session were written +after the abandoned attempt's package move, so they refer to `io.dagger.sdk`. +This design keeps the runtime where it is, so those two are re-authored rather +than cherry-picked. The serve preamble additionally becomes lazy and per target +rather than eager and per session. + +**Rejected on the merits.** The session and core split, and the self client. See +**Alternatives considered**. + +## Progress + +- **Phase 0, done.** Repository confirmed as `dagger/java-sdk`. Base is + `24f430a529a5aa07b0d3ca64417d8f460394f004`. Design home is `hack/designs`. + Stacked Git. GitHub. Sign-off trailer, no AI attribution. +- **Phase 1 and 2, done.** This document. +- **Phase 3, done.** Two independent reviewers, a skeptic and a + design/spec-compliance reviewer, read the first draft. The revisions they + caused: core keeps module-contributed fields on core types, which removed the + need for an accessor synthesizer; the standalone core is merged and skew is + refused, replacing "take the first target's core"; the self client is dropped + as circular; serving became lazy and per target; the service file is a + resource root, not a source root; the prebuilt plugin is rebuilt before the + first driver patch; the drift risk was corrected from loud to silent; the + engine-namespacing claim was corrected from "cannot collide" to "reduces + collisions"; name validation was added; the Maven profile grew a marker, a + same-id refusal and enumerated dependencies; `CLISession` grew a timeout, + graceful shutdown and a synchronized `dag()`; and the vocabulary was settled + on one word per concept. +- **Phase 4, done.** The sixteen patches above. Two defects that only running + the thing could find: every generated constructor was package-private, which + is correct in one package and fatal across two; and the first patch order had + the generator emitting calls into runtime API that arrived three patches + later. +- **Phase 5, done.** Two independent reviewers read the implemented series, one + for correctness and one for design. They converged on two defects that block: + a local target's descriptor recorded the path the engine resolves from the + session's working directory rather than from the workspace root, so it worked + only from the workspace root — reproduced against a real engine before it was + believed; and the guard this document promises for a module scope existed only + on the standalone branch. + + The second one changed what this document says. The rule here was written as + equality, and equality is wrong in that direction: a module's core is the + narrowed, module-facing rendering and a target's is the full client-facing + one, so they are never equal. Measured against a real engine, for two + independent pairs, the module-facing core turned out to be a strict subset of + the client-facing one with identical shapes — 110 types against 115, nothing + present only on the module side, and no shape difference on anything shared. + The rule the code implements is therefore coverage rather than equality: + everything the module sees, the target must have with the same shape, and the + target may have more. + + The rest: a duplicate type name across two targets silently reassigned a + package, which is now refused; the skew comparison looked at names alone and + now compares kinds, rendered type references and argument types; its message + named nothing and now names both targets and the first difference; the CLI was + force-killed five seconds after close, where a cache export needs minutes; two + providers claiming one target resolved by classpath order; the user's pom was + written in place rather than atomically, and a pom with a byte-order mark was + refused. Deliberately not done: `ClientPom` infers the indentation of the + block it inserts from the file around it, which is about sixty lines more than + the problem needs. It is well covered by tests, and shrinking it late is a + worse trade than leaving it. + + One defect surfaced only from running the repository's own check suite rather + than its tests: `main.dang` is generated from `main.dang.tmpl` by the + `templates` module, and editing the output without the source leaves + `templates:generate` reporting unapplied changes. Both patches that touch + `main.dang` now carry the matching template edit. From 93913faa8fb2b2c361065d55121c46d9b6da176e Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:49:25 +0200 Subject: [PATCH 02/28] sdk: apply the formatter Five files drifted from what fmt-maven-plugin produces, so every build rewrites them and every patch has to be checked for the noise. Apply it once. Signed-off-by: Yves Brissaud --- .../introspection/NullableObjectCodegenTest.java | 13 +++++-------- .../client/exception/DaggerExceptionUtils.java | 4 +--- .../io/dagger/client/graphql/GraphQLClient.java | 7 ++----- .../io/dagger/client/graphql/GraphQLResponse.java | 4 +--- .../io/dagger/client/graphql/GraphQLValues.java | 4 ++-- 5 files changed, 11 insertions(+), 21 deletions(-) diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java index 4631f75..aac1e82 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java @@ -132,8 +132,8 @@ void coercedNonNullInterfaceFieldPreservesCovariantReturn() throws Exception { } /** - * Interfaces that merely share an ancestor do not impose an override obligation on one another. - * A nullable field on one sibling must not make a same-named non-null field on another sibling + * Interfaces that merely share an ancestor do not impose an override obligation on one another. A + * nullable field on one sibling must not make a same-named non-null field on another sibling * Optional when their common ancestor does not declare that field. */ @Test @@ -154,24 +154,21 @@ void nullableFieldDoesNotPropagateBetweenSiblingInterfaces() throws Exception { Type nullableImplementation = type("NullableImplementation", TypeKind.OBJECT); nullableImplementation.setInterfaces( List.of( - typeRef(TypeKind.INTERFACE, "NullableSibling"), - typeRef(TypeKind.INTERFACE, "Root"))); + typeRef(TypeKind.INTERFACE, "NullableSibling"), typeRef(TypeKind.INTERFACE, "Root"))); nullableImplementation.setFields( List.of(field("child", typeRef(TypeKind.OBJECT, "Foo"), nullableImplementation))); Type implementation = type("NonNullImplementation", TypeKind.OBJECT); implementation.setInterfaces( List.of( - typeRef(TypeKind.INTERFACE, "NonNullSibling"), - typeRef(TypeKind.INTERFACE, "Root"))); + typeRef(TypeKind.INTERFACE, "NonNullSibling"), typeRef(TypeKind.INTERFACE, "Root"))); implementation.setFields( List.of(field("child", nonNull(typeRef(TypeKind.OBJECT, "Foo")), implementation))); Map generated = sources(root, nullableSibling, nonNullSibling, nullableImplementation, implementation); - assertThat(generated.get("io.dagger.client.NullableSibling")) - .contains("Optional child()"); + assertThat(generated.get("io.dagger.client.NullableSibling")).contains("Optional child()"); assertThat(generated.get("io.dagger.client.NonNullSibling")) .contains("Foo child();") .doesNotContain("Optional child()"); diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionUtils.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionUtils.java index 66dc581..2fd1a39 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionUtils.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionUtils.java @@ -58,9 +58,7 @@ public static String getStdErr(GraphQLError error) { public static String toSimpleMessage(GraphQLError... errors) { return Arrays.stream(errors) - .map( - e -> - String.format(SIMPLE_MESSAGE, e.getMessage(), join(getPath(e), "."), getType(e))) + .map(e -> String.format(SIMPLE_MESSAGE, e.getMessage(), join(getPath(e), "."), getType(e))) .collect(Collectors.joining("\n")); } diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLClient.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLClient.java index 538980a..26af6e2 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLClient.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLClient.java @@ -14,9 +14,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** - * Minimal synchronous GraphQL-over-HTTP client for the Dagger session endpoint. - */ +/** Minimal synchronous GraphQL-over-HTTP client for the Dagger session endpoint. */ public final class GraphQLClient implements AutoCloseable { private final HttpClient http; @@ -28,8 +26,7 @@ public GraphQLClient(String url, String sessionToken, Map extraH this.endpoint = URI.create(url); this.headers = new LinkedHashMap<>(extraHeaders); String encodedToken = - Base64.getEncoder() - .encodeToString((sessionToken + ":").getBytes(StandardCharsets.UTF_8)); + Base64.getEncoder().encodeToString((sessionToken + ":").getBytes(StandardCharsets.UTF_8)); this.headers.put("authorization", "Basic " + encodedToken); // Daemon threads so a module entrypoint exits even if close() is skipped this.executor = diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLResponse.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLResponse.java index 380518b..8a462a2 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLResponse.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLResponse.java @@ -7,9 +7,7 @@ import java.io.StringReader; import java.util.List; -/** - * A parsed GraphQL response payload ({@code {"data": ..., "errors": [...]}}). - */ +/** A parsed GraphQL response payload ({@code {"data": ..., "errors": [...]}}). */ public final class GraphQLResponse { private final JsonObject data; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLValues.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLValues.java index 90cb425..059c547 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLValues.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLValues.java @@ -6,8 +6,8 @@ /** * Renders Java values as GraphQL literals. Supported inputs are the normalized argument values - * produced by io.dagger.client.Arguments: null, String, Integer, Long, Boolean, List and Map - * (input objects). + * produced by io.dagger.client.Arguments: null, String, Integer, Long, Boolean, List and Map (input + * objects). */ public final class GraphQLValues { From 4f1c6994a25bd1e4ff66ec06e10d657bbd1a3f34 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:20:02 +0200 Subject: [PATCH 03/28] codegen: read @sourceMap module attribution The engine attributes every type and field a module contributes with a @sourceMap directive naming that module. Nothing in codegen read it, so a dependency's types were indistinguishable from core once generated. Signed-off-by: Yves Brissaud --- .../codegen/introspection/Directive.java | 25 ++++++ .../dagger/codegen/introspection/Field.java | 6 ++ .../io/dagger/codegen/introspection/Type.java | 7 ++ .../SourceMapAttributionTest.java | 79 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Directive.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Directive.java index d863863..6372bea 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Directive.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Directive.java @@ -64,6 +64,31 @@ public static String getExpectedType(List directives) { return null; } + /** + * Get the owning module name from a list of directives. The engine attributes every type and + * field a module contributes with @sourceMap(module: "name"); core carries no module, so core + * returns null. + */ + public static String getSourceMapModule(List directives) { + if (directives == null) { + return null; + } + for (Directive d : directives) { + if ("sourceMap".equals(d.getName())) { + String val = d.getArgValue("module"); + if (val == null) { + return null; + } + // The engine sends the argument JSON-encoded, so the quotes are part of the value. + if (val.length() > 1 && val.startsWith("\"") && val.endsWith("\"")) { + val = val.substring(1, val.length() - 1); + } + return val.isEmpty() ? null : val; + } + } + return null; + } + @Override public String toString() { return "Directive{" + "name='" + name + '\'' + ", args=" + args + '}'; diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Field.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Field.java index aecf0ab..194ae5b 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Field.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Field.java @@ -81,6 +81,12 @@ public void setParentObject(Type parentObject) { this.parentObject = parentObject; } + /** The module that contributes this field, or null when the engine core owns it. */ + @JsonbTransient + public String getOwningModule() { + return Directive.getSourceMapModule(directives); + } + public List getDirectives() { return directives; } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java index f80f3ba..751bd0b 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java @@ -2,6 +2,7 @@ import static java.util.Comparator.comparing; +import jakarta.json.bind.annotation.JsonbTransient; import java.util.List; public class Type { @@ -83,6 +84,12 @@ public void setPossibleTypes(List possibleTypes) { this.possibleTypes = possibleTypes; } + /** The module that contributes this type, or null when the engine core owns it. */ + @JsonbTransient + public String getOwningModule() { + return Directive.getSourceMapModule(directives); + } + public List getDirectives() { return directives; } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java new file mode 100644 index 0000000..c1a67b6 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java @@ -0,0 +1,79 @@ +package io.dagger.codegen.introspection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class SourceMapAttributionTest { + + @Test + void aTypeTheEngineAttributesToAModuleReportsThatModule() throws Exception { + Schema schema = parse(SCHEMA); + + assertThat(typeNamed(schema, "ClientDep").getOwningModule()).isEqualTo("client-dep"); + } + + @Test + void aCoreTypeReportsNoModule() throws Exception { + Schema schema = parse(SCHEMA); + + assertThat(typeNamed(schema, "Container").getOwningModule()).isNull(); + } + + @Test + void aFieldAModuleContributesToACoreTypeReportsThatModule() throws Exception { + Schema schema = parse(SCHEMA); + Type query = typeNamed(schema, "Query"); + + assertThat(fieldNamed(query, "clientDep").getOwningModule()).isEqualTo("client-dep"); + assertThat(fieldNamed(query, "container").getOwningModule()).isNull(); + } + + @Test + void aDirectiveWithNoModuleArgumentOrAnEmptyOneReportsNoModule() throws Exception { + Schema schema = parse(SCHEMA); + + assertThat(typeNamed(schema, "Anonymous").getOwningModule()).isNull(); + assertThat(typeNamed(schema, "Unattributed").getOwningModule()).isNull(); + } + + private static Schema parse(String json) throws Exception { + return Schema.initialize( + new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.13"); + } + + private static Type typeNamed(Schema schema, String name) { + return schema.getTypes().stream() + .filter(type -> name.equals(type.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("no type named " + name)); + } + + private static Field fieldNamed(Type type, String name) { + return type.getFields().stream() + .filter(field -> name.equals(field.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("no field named " + name)); + } + + // The engine sends a directive argument JSON-encoded, so "client-dep" arrives with its quotes. + private static final String SCHEMA = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []}, + {"name": "clientDep", "type": {"kind": "OBJECT", "name": "ClientDep"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"client-dep\\""}]}]} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "ClientDep", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"client-dep\\""}]}]}, + {"kind": "OBJECT", "name": "Anonymous", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "filename", "value": "\\"main.go\\""}]}]}, + {"kind": "OBJECT", "name": "Unattributed", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"\\""}]}]} + ]}} + """; +} From b7ea7984474dba711bdb3e599dca57c62fdade68 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:21:54 +0200 Subject: [PATCH 04/28] codegen: partition a schema into core and one module Core is every type no module owns, with every module-contributed field removed, so it names no client package. A client is every type its module owns, plus the fields that module contributes to core types, which have no class of their own and are emitted as entry points on the module's root. Signed-off-by: Yves Brissaud --- .../dagger/codegen/introspection/Schema.java | 7 +- .../introspection/SchemaPartition.java | 160 +++++++++++++++++ .../io/dagger/codegen/introspection/Type.java | 21 +++ .../introspection/SchemaPartitionTest.java | 166 ++++++++++++++++++ 4 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/SchemaPartition.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SchemaPartitionTest.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Schema.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Schema.java index fc25676..0944ce8 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Schema.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Schema.java @@ -15,6 +15,10 @@ public class Schema { private static final ComparableVersion NULLABLE_OBJECTS_VERSION = new ComparableVersion("1.0.0-beta.10"); + /** Scalars the generator never emits, because Java already has them. */ + static final List BUILTIN_SCALARS = + List.of("Boolean", "String", "Float", "Int", "DateTime"); + public static class SchemaContainer { @JsonbProperty("__schema") @@ -105,8 +109,7 @@ public void visit(SchemaVisitor visitor) { filteredTypes.stream() .filter(t -> t.getKind() == TypeKind.SCALAR) - .filter( - t -> !List.of("Boolean", "String", "Float", "Int", "DateTime").contains(t.getName())) + .filter(t -> !BUILTIN_SCALARS.contains(t.getName())) .forEach(visitor::visitScalar); filteredTypes.stream() diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/SchemaPartition.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/SchemaPartition.java new file mode 100644 index 0000000..19704b3 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/SchemaPartition.java @@ -0,0 +1,160 @@ +package io.dagger.codegen.introspection; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * One generated package's worth of a schema. + * + *

The engine marks every type and field a module contributes with {@code @sourceMap(module:)}; + * core carries no mark. Java has no partial classes, so a type is generated once, in one package, + * and the split follows from that: + * + *

    + *
  • {@link #core}: every unmarked type, with every module-contributed field removed. Core is + * then the same whichever targets a scope happens to have, and nothing in it names a client + * package. + *
  • {@link #client}: every type one module owns, plus that module's fields on core types — its + * {@link #extensions() extensions}, {@code Query.hello} and {@code Binding.asHello}, which + * have no class of their own and are emitted as static entry points on the module's root + * type. A client package is what a caller needs and nothing else. + *
+ * + *

The whole schema stays reachable through {@link #schema()} for lookups. Only what is emitted + * is narrowed. + */ +public final class SchemaPartition { + + private final Schema schema; + private final String module; + private final List types; + private final Map> extensions; + + private SchemaPartition( + Schema schema, String module, List types, Map> extensions) { + this.schema = schema; + this.module = module; + this.types = types; + this.extensions = extensions; + } + + /** The unmarked part of a schema, with every module-contributed field stripped from it. */ + public static SchemaPartition core(Schema schema) { + List types = + emittable(schema) + .filter(type -> type.getOwningModule() == null) + .map(type -> type.withFields(fieldsOwnedBy(type, null))) + .toList(); + return new SchemaPartition(schema, null, types, Map.of()); + } + + /** + * The part of a schema {@code module} owns. + * + *

An empty result is refused. The engine returns core alone for a module source whose SDK does + * not resolve as a runtime, so a partition with nothing in it means the target cannot be bound, + * not that it is empty. + */ + public static SchemaPartition client(Schema schema, String module) { + Objects.requireNonNull(module, "module"); + List types = + emittable(schema).filter(type -> module.equals(type.getOwningModule())).toList(); + Map> extensions = new LinkedHashMap<>(); + emittable(schema) + .filter(type -> type.getOwningModule() == null) + .forEach( + type -> { + List owned = fieldsOwnedBy(type, module); + if (owned != null && !owned.isEmpty()) { + extensions.put(type.getName(), owned); + } + }); + if (types.isEmpty() && extensions.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "the schema holds nothing owned by module %s; it owns %s." + + " A module source whose SDK is not a runtime yields core alone.", + module, ownedModules(schema))); + } + // Not Map.copyOf: the entry points come out in this order, and an unordered copy would + // reshuffle them from one run to the next. + return new SchemaPartition(schema, module, types, Collections.unmodifiableMap(extensions)); + } + + /** The schema this partition was cut from, whole, for type lookups. */ + public Schema schema() { + return schema; + } + + /** The module this partition emits for, or null for core. */ + public String module() { + return module; + } + + /** The types this partition emits, in schema order. */ + public List types() { + return types; + } + + /** The names of the types this partition emits, in schema order. */ + public List typeNames() { + return types.stream().map(Type::getName).toList(); + } + + /** Core types carrying fields this partition's module contributes, by type name. */ + public Map> extensions() { + return extensions; + } + + /** Every module named by a {@code @sourceMap} mark anywhere in the schema, in schema order. */ + public static List ownedModules(Schema schema) { + return Stream.concat( + emittable(schema).map(Type::getOwningModule), + emittable(schema) + .flatMap(type -> type.getFields() == null ? Stream.of() : type.getFields().stream()) + .map(Field::getOwningModule)) + .filter(Objects::nonNull) + .distinct() + .toList(); + } + + /** + * Walk what this partition emits, in the order the generator needs. + * + *

The emissions that are not per-type — the version constant and the JSON converter — belong + * to core alone. Emitted into a module's package too, they would be a second copy of a class core + * already has. + */ + public void visit(SchemaVisitor visitor) { + types.stream() + .filter(t -> t.getKind() == TypeKind.SCALAR) + .filter(t -> !Schema.BUILTIN_SCALARS.contains(t.getName())) + .forEach(visitor::visitScalar); + types.stream().filter(t -> t.getKind() == TypeKind.INPUT_OBJECT).forEach(visitor::visitInput); + types.stream().filter(t -> t.getKind() == TypeKind.INTERFACE).forEach(visitor::visitInterface); + types.stream().filter(t -> t.getKind() == TypeKind.OBJECT).forEach(visitor::visitObject); + types.stream().filter(t -> t.getKind() == TypeKind.ENUM).forEach(visitor::visitEnum); + if (module == null) { + visitor.visitVersion(schema.getVersion()); + visitor.visitIDAbles( + types.stream().filter(t -> t.getKind() == TypeKind.OBJECT && t.providesId()).toList()); + } + } + + private static Stream emittable(Schema schema) { + return schema.getTypes().stream().filter(t -> !t.getName().startsWith("_")); + } + + private static List fieldsOwnedBy(Type type, String module) { + if (type.getFields() == null) { + return null; + } + return type.getFields().stream() + .filter(f -> Objects.equals(module, f.getOwningModule())) + .toList(); + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java index 751bd0b..d27d422 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Type.java @@ -98,6 +98,27 @@ public void setDirectives(List directives) { this.directives = directives; } + /** + * The same type carrying only the given fields. + * + *

A copy rather than a mutation: the schema this narrows stays whole, so the fields left out + * here are still reachable from it. The fields are shared with it too, and re-parenting them + * would rewrite it; the visitors only ever read the parent's name, which the copy keeps. + */ + Type withFields(List narrowed) { + Type copy = new Type(); + copy.kind = kind; + copy.name = name; + copy.description = description; + copy.inputFields = inputFields; + copy.enumValues = enumValues; + copy.interfaces = interfaces; + copy.possibleTypes = possibleTypes; + copy.directives = directives; + copy.fields = narrowed; + return copy; + } + /** * Checks if this type has an "id" field. With unified IDs, the id field returns the unified ID * scalar. Falls back to legacy FooID check. diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SchemaPartitionTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SchemaPartitionTest.java new file mode 100644 index 0000000..c969e40 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SchemaPartitionTest.java @@ -0,0 +1,166 @@ +package io.dagger.codegen.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SchemaPartitionTest { + + @Test + void coreKeepsEveryUnownedType() throws Exception { + SchemaPartition core = SchemaPartition.core(parse(TWO_MODULES)); + + assertThat(names(core)).containsExactlyInAnyOrder("Query", "Container", "Binding"); + } + + /** Core depends on no client package, so nothing a module contributes stays on it. */ + @Test + void coreDropsTheFieldsModulesContributeToCoreTypes() throws Exception { + SchemaPartition core = SchemaPartition.core(parse(TWO_MODULES)); + + assertThat(fieldNames(core, "Query")).containsExactly("container"); + assertThat(fieldNames(core, "Binding")).isEmpty(); + } + + @Test + void aClientKeepsItsOwnTypes() throws Exception { + Schema schema = parse(TWO_MODULES); + + assertThat(names(SchemaPartition.client(schema, "alpha"))) + .containsExactlyInAnyOrder("Alpha", "AlphaReport"); + assertThat(names(SchemaPartition.client(schema, "beta"))).containsExactly("Beta"); + } + + /** What core drops the contributing module picks up, on {@code Query} and elsewhere alike. */ + @Test + void aClientKeepsTheFieldsItsModuleContributesToCoreTypes() throws Exception { + Schema schema = parse(TWO_MODULES); + + assertThat(extensionNames(SchemaPartition.client(schema, "alpha"))) + .containsExactly(entry("Binding", List.of("asAlpha")), entry("Query", List.of("alpha"))); + assertThat(extensionNames(SchemaPartition.client(schema, "beta"))) + .containsExactly(entry("Query", List.of("beta"))); + } + + @Test + void aClientPartitionDoesNotDependOnWhichOtherModulesAreInTheSchema() throws Exception { + SchemaPartition fromBoth = SchemaPartition.client(parse(TWO_MODULES), "alpha"); + SchemaPartition fromAlone = SchemaPartition.client(parse(ALPHA_ONLY), "alpha"); + + assertThat(names(fromBoth)).isEqualTo(names(fromAlone)); + assertThat(extensionNames(fromBoth)).isEqualTo(extensionNames(fromAlone)); + } + + @Test + void aCoreOnlySchemaPartitionsToItself() throws Exception { + Schema schema = parse(CORE_ONLY); + + assertThat(names(SchemaPartition.core(schema))).containsExactlyInAnyOrder("Query", "Container"); + } + + /** The engine yields core alone for a module source whose SDK does not resolve as a runtime. */ + @Test + void aModuleWithNothingOwnedIsRefused() throws Exception { + Schema schema = parse(CORE_ONLY); + + assertThatThrownBy(() -> SchemaPartition.client(schema, "alpha")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("alpha") + .hasMessageContaining("not a runtime"); + } + + @Test + void ownedModulesReportsEveryModuleTheSchemaMentions() throws Exception { + assertThat(SchemaPartition.ownedModules(parse(TWO_MODULES))).containsExactly("alpha", "beta"); + assertThat(SchemaPartition.ownedModules(parse(CORE_ONLY))).isEmpty(); + } + + private static Schema parse(String json) throws Exception { + return Schema.initialize( + new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.13"); + } + + private static List names(SchemaPartition partition) { + return partition.types().stream().map(Type::getName).toList(); + } + + private static Map> extensionNames(SchemaPartition partition) { + Map> names = new LinkedHashMap<>(); + partition + .extensions() + .forEach((type, fields) -> names.put(type, fields.stream().map(Field::getName).toList())); + return names; + } + + private static List fieldNames(SchemaPartition partition, String typeName) { + return partition.types().stream() + .filter(type -> typeName.equals(type.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("no type named " + typeName)) + .getFields() + .stream() + .map(Field::getName) + .toList(); + } + + private static final String TWO_MODULES = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []}, + {"name": "alpha", "type": {"kind": "OBJECT", "name": "Alpha"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]}, + {"name": "beta", "type": {"kind": "OBJECT", "name": "Beta"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"beta\\""}]}]} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Binding", "fields": [ + {"name": "asAlpha", "type": {"kind": "OBJECT", "name": "Alpha"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]} + ]}, + {"kind": "OBJECT", "name": "Alpha", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]}, + {"kind": "OBJECT", "name": "Beta", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"beta\\""}]}]} + ]}} + """; + + private static final String ALPHA_ONLY = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []}, + {"name": "alpha", "type": {"kind": "OBJECT", "name": "Alpha"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Binding", "fields": [ + {"name": "asAlpha", "type": {"kind": "OBJECT", "name": "Alpha"}, "args": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]} + ]}, + {"kind": "OBJECT", "name": "Alpha", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], + "directives": [{"name": "sourceMap", "args": [{"name": "module", "value": "\\"alpha\\""}]}]} + ]}} + """; + + private static final String CORE_ONLY = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []} + ]}} + """; +} From 8876d8b834e2727a03e6569c3c02f7d55db87644 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Mon, 14 Sep 2026 19:52:47 +0200 Subject: [PATCH 05/28] codegen: read a client's entry points off its schema A module's root type is the return type of the one Query field it owns, not its name: deriving it from the name gives E2e where the engine says E2E. A module that owns no Query field or several, or that is reached as a core type it does not own, is refused. Signed-off-by: Yves Brissaud --- .../introspection/ClientEntryPoint.java | 90 ++++++++++++++ .../introspection/ClientEntryPointTest.java | 116 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java new file mode 100644 index 0000000..26991c4 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java @@ -0,0 +1,90 @@ +package io.dagger.codegen.introspection; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The way into a client package: the fields a module contributes to core types, and the class they + * are emitted on. + * + *

A contributed field has no class of its own — Java generates a type once, in one package, and + * {@code Query} and {@code Binding} belong to core. Each one becomes a static method on the + * module's root type, named after the field, taking the core receiver it was reached through as its + * first argument. {@code Query}'s receiver is the session, so it stays implicit. + * + *

The root type is read off the schema, as the return type of the one {@code Query} field the + * module owns. Deriving it from the module name instead would give {@code E2e} where the engine + * says {@code E2E}. + */ +public final class ClientEntryPoint { + + private static final String QUERY = "Query"; + + private final SchemaPartition client; + private final Field entryField; + + public ClientEntryPoint(SchemaPartition client) { + if (client.module() == null) { + throw new IllegalArgumentException("an entry point needs a client partition, not core"); + } + this.client = client; + this.entryField = requireOneQueryField(client); + String root = entryField.getTypeRef().getTypeName(); + if (!client.typeNames().contains(root)) { + throw new IllegalArgumentException( + String.format( + "module %s is reached as the core type %s, which it does not own, so there is no" + + " class to put its entry points on: a module named after a core type collides" + + " with it. Rename the module, or alias the target.", + client.module(), root)); + } + } + + /** The module this enters. */ + public String module() { + return client.module(); + } + + /** The {@code Query} field the module owns: how a caller constructs its root. */ + public Field entryField() { + return entryField; + } + + /** The GraphQL name of the root type the entry points are emitted on. */ + public String rootTypeName() { + return entryField.getTypeRef().getTypeName(); + } + + /** + * The module's fields on core types other than {@code Query}, by type name, in the partition's + * order so the emitted entry points come out the same on every run. + */ + public Map> shims() { + Map> shims = new LinkedHashMap<>(); + client + .extensions() + .forEach( + (typeName, fields) -> { + if (!QUERY.equals(typeName)) { + shims.put(typeName, fields); + } + }); + return shims; + } + + private static Field requireOneQueryField(SchemaPartition client) { + List fields = client.extensions().getOrDefault(QUERY, List.of()); + if (fields.size() != 1) { + throw new IllegalArgumentException( + String.format( + "module %s owns %d fields on Query, expected exactly one; it owns the types %s and" + + " contributes the Query fields %s", + client.module(), + fields.size(), + client.typeNames(), + fields.stream().map(Field::getName).toList())); + } + return fields.get(0); + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java new file mode 100644 index 0000000..af2ff97 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java @@ -0,0 +1,116 @@ +package io.dagger.codegen.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class ClientEntryPointTest { + + /** The engine camel-cases a module's name to namespace its types, and {@code e2e} becomes E2E. */ + @Test + void theRootTypeIsTheReturnTypeOfTheQueryFieldNotTheModuleName() throws Exception { + ClientEntryPoint entry = entryPoint(E2E, "e2e"); + + assertThat(entry.rootTypeName()).isEqualTo("E2E"); + assertThat(entry.entryField().getName()).isEqualTo("e2e"); + } + + @Test + void aModulesFieldsOnOtherCoreTypesAreShims() throws Exception { + assertThat(entryPoint(E2E, "e2e").shims()).containsOnlyKeys("Binding"); + } + + @Test + void aModuleWithNoQueryFieldIsRefused() throws Exception { + assertThatThrownBy(() -> entryPoint(NO_QUERY_FIELD, "e2e")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("module e2e owns 0 fields on Query") + .hasMessageContaining("E2E"); + } + + @Test + void aModuleWithTwoQueryFieldsIsRefused() throws Exception { + assertThatThrownBy(() -> entryPoint(TWO_QUERY_FIELDS, "e2e")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("module e2e owns 2 fields on Query") + .hasMessageContaining("[e2e, e2eAgain]"); + } + + /** A module named after a core type is reached as that type, and has no class of its own. */ + @Test + void aModuleReachedAsACoreTypeItDoesNotOwnIsRefused() throws Exception { + assertThatThrownBy(() -> entryPoint(NAMED_AFTER_A_CORE_TYPE, "container")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("module container is reached as the core type Container") + .hasMessageContaining("Rename the module"); + } + + private static ClientEntryPoint entryPoint(String json, String module) throws Exception { + Schema schema = + Schema.initialize( + new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.13"); + return new ClientEntryPoint(SchemaPartition.client(schema, module)); + } + + private static String owned(String module) { + return "\"directives\": [{\"name\": \"sourceMap\", \"args\": [{\"name\": \"module\"," + + " \"value\": \"\\\"" + + module + + "\\\"\"}]}]"; + } + + private static final String E2E = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []}, + {"name": "e2e", "type": {"kind": "OBJECT", "name": "E2E"}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Binding", "fields": [ + {"name": "asE2E", "type": {"kind": "OBJECT", "name": "E2E"}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "E2E", "fields": [], %s} + ]}} + """ + .formatted(owned("e2e"), owned("e2e"), owned("e2e")); + + private static final String NO_QUERY_FIELD = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": []} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "E2E", "fields": [], %s} + ]}} + """ + .formatted(owned("e2e")); + + private static final String TWO_QUERY_FIELDS = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "e2e", "type": {"kind": "OBJECT", "name": "E2E"}, "args": [], %s}, + {"name": "e2eAgain", "type": {"kind": "OBJECT", "name": "E2E"}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "E2E", "fields": [], %s} + ]}} + """ + .formatted(owned("e2e"), owned("e2e"), owned("e2e")); + + private static final String NAMED_AFTER_A_CORE_TYPE = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "OBJECT", "name": "Container"}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "ContainerReport", "fields": [], %s} + ]}} + """ + .formatted(owned("container"), owned("container")); +} From a44eafa9d2471ef1e99d3733f02035570d41a548 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:24:48 +0200 Subject: [PATCH 06/28] codegen: map a module name to a Java package, and refuse a set it cannot separate A Dagger name can hold characters a Java package segment cannot, and two distinct names can normalize to the same segment, which would silently make one module's bindings overwrite another's. Signed-off-by: Yves Brissaud --- .../java/io/dagger/codegen/ModulePackage.java | 75 +++++++++++++++++++ .../io/dagger/codegen/ModulePackageTest.java | 67 +++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java new file mode 100644 index 0000000..aee7620 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java @@ -0,0 +1,75 @@ +package io.dagger.codegen; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import javax.lang.model.SourceVersion; + +/** + * The Java package a module's bindings are generated into. + * + *

A module name is a Dagger name, so it can hold characters a Java package segment cannot: + * {@code sdk-helpers} becomes {@code sdkhelpers}. Distinct module names can normalize to the same + * segment, which would make one module's bindings overwrite another's, so the mapping is computed + * for a whole target set at once and refuses a set it cannot separate. + */ +public final class ModulePackage { + + /** The package every module's bindings go under. */ + public static final String ROOT = "io.dagger.client.modules"; + + private ModulePackage() {} + + /** + * Map each module name to its fully qualified package, refusing a set that cannot be separated. + * + *

The comparison that decides separation is case-insensitive, because a case-sensitive + * filesystem is not the only kind these packages are written to. + */ + public static Map packagesFor(List moduleNames) { + Map packages = new LinkedHashMap<>(); + Map claimedBy = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (String moduleName : moduleNames) { + String segment = segmentFor(moduleName); + String previous = claimedBy.putIfAbsent(segment, moduleName); + if (previous != null && !previous.equals(moduleName)) { + throw new IllegalArgumentException( + String.format( + "modules %s and %s both generate into %s.%s; rename or alias one of them", + previous, moduleName, ROOT, segment)); + } + packages.put(moduleName, ROOT + "." + segment); + } + return packages; + } + + /** The package segment for one module name. */ + public static String segmentFor(String moduleName) { + StringBuilder segment = new StringBuilder(moduleName.length()); + for (int i = 0; i < moduleName.length(); i++) { + char c = moduleName.charAt(i); + if (Character.isLetterOrDigit(c) && c < 128) { + segment.append(Character.toLowerCase(c)); + } + } + String candidate = segment.toString(); + if (candidate.isEmpty() || Character.isDigit(candidate.charAt(0))) { + throw new IllegalArgumentException( + String.format( + "module %s does not name a Java package segment; a segment needs a leading ASCII" + + " letter", + moduleName)); + } + if (RESERVED.contains(candidate) || !SourceVersion.isName(candidate)) { + throw new IllegalArgumentException( + String.format("module %s normalizes to %s, which Java reserves", moduleName, candidate)); + } + return candidate; + } + + // isName rejects keywords but not these, which are legal identifiers a package segment may not + // be. + private static final Set RESERVED = Set.of("var", "yield", "record", "sealed", "permits"); +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java new file mode 100644 index 0000000..885e036 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java @@ -0,0 +1,67 @@ +package io.dagger.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ModulePackageTest { + + @Test + void aDaggerNameBecomesALowercaseSegment() { + assertThat(ModulePackage.segmentFor("sdk-helpers")).isEqualTo("sdkhelpers"); + assertThat(ModulePackage.segmentFor("clientDep")).isEqualTo("clientdep"); + assertThat(ModulePackage.segmentFor("my_module2")).isEqualTo("mymodule2"); + } + + @Test + void everyTargetGetsItsOwnPackageUnderTheOneRoot() { + assertThat(ModulePackage.packagesFor(List.of("alpha", "sdk-helpers"))) + .containsExactly( + entry("alpha", "io.dagger.client.modules.alpha"), + entry("sdk-helpers", "io.dagger.client.modules.sdkhelpers")); + } + + @Test + void twoNamesThatNormalizeToOneSegmentAreRefused() { + assertThatThrownBy(() -> ModulePackage.packagesFor(List.of("foo-bar", "foo_bar"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("foo-bar") + .hasMessageContaining("foo_bar") + .hasMessageContaining("io.dagger.client.modules.foobar"); + } + + /** A package directory written to a case-insensitive filesystem separates no better than this. */ + @Test + void segmentsThatDifferOnlyByCaseAreRefused() { + assertThatThrownBy(() -> ModulePackage.packagesFor(List.of("Alpha", "alpha"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void theSameModuleListedTwiceIsNotACollision() { + assertThat(ModulePackage.packagesFor(List.of("alpha", "alpha"))).hasSize(1); + } + + @Test + void aNameWithNoUsableCharactersIsRefused() { + assertThatThrownBy(() -> ModulePackage.segmentFor("--")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("leading ASCII letter"); + assertThatThrownBy(() -> ModulePackage.segmentFor("2fast")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("leading ASCII letter"); + } + + @Test + void aNameJavaReservesIsRefused() { + assertThatThrownBy(() -> ModulePackage.segmentFor("package")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("reserves"); + assertThatThrownBy(() -> ModulePackage.segmentFor("record")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("reserves"); + } +} From 2a6301e1c3b6e01685ba0c2d0549fa43b89e420b Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:28:56 +0200 Subject: [PATCH 07/28] codegen: resolve type references through a registry Every generated reference named its target by simple name, which is only correct while everything lands in one package. Route schema types, the hand-written runtime and the type being written through a TypeRegistry, and let CodeWriter take its package from it. Behaviour is preserved. Generating from a real engine schema (v1.0.0-beta.13, 114 files) before and after this patch differs in exactly one way: executeQuery(java.lang.String.class) becomes executeQuery(String.class), and the same for Boolean and Integer, because a ClassName lets javapoet elide the implicit java.lang import where the interpolated name printed it in full. Signed-off-by: Yves Brissaud --- .../io/dagger/codegen/DaggerCodegenMojo.java | 8 +- .../AbstractMultiTypesVisitor.java | 13 ++- .../introspection/AbstractVisitor.java | 13 ++- .../codegen/introspection/CodeWriter.java | 10 +- .../codegen/introspection/CodegenVisitor.java | 17 +-- .../codegen/introspection/EnumVisitor.java | 4 +- .../dagger/codegen/introspection/Helpers.java | 22 ++-- .../codegen/introspection/IDAbleVisitor.java | 17 +-- .../codegen/introspection/InputVisitor.java | 19 ++-- .../introspection/InterfaceVisitor.java | 62 ++++++----- .../codegen/introspection/ObjectVisitor.java | 104 +++++++++--------- .../codegen/introspection/ScalarVisitor.java | 13 ++- .../dagger/codegen/introspection/TypeRef.java | 30 ++--- .../codegen/introspection/TypeRegistry.java | 67 +++++++++++ .../codegen/introspection/VersionVisitor.java | 4 +- .../NullableObjectCodegenTest.java | 13 ++- .../OptionalArgsCodegenTest.java | 7 +- 17 files changed, 264 insertions(+), 159 deletions(-) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java index 1723597..cfb0417 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java @@ -4,6 +4,7 @@ import io.dagger.codegen.introspection.Schema; import io.dagger.codegen.introspection.SchemaVisitor; import io.dagger.codegen.introspection.Type; +import io.dagger.codegen.introspection.TypeRegistry; import java.io.*; import java.nio.charset.Charset; import java.nio.file.Path; @@ -61,7 +62,12 @@ public void execute() throws MojoExecutionException, MojoFailureException { Path dest = outputDir.toPath(); try (InputStream in = getInstrospectionJson()) { Schema schema = Schema.initialize(in, version); - SchemaVisitor codegen = new CodegenVisitor(schema, dest, Charset.forName(outputEncoding)); + SchemaVisitor codegen = + new CodegenVisitor( + schema, + TypeRegistry.singlePackage("io.dagger.client"), + dest, + Charset.forName(outputEncoding)); schema.visit( new SchemaVisitor() { @Override diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractMultiTypesVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractMultiTypesVisitor.java index 2855b46..5c9a910 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractMultiTypesVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractMultiTypesVisitor.java @@ -8,11 +8,18 @@ abstract class AbstractMultiTypesVisitor extends CodeWriter { - private Schema schema; + private final Schema schema; + private final TypeRegistry registry; - public AbstractMultiTypesVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(targetDirectory, encoding); + public AbstractMultiTypesVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(registry.targetPackage(), targetDirectory, encoding); this.schema = schema; + this.registry = registry; + } + + TypeRegistry registry() { + return registry; } void visit(List types) throws IOException { diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractVisitor.java index f93247e..5d818c0 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/AbstractVisitor.java @@ -14,11 +14,18 @@ abstract class AbstractVisitor extends CodeWriter { - private Schema schema; + private final Schema schema; + private final TypeRegistry registry; - public AbstractVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(targetDirectory, encoding); + public AbstractVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(registry.targetPackage(), targetDirectory, encoding); this.schema = schema; + this.registry = registry; + } + + TypeRegistry registry() { + return registry; } void visit(Type type) throws IOException { diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodeWriter.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodeWriter.java index fdef367..a442e7d 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodeWriter.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodeWriter.java @@ -7,17 +7,19 @@ import java.nio.file.Path; public class CodeWriter { - private Charset encoding; - private Path targetDirectory; + private final String targetPackage; + private final Charset encoding; + private final Path targetDirectory; - public CodeWriter(Path targetDirectory, Charset encoding) { + public CodeWriter(String targetPackage, Path targetDirectory, Charset encoding) { + this.targetPackage = targetPackage; this.encoding = encoding; this.targetDirectory = targetDirectory; } public void write(TypeSpec typeSpec) throws IOException { JavaFile javaFile = - JavaFile.builder("io.dagger.client", typeSpec) + JavaFile.builder(targetPackage, typeSpec) .addFileComment("This class has been generated by dagger-java-sdk. DO NOT EDIT.") .indent(" ") .build(); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java index 1181802..1af89af 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java @@ -15,14 +15,15 @@ public class CodegenVisitor implements SchemaVisitor { private final VersionVisitor versionVisitor; private final IDAbleVisitor idAbleVisitor; - public CodegenVisitor(Schema schema, Path targetDirectory, Charset encoding) { - this.scalarVisitor = new ScalarVisitor(schema, targetDirectory, encoding); - this.inputVisitor = new InputVisitor(schema, targetDirectory, encoding); - this.enumVisitor = new EnumVisitor(schema, targetDirectory, encoding); - this.objectVisitor = new ObjectVisitor(schema, targetDirectory, encoding); - this.interfaceVisitor = new InterfaceVisitor(schema, targetDirectory, encoding); - this.versionVisitor = new VersionVisitor(targetDirectory, encoding); - this.idAbleVisitor = new IDAbleVisitor(schema, targetDirectory, encoding); + public CodegenVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + this.scalarVisitor = new ScalarVisitor(schema, registry, targetDirectory, encoding); + this.inputVisitor = new InputVisitor(schema, registry, targetDirectory, encoding); + this.enumVisitor = new EnumVisitor(schema, registry, targetDirectory, encoding); + this.objectVisitor = new ObjectVisitor(schema, registry, targetDirectory, encoding); + this.interfaceVisitor = new InterfaceVisitor(schema, registry, targetDirectory, encoding); + this.versionVisitor = new VersionVisitor(registry.targetPackage(), targetDirectory, encoding); + this.idAbleVisitor = new IDAbleVisitor(schema, registry, targetDirectory, encoding); } @Override diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/EnumVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/EnumVisitor.java index 867423f..3330927 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/EnumVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/EnumVisitor.java @@ -7,8 +7,8 @@ public class EnumVisitor extends AbstractVisitor { - public EnumVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public EnumVisitor(Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java index bab17a5..9e65f79 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java @@ -65,18 +65,15 @@ public class Helpers { "super", "while"); - static ClassName convertScalarToObject(String typeName, String expectedType) { + static ClassName convertScalarToObject( + TypeRegistry registry, String typeName, String expectedType) { if (expectedType != null && !expectedType.isEmpty()) { - return ClassName.bestGuess(expectedType); + return registry.forType(expectedType); } if (typeName.endsWith("ID") && typeName.length() > 2) { - return ClassName.bestGuess(typeName.substring(0, typeName.length() - 2)); + return registry.forType(typeName.substring(0, typeName.length() - 2)); } - return ClassName.bestGuess(typeName); - } - - static ClassName convertScalarToObject(String typeName) { - return convertScalarToObject(typeName, null); + return registry.forType(typeName); } /** @@ -128,10 +125,15 @@ static List getArrayField(Field field, Schema schema) { } static String formatName(Type type) { - if ("Query".equals(type.getName())) { + return formatName(type.getName()); + } + + /** The Java simple name generated for a GraphQL type name. */ + static String formatName(String graphqlName) { + if ("Query".equals(graphqlName)) { return "Client"; } else { - return capitalize(type.getName()); + return capitalize(graphqlName); } } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/IDAbleVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/IDAbleVisitor.java index 9d7857a..ce91208 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/IDAbleVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/IDAbleVisitor.java @@ -11,8 +11,9 @@ import javax.lang.model.element.Modifier; public class IDAbleVisitor extends AbstractMultiTypesVisitor { - public IDAbleVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public IDAbleVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override @@ -24,7 +25,7 @@ TypeSpec generateType(List types) { .addMethod( MethodSpec.methodBuilder("toJSON") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.bestGuess("JSON")) + .returns(registry().forType("JSON")) .addException(Exception.class) .addParameter(Object.class, "object") .beginControlFlow( @@ -32,15 +33,15 @@ TypeSpec generateType(List types) { Jsonb.class, JsonbBuilder.class, JsonbConfig.class, - ClassName.bestGuess("io.dagger.client.FieldsStrategy")) + registry().runtime("FieldsStrategy")) .beginControlFlow("if (object instanceof $T)", Enum.class) .addStatement( "return $T.from(jsonb.toJson((($T) object).name()))", - ClassName.bestGuess("JSON"), + registry().forType("JSON"), Enum.class) .endControlFlow() .addStatement( - "return $T.from(jsonb.toJson(object))", ClassName.bestGuess("JSON")) + "return $T.from(jsonb.toJson(object))", registry().forType("JSON")) .endControlFlow() .build()) .addMethod( @@ -48,7 +49,7 @@ TypeSpec generateType(List types) { .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addTypeVariable(TypeVariableName.get("T")) .returns(TypeVariableName.get("T")) - .addParameter(ClassName.bestGuess("JSON"), "json") + .addParameter(registry().forType("JSON"), "json") .addParameter( ParameterizedTypeName.get( ClassName.get(Class.class), TypeVariableName.get("T")), @@ -72,7 +73,7 @@ TypeSpec generateType(List types) { Jsonb.class, JsonbBuilder.class, JsonbConfig.class, - ClassName.bestGuess("io.dagger.client.FieldsStrategy")) + registry().runtime("FieldsStrategy")) .beginControlFlow("if (clazz.isEnum())") .addStatement( "$T valueOf = clazz.getMethod($S, $T.class)", diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InputVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InputVisitor.java index 4e48778..792a22c 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InputVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InputVisitor.java @@ -9,8 +9,9 @@ class InputVisitor extends AbstractVisitor { - public InputVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public InputVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override @@ -19,24 +20,26 @@ TypeSpec generateType(Type type) { TypeSpec.classBuilder(Helpers.formatName(type)) .addJavadoc(type.getDescription() != null ? type.getDescription() : "") .addModifiers(Modifier.PUBLIC) - .addSuperinterface(ClassName.bestGuess("InputValue")); + .addSuperinterface(registry().runtime("InputValue")); for (InputObject inputObject : type.getInputFields()) { classBuilder.addField( FieldSpec.builder( - inputObject.getType().formatInput(), inputObject.getName(), Modifier.PRIVATE) + inputObject.getType().formatInput(registry()), + inputObject.getName(), + Modifier.PRIVATE) .build()); classBuilder.addMethod( - Helpers.getter(inputObject.getName(), inputObject.getType().formatInput())); + Helpers.getter(inputObject.getName(), inputObject.getType().formatInput(registry()))); classBuilder.addMethod( - Helpers.setter(inputObject.getName(), inputObject.getType().formatOutput())); + Helpers.setter(inputObject.getName(), inputObject.getType().formatOutput(registry()))); classBuilder.addMethod( Helpers.withSetter( inputObject, - inputObject.getType().formatInput(), - ClassName.bestGuess(Helpers.formatName(type)))); + inputObject.getType().formatInput(registry()), + registry().forType(type.getName()))); } MethodSpec.Builder toMapMethod = diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java index 38313ac..259ed7a 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java @@ -15,8 +15,9 @@ * when loading from ID or returning from fields. */ class InterfaceVisitor extends AbstractVisitor { - public InterfaceVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public InterfaceVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override @@ -42,7 +43,7 @@ TypeSpec generateType(Type type) { // Arguments.Builder overloads. if (type.providesId()) { interfaceBuilder.addSuperinterface( - ParameterizedTypeName.get(ClassName.bestGuess("IDAble"), ClassName.bestGuess("ID"))); + ParameterizedTypeName.get(registry().runtime("IDAble"), registry().forType("ID"))); } if (type.getFields() != null) { @@ -84,7 +85,7 @@ TypeSpec generateType(Type type) { methodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } if (field.isDeprecated()) { @@ -101,7 +102,7 @@ TypeSpec generateType(Type type) { /** Generates the FooClient class that implements the Foo interface via query building. */ TypeSpec generateClientType(Type type) { String clientName = Helpers.formatName(type) + "Client"; - ClassName interfaceName = ClassName.bestGuess(Helpers.formatName(type)); + ClassName interfaceName = registry().forType(type.getName()); TypeSpec.Builder classBuilder = TypeSpec.classBuilder(clientName) @@ -110,13 +111,13 @@ TypeSpec generateClientType(Type type) { .addSuperinterface(interfaceName) .addField( FieldSpec.builder( - ClassName.bestGuess("QueryBuilder"), "queryBuilder", Modifier.PRIVATE) + registry().runtime("QueryBuilder"), "queryBuilder", Modifier.PRIVATE) .build()); // Constructor MethodSpec constructor = MethodSpec.constructorBuilder() - .addParameter(ClassName.bestGuess("QueryBuilder"), "queryBuilder") + .addParameter(registry().runtime("QueryBuilder"), "queryBuilder") .addCode("this.queryBuilder = queryBuilder;") .build(); classBuilder.addMethod(constructor); @@ -181,66 +182,69 @@ private void buildFieldMethod( if (field.getTypeRef().isListOfObject()) { String objName = field.getTypeRef().getListElementType().getName(); - String clientClassName = - field.getTypeRef().getListElementType().isInterface() ? objName + "Client" : objName; + ClassName clientClass = + field.getTypeRef().getListElementType().isInterface() + ? registry().forInterfaceClient(objName) + : registry().forType(objName); fieldMethodBuilder.addStatement( "nextQueryBuilder = nextQueryBuilder.chain(List.of($S))", "id"); fieldMethodBuilder.addStatement( "List builders = nextQueryBuilder.executeObjectListQuery($S)", objName); fieldMethodBuilder.addStatement( - "return builders.stream().map(qb -> new $L(qb)).toList()", clientClassName); + "return builders.stream().map(qb -> new $T(qb)).toList()", clientClass); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (field.getTypeRef().isList()) { fieldMethodBuilder.addStatement( - "return nextQueryBuilder.executeListQuery($L.class)", - field.getTypeRef().getListElementType().getName()); + "return nextQueryBuilder.executeListQuery($T.class)", + field.getTypeRef().getListElementType().formatOutput(registry())); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (Helpers.isIdToConvert(field)) { fieldMethodBuilder.addStatement("nextQueryBuilder.executeQuery()"); fieldMethodBuilder.addStatement("return this"); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (nullableObject) { String graphqlTypeName = field.getTypeRef().getTypeName(); - String clientClassName = + TypeName clientClass = field.getTypeRef().isInterface() - ? graphqlTypeName + "Client" - : objectReturnType.toString(); + ? registry().forInterfaceClient(graphqlTypeName) + : objectReturnType; fieldMethodBuilder.addStatement( "QueryBuilder objectQueryBuilder = nextQueryBuilder.executeNullableObjectQuery($S)", graphqlTypeName); fieldMethodBuilder.addStatement( - "return Optional.ofNullable(objectQueryBuilder).map(qb -> new $L(qb))", - ClassName.bestGuess(clientClassName)); + "return Optional.ofNullable(objectQueryBuilder).map(qb -> new $T(qb))", clientClass); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (field.getTypeRef().isObjectOrInterface()) { // For interface return types, instantiate the client class CodeBlock instantiation = field.getTypeRef().isInterface() - ? CodeBlock.of("new $LClient(nextQueryBuilder)", field.getTypeRef().getTypeName()) - : CodeBlock.of("new $L(nextQueryBuilder)", objectReturnType); + ? CodeBlock.of( + "new $T(nextQueryBuilder)", + registry().forInterfaceClient(field.getTypeRef().getTypeName())) + : CodeBlock.of("new $T(nextQueryBuilder)", objectReturnType); if (presentObject) { fieldMethodBuilder.addStatement("return $T.of($L)", Optional.class, instantiation); } else { fieldMethodBuilder.addStatement("return $L", instantiation); } } else { - fieldMethodBuilder.addStatement("return nextQueryBuilder.executeQuery($L.class)", returnType); + fieldMethodBuilder.addStatement("return nextQueryBuilder.executeQuery($T.class)", returnType); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } if (field.isDeprecated()) { @@ -252,19 +256,19 @@ private void buildFieldMethod( private TypeName resolveReturnType(Field field) { if ("id".equals(field.getName())) { - return field.getTypeRef().formatOutput(); + return field.getTypeRef().formatOutput(registry()); } if (Helpers.isIdToConvert(field)) { // sync-like: return the parent object type - return ClassName.bestGuess(Helpers.formatName(field.getParentObject())); + return registry().forType(field.getParentObject().getName()); } String expectedType = field.getExpectedType(); - return field.getTypeRef().formatInput(expectedType); + return field.getTypeRef().formatInput(registry(), expectedType); } private TypeName resolveArgType(InputObject arg) { String expectedType = arg.getExpectedType(); - return arg.getType().formatInput(expectedType); + return arg.getType().formatInput(registry(), expectedType); } private boolean isNullableObject(Field field) { diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java index 965a68e..ec53087 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java @@ -17,40 +17,39 @@ import javax.lang.model.element.Modifier; class ObjectVisitor extends AbstractVisitor { - public ObjectVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public ObjectVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override TypeSpec generateType(Type type) { + ClassName thisType = registry().forType(type.getName()); TypeSpec.Builder classBuilder = TypeSpec.classBuilder(Helpers.formatName(type)) .addJavadoc(Helpers.escapeJavadoc(type.getDescription())) .addModifiers(Modifier.PUBLIC) .addField( FieldSpec.builder( - ClassName.bestGuess("QueryBuilder"), "queryBuilder", Modifier.PRIVATE) + registry().runtime("QueryBuilder"), "queryBuilder", Modifier.PRIVATE) .build()); // Add implements for any interfaces this object implements for (String ifaceName : type.getImplementedInterfaceNames()) { - classBuilder.addSuperinterface(ClassName.bestGuess(ifaceName)); + classBuilder.addSuperinterface(registry().forType(ifaceName)); } if ("Query".equals(type.getName())) { MethodSpec constructor = MethodSpec.constructorBuilder() - .addParameter( - ClassName.bestGuess("io.dagger.client.engineconn.Connection"), "connection") + .addParameter(registry().runtime("engineconn", "Connection"), "connection") .addStatement("this.connection = connection") .addStatement("this.queryBuilder = new QueryBuilder(connection.getGraphQLClient())") .build(); classBuilder.addMethod(constructor); classBuilder.addField( FieldSpec.builder( - ClassName.bestGuess("io.dagger.client.engineconn.Connection"), - "connection", - Modifier.PRIVATE) + registry().runtime("engineconn", "Connection"), "connection", Modifier.PRIVATE) .build()); MethodSpec closeMethod = MethodSpec.methodBuilder("close") @@ -69,7 +68,7 @@ TypeSpec generateType(Type type) { .addParameter( ParameterizedTypeName.get(ClassName.get(Class.class), TypeVariableName.get("T")), "clazz") - .addParameter(ClassName.bestGuess("ID"), "id") + .addParameter(registry().forType("ID"), "id") .addJavadoc("Load any object by its ID using node(id:) with an inline fragment.\n") .beginControlFlow("try") .addStatement( @@ -85,9 +84,9 @@ TypeSpec generateType(Type type) { classBuilder.addMethod( MethodSpec.methodBuilder("nodeQueryBuilder") .addModifiers(Modifier.PUBLIC) - .returns(ClassName.bestGuess("QueryBuilder")) + .returns(registry().runtime("QueryBuilder")) .addParameter(ClassName.get(String.class), "typeName") - .addParameter(ClassName.bestGuess("ID"), "id") + .addParameter(registry().forType("ID"), "id") .addJavadoc( "Create a QueryBuilder for node(id:) scoped to the given type via an inline fragment.\n") .addStatement("return this.queryBuilder.chainNode(typeName, id)") @@ -105,30 +104,25 @@ TypeSpec generateType(Type type) { if (type.providesId()) { // With unified IDs, id() returns the ID scalar type classBuilder.addSuperinterface( - ParameterizedTypeName.get(ClassName.bestGuess("IDAble"), ClassName.bestGuess("ID"))); + ParameterizedTypeName.get(registry().runtime("IDAble"), registry().forType("ID"))); classBuilder.addAnnotation( AnnotationSpec.builder(JsonbTypeSerializer.class) - .addMember("value", "$T.class", ClassName.bestGuess("IDAbleSerializer")) + .addMember("value", "$T.class", registry().runtime("IDAbleSerializer")) .build()); classBuilder.addAnnotation( AnnotationSpec.builder(JsonbTypeDeserializer.class) - .addMember( - "value", - "$T.class", - ClassName.bestGuess(Helpers.formatName(type) + ".Deserializer")) + .addMember("value", "$T.class", thisType.nestedClass("Deserializer")) .build()); classBuilder.addType( TypeSpec.classBuilder("Deserializer") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addSuperinterface( - ParameterizedTypeName.get( - ClassName.get(JsonbDeserializer.class), - ClassName.bestGuess(Helpers.formatName(type)))) + ParameterizedTypeName.get(ClassName.get(JsonbDeserializer.class), thisType)) .addMethod( MethodSpec.methodBuilder("deserialize") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) - .returns(ClassName.bestGuess(Helpers.formatName(type))) + .returns(thisType) .addParameter(JsonParser.class, "parser") .addParameter(DeserializationContext.class, "ctx") .addParameter(java.lang.reflect.Type.class, "type") @@ -136,11 +130,11 @@ TypeSpec generateType(Type type) { "$T id = ctx.deserialize($T.class, parser)", String.class, String.class) .addStatement( "$T o = new $T($T.dag().nodeQueryBuilder($S, new $T(id)))", - ClassName.bestGuess(Helpers.formatName(type)), - ClassName.bestGuess(Helpers.formatName(type)), - ClassName.bestGuess("io.dagger.client.Dagger"), + thisType, + thisType, + registry().runtime("Dagger"), type.getName(), - ClassName.bestGuess("ID")) + registry().forType("ID")) .addStatement("return o") .build()) .build()); @@ -149,7 +143,7 @@ TypeSpec generateType(Type type) { for (Field scalarField : type.getFields().stream().filter(f -> f.getTypeRef().isScalar()).toList()) { classBuilder.addField( - scalarField.getTypeRef().formatOutput(), + scalarField.getTypeRef().formatOutput(registry()), Helpers.formatName(scalarField), Modifier.PRIVATE); } @@ -158,7 +152,7 @@ TypeSpec generateType(Type type) { // Object constructor for query building MethodSpec constructor = MethodSpec.constructorBuilder() - .addParameter(ClassName.bestGuess("QueryBuilder"), "queryBuilder") + .addParameter(registry().runtime("QueryBuilder"), "queryBuilder") .addCode("this.queryBuilder = queryBuilder;") .build(); classBuilder.addMethod(constructor); @@ -173,7 +167,6 @@ TypeSpec generateType(Type type) { } if (List.of("Container", "Directory").contains(type.getName())) { - ClassName thisType = ClassName.bestGuess(Helpers.formatName(type)); String argName = type.getName().toLowerCase() + "Func"; classBuilder.addMethod( MethodSpec.methodBuilder("with") @@ -190,23 +183,23 @@ TypeSpec generateType(Type type) { private TypeName resolveArgType(InputObject arg, Field field) { // For Query.node(id: ID!), keep as raw ID scalar type if ("Query".equals(field.getParentObject().getName()) && "id".equals(arg.getName())) { - return arg.getType().formatOutput(); + return arg.getType().formatOutput(registry()); } String expectedType = arg.getExpectedType(); - return arg.getType().formatInput(expectedType); + return arg.getType().formatInput(registry(), expectedType); } private TypeName resolveReturnType(Field field) { if ("id".equals(field.getName())) { // id() field: with unified IDs, returns String - return field.getTypeRef().formatOutput(); + return field.getTypeRef().formatOutput(registry()); } if (Helpers.isIdToConvert(field)) { // sync-like fields: return the parent object type - return ClassName.bestGuess(Helpers.formatName(field.getParentObject())); + return registry().forType(field.getParentObject().getName()); } String expectedType = field.getExpectedType(); - return field.getTypeRef().formatInput(expectedType); + return field.getTypeRef().formatInput(registry(), expectedType); } private void buildFieldMethod( @@ -280,66 +273,69 @@ private void buildFieldMethod( if (field.getTypeRef().isListOfObject()) { String objName = field.getTypeRef().getListElementType().getName(); // For interface list elements, use the client class - String clientClassName = - field.getTypeRef().getListElementType().isInterface() ? objName + "Client" : objName; + ClassName clientClass = + field.getTypeRef().getListElementType().isInterface() + ? registry().forInterfaceClient(objName) + : registry().forType(objName); fieldMethodBuilder.addStatement( "nextQueryBuilder = nextQueryBuilder.chain(List.of($S))", "id"); fieldMethodBuilder.addStatement( "List builders = nextQueryBuilder.executeObjectListQuery($S)", objName); fieldMethodBuilder.addStatement( - "return builders.stream().map(qb -> new $L(qb)).toList()", clientClassName); + "return builders.stream().map(qb -> new $T(qb)).toList()", clientClass); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (field.getTypeRef().isList()) { fieldMethodBuilder.addStatement( - "return nextQueryBuilder.executeListQuery($L.class)", - field.getTypeRef().getListElementType().getName()); + "return nextQueryBuilder.executeListQuery($T.class)", + field.getTypeRef().getListElementType().formatOutput(registry())); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (Helpers.isIdToConvert(field)) { fieldMethodBuilder.addStatement("nextQueryBuilder.executeQuery()"); fieldMethodBuilder.addStatement("return this"); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (nullableObject) { String graphqlTypeName = field.getTypeRef().getTypeName(); - String clientClassName = + TypeName clientClass = field.getTypeRef().isInterface() - ? graphqlTypeName + "Client" - : objectReturnType.toString(); + ? registry().forInterfaceClient(graphqlTypeName) + : objectReturnType; fieldMethodBuilder.addStatement( "QueryBuilder objectQueryBuilder = nextQueryBuilder.executeNullableObjectQuery($S)", graphqlTypeName); fieldMethodBuilder.addStatement( - "return Optional.ofNullable(objectQueryBuilder).map(qb -> new $L(qb))", - ClassName.bestGuess(clientClassName)); + "return Optional.ofNullable(objectQueryBuilder).map(qb -> new $T(qb))", clientClass); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } else if (field.getTypeRef().isObjectOrInterface()) { // For interface return types, instantiate the client class CodeBlock instantiation = field.getTypeRef().isInterface() - ? CodeBlock.of("new $LClient(nextQueryBuilder)", field.getTypeRef().getTypeName()) - : CodeBlock.of("new $L(nextQueryBuilder)", objectReturnType); + ? CodeBlock.of( + "new $T(nextQueryBuilder)", + registry().forInterfaceClient(field.getTypeRef().getTypeName())) + : CodeBlock.of("new $T(nextQueryBuilder)", objectReturnType); if (presentObject) { fieldMethodBuilder.addStatement("return $T.of($L)", Optional.class, instantiation); } else { fieldMethodBuilder.addStatement("return $L", instantiation); } } else { - fieldMethodBuilder.addStatement("return nextQueryBuilder.executeQuery($L.class)", returnType); + fieldMethodBuilder.addStatement("return nextQueryBuilder.executeQuery($T.class)", returnType); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) - .addException(ClassName.get("io.dagger.client.exception", "DaggerQueryException")); + .addException(registry().runtime("exception", "DaggerQueryException")); } if (field.isDeprecated()) { @@ -399,7 +395,7 @@ private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field fie .toList(); MethodSpec toArguments = MethodSpec.methodBuilder("toArguments") - .returns(ClassName.bestGuess("Arguments")) + .returns(registry().runtime("Arguments")) .addStatement("Arguments.Builder builder = Arguments.newBuilder()") .addCode(CodeBlock.join(blocks, "\n")) .addStatement("\nreturn builder.build()") @@ -407,7 +403,7 @@ private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field fie fieldArgumentsClassBuilder.addMethod(toArguments); fieldArgumentsClassBuilder.addJavadoc( "Optional arguments for {@link $L#$L}\n\n", - ClassName.bestGuess(Helpers.formatName(type)), + registry().forType(type.getName()).simpleName(), Helpers.formatName(field)); classBuilder.addType(fieldArgumentsClassBuilder.build()); } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java index 1813c95..09d40bc 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java @@ -8,8 +8,9 @@ import javax.lang.model.element.Modifier; class ScalarVisitor extends AbstractVisitor { - public ScalarVisitor(Schema schema, Path targetDirectory, Charset encoding) { - super(schema, targetDirectory, encoding); + public ScalarVisitor( + Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + super(schema, registry, targetDirectory, encoding); } @Override @@ -20,14 +21,14 @@ TypeSpec generateType(Type type) { .addModifiers(Modifier.PUBLIC) .superclass( ParameterizedTypeName.get( - ClassName.bestGuess("Scalar"), ClassName.get(String.class))) + registry().runtime("Scalar"), ClassName.get(String.class))) .addAnnotation( AnnotationSpec.builder(JsonbTypeSerializer.class) - .addMember("value", "$T.class", ClassName.bestGuess("ScalarSerializer")) + .addMember("value", "$T.class", registry().runtime("ScalarSerializer")) .build()) .addAnnotation( AnnotationSpec.builder(JsonbTypeDeserializer.class) - .addMember("value", "$T.class", ClassName.bestGuess("ScalarStringDeserializer")) + .addMember("value", "$T.class", registry().runtime("ScalarStringDeserializer")) .build()); MethodSpec constructor = @@ -37,7 +38,7 @@ TypeSpec generateType(Type type) { .build(); classBuilder.addMethod(constructor); - ClassName className = ClassName.bestGuess(Helpers.formatName(type)); + ClassName className = registry().forType(type.getName()); MethodSpec fromMethod = MethodSpec.methodBuilder("from") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRef.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRef.java index 3307c23..d8b85fd 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRef.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRef.java @@ -101,22 +101,22 @@ public TypeRef getListElementType() { return ref; } - public TypeName formatOutput() { - return formatType(false, null); + public TypeName formatOutput(TypeRegistry registry) { + return formatType(registry, false, null); } - public TypeName formatInput() { - return formatType(true, null); + public TypeName formatInput(TypeRegistry registry) { + return formatType(registry, true, null); } /** Format as input type, using the given expectedType for ID scalar resolution. */ - public TypeName formatInput(String expectedType) { - return formatType(true, expectedType); + public TypeName formatInput(TypeRegistry registry, String expectedType) { + return formatType(registry, true, expectedType); } - private TypeName formatType(boolean isInput, String expectedType) { + private TypeName formatType(TypeRegistry registry, boolean isInput, String expectedType) { if ("Query".equals(getName())) { - return ClassName.bestGuess("Client"); + return registry.forType("Query"); } switch (getKind()) { case SCALAR -> { @@ -133,28 +133,28 @@ private TypeName formatType(boolean isInput, String expectedType) { case "ID" -> { // Unified ID scalar: resolve to expected type if present if (isInput && expectedType != null && !expectedType.isEmpty()) { - return ClassName.bestGuess(expectedType); + return registry.forType(expectedType); } // When used as output (e.g. id() field), return the ID type - return ClassName.bestGuess("ID"); + return registry.forType("ID"); } default -> { if (!isInput) { - return ClassName.bestGuess(getName()); + return registry.forType(getName()); } - return Helpers.convertScalarToObject(getName(), expectedType); + return Helpers.convertScalarToObject(registry, getName(), expectedType); } } } case OBJECT, ENUM, INPUT_OBJECT, INTERFACE -> { - return ClassName.bestGuess(getName()); + return registry.forType(getName()); } case LIST -> { return ParameterizedTypeName.get( - ClassName.get(List.class), getOfType().formatType(isInput, expectedType)); + ClassName.get(List.class), getOfType().formatType(registry, isInput, expectedType)); } default -> { - return getOfType().formatType(isInput, expectedType); + return getOfType().formatType(registry, isInput, expectedType); } } } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java new file mode 100644 index 0000000..5e8bf28 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java @@ -0,0 +1,67 @@ +package io.dagger.codegen.introspection; + +import com.palantir.javapoet.ClassName; + +/** + * Where every Java class a generated package refers to lives. + * + *

Generated code names three kinds of class: a schema type, a hand-written runtime class ({@code + * QueryBuilder}, {@code Arguments}, ...), and itself. Every visitor used to name them by simple + * name, which is only correct while everything lands in one package. Routing them through a + * registry is the seam a second package needs. + */ +public final class TypeRegistry { + + private final String targetPackage; + private final String corePackage; + + private TypeRegistry(String targetPackage, String corePackage) { + this.targetPackage = targetPackage; + this.corePackage = corePackage; + } + + /** Everything in one package. */ + public static TypeRegistry singlePackage(String pkg) { + return new TypeRegistry(pkg, pkg); + } + + /** The package this registry emits into. */ + public String targetPackage() { + return targetPackage; + } + + /** + * The Java class generated for a GraphQL type. {@code Query} is {@code Client}, and the builtin + * scalars are their {@code java.lang} counterparts. + */ + public ClassName forType(String graphqlName) { + switch (graphqlName) { + case "String": + return ClassName.get(String.class); + case "Boolean": + return ClassName.get(Boolean.class); + case "Int": + return ClassName.get(Integer.class); + case "Float": + return ClassName.get(Float.class); + default: + return ClassName.get(corePackage, Helpers.formatName(graphqlName)); + } + } + + /** The query-builder implementation generated next to a GraphQL interface. */ + public ClassName forInterfaceClient(String graphqlName) { + ClassName iface = forType(graphqlName); + return iface.peerClass(iface.simpleName() + "Client"); + } + + /** A hand-written runtime class. */ + public ClassName runtime(String simpleName) { + return ClassName.get(corePackage, simpleName); + } + + /** A hand-written runtime class in a subpackage of the runtime. */ + public ClassName runtime(String subpackage, String simpleName) { + return ClassName.get(corePackage + "." + subpackage, simpleName); + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/VersionVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/VersionVisitor.java index 38e0135..877f5cd 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/VersionVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/VersionVisitor.java @@ -9,8 +9,8 @@ public class VersionVisitor extends CodeWriter { - public VersionVisitor(Path targetDirectory, Charset encoding) { - super(targetDirectory, encoding); + public VersionVisitor(String targetPackage, Path targetDirectory, Charset encoding) { + super(targetPackage, targetDirectory, encoding); } public void visit(String version) throws IOException { diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java index aac1e82..00bdca9 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java @@ -22,6 +22,8 @@ class NullableObjectCodegenTest { + private static final TypeRegistry REGISTRY = TypeRegistry.singlePackage("io.dagger.client"); + @TempDir Path compilationOutputDirectory; @Test @@ -190,14 +192,14 @@ private Map sources(Type... types) throws Exception { String qualifiedName = "io.dagger.client." + type.getName(); if (type.getKind() == TypeKind.INTERFACE) { InterfaceVisitor visitor = - new InterfaceVisitor(schema, Path.of("."), StandardCharsets.UTF_8); + new InterfaceVisitor(schema, REGISTRY, Path.of("."), StandardCharsets.UTF_8); sources.put(qualifiedName, javaFile(visitor.generateType(type))); sources.put(qualifiedName + "Client", javaFile(visitor.generateClientType(type))); } else { sources.put( qualifiedName, javaFile( - new ObjectVisitor(schema, Path.of("."), StandardCharsets.UTF_8) + new ObjectVisitor(schema, REGISTRY, Path.of("."), StandardCharsets.UTF_8) .generateType(type))); } } @@ -241,9 +243,10 @@ private static String javaFile(TypeSpec typeSpec) { } private static String generateInterface(Type type, String version) throws Exception { - return new InterfaceVisitor(schemaAtVersion(version), Path.of("."), StandardCharsets.UTF_8) - .generateType(type) - .toString(); + return javaFile( + new InterfaceVisitor( + schemaAtVersion(version), REGISTRY, Path.of("."), StandardCharsets.UTF_8) + .generateType(type)); } private static Schema schemaAtVersion(String version) throws Exception { diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java index 0b3a453..49da79c 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java @@ -66,7 +66,12 @@ private static String generateQuery(Field field) throws Exception { byte[] introspection = "{\"__schema\":{\"types\":[]}}".getBytes(StandardCharsets.UTF_8); Schema schema = Schema.initialize(new ByteArrayInputStream(introspection), "v1.0.0-beta.11"); TypeSpec client = - new ObjectVisitor(schema, Path.of("."), StandardCharsets.UTF_8).generateType(query); + new ObjectVisitor( + schema, + TypeRegistry.singlePackage("io.dagger.client"), + Path.of("."), + StandardCharsets.UTF_8) + .generateType(query); return JavaFile.builder("io.dagger.client", client).build().toString(); } From 21238e6504054f3be9f04f0732fa736c9c662e40 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:42:04 +0200 Subject: [PATCH 08/28] sdk: make the query transport public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated bindings are about to leave io.dagger.client for one package per target, io.dagger.client.modules.. Every generated type is built on a QueryBuilder, chains through it, implements InputValue, merges Arguments and converts Scalars. All of those were package-private, which was right while the generated code sat next to them and is impossible once it does not: a class cannot implement a non-public interface from another package. So QueryBuilder and its chain/execute methods, its GraphQLClient constructor, InputValue, Arguments.merge and Scalar.convert become public. QueryBuilder also gains client(), which exposes the session a chain is attached to so that code which has to key something on the session identity — the lazy serve registry, chiefly — can reach it. This deliberately reverses the decision in hack/designs/2026-08-17-nullable-object-returns.md to keep QueryBuilder package-private: a public transport is the price of generating into more than one package, and the javadoc says it is not a user-facing API. buildQuery and executeQuery(String) stay package-private: no generated code calls either, only the tests in this package do. Signed-off-by: Yves Brissaud --- .../main/java/io/dagger/client/Arguments.java | 2 +- .../java/io/dagger/client/InputValue.java | 3 +- .../java/io/dagger/client/QueryBuilder.java | 37 +++++++++++++------ .../main/java/io/dagger/client/Scalar.java | 2 +- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java index db469b0..64ba306 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java @@ -34,7 +34,7 @@ private Builder builder() { return new Builder(); } - Arguments merge(Arguments other) { + public Arguments merge(Arguments other) { HashMap newMap = new HashMap<>(this.args); newMap.putAll(other.args); return new Arguments(newMap); diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java index af6cd17..05d5341 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java @@ -2,6 +2,7 @@ import java.util.Map; -interface InputValue { +/** A GraphQL input object, as generated input types implement it from their own package. */ +public interface InputValue { Map toMap(); } diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java index 6b603dc..91d19c9 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java @@ -29,7 +29,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class QueryBuilder { +/** + * Builds and executes one GraphQL selection chain. + * + *

Public because generated code lives in packages of its own — one {@code + * io.dagger.client.modules.} per target — and every generated type is built on, and chains + * through, a query builder. Not a user-facing API: module and client code goes through the + * generated types. + */ +public final class QueryBuilder { static final Logger LOG = LoggerFactory.getLogger(QueryBuilder.class); @@ -38,7 +46,7 @@ class QueryBuilder { private final List leaves; private final String inlineFragmentType; - QueryBuilder(GraphQLClient client) { + public QueryBuilder(GraphQLClient client) { this(client, new LinkedList<>(), new ArrayList<>(), null); } @@ -61,11 +69,16 @@ private QueryBuilder( this.inlineFragmentType = inlineFragmentType; } - QueryBuilder chain(String operation) { + /** The session this builder talks to, and the identity of that session. */ + public GraphQLClient client() { + return this.client; + } + + public QueryBuilder chain(String operation) { return chain(operation, Arguments.noArgs()); } - QueryBuilder chain(String operation, Arguments arguments) { + public QueryBuilder chain(String operation, Arguments arguments) { if (leaves != null && !leaves.isEmpty()) { throw new IllegalStateException("A new field cannot be chained"); } @@ -75,7 +88,7 @@ QueryBuilder chain(String operation, Arguments arguments) { return new QueryBuilder(client, list, new ArrayList<>(), inlineFragmentType); } - QueryBuilder chain(String operation, List leaves) { + public QueryBuilder chain(String operation, List leaves) { if (!this.leaves.isEmpty()) { throw new IllegalStateException("A new field cannot be chained"); } @@ -85,7 +98,7 @@ QueryBuilder chain(String operation, List leaves) { return new QueryBuilder(client, list, leaves, inlineFragmentType); } - QueryBuilder chain(List leaves) { + public QueryBuilder chain(List leaves) { if (!this.leaves.isEmpty()) { throw new IllegalStateException("A new field cannot be chained"); } @@ -99,7 +112,7 @@ QueryBuilder chain(List leaves) { * *

This produces queries like: {@code node(id: "...") { ... on Container { field { ... } } }} */ - QueryBuilder chainNode(String typeName, Object id) { + public QueryBuilder chainNode(String typeName, Object id) { Deque list = new LinkedList<>(); list.addAll(this.parts); // Unwrap Scalar (e.g. ID) to its inner value — Scalar doesn't override toString() @@ -165,11 +178,11 @@ GraphQLResponse executeQuery(String query) * @throws InterruptedException * @throws DaggerQueryException */ - void executeQuery() throws ExecutionException, InterruptedException, DaggerQueryException { + public void executeQuery() throws ExecutionException, InterruptedException, DaggerQueryException { executeQuery(buildQuery()); } - T executeQuery(Class klass) + public T executeQuery(Class klass) throws ExecutionException, InterruptedException, DaggerQueryException { List pathElts = StreamSupport.stream( @@ -214,7 +227,7 @@ T executeQuery(Class klass) * this cannot stay lazy. What comes back is lazy again: the caller wraps it in a normal client * object. */ - QueryBuilder executeNullableObjectQuery(String graphqlTypeName) + public QueryBuilder executeNullableObjectQuery(String graphqlTypeName) throws ExecutionException, InterruptedException, DaggerQueryException { // chain(String), not chain(List): only parts are walked when reading the response back. String id = chain("id").executeQuery(String.class); @@ -224,7 +237,7 @@ QueryBuilder executeNullableObjectQuery(String graphqlTypeName) return new QueryBuilder(this.client).chainNode(graphqlTypeName, id); } - List executeListQuery(Class klass) + public List executeListQuery(Class klass) throws ExecutionException, InterruptedException, DaggerQueryException { List pathElts = StreamSupport.stream( @@ -268,7 +281,7 @@ public Type getOwnerType() { * * @param graphqlTypeName the GraphQL type name for inline fragment resolution */ - List executeObjectListQuery(String graphqlTypeName) + public List executeObjectListQuery(String graphqlTypeName) throws ExecutionException, InterruptedException, DaggerQueryException { List pathElts = StreamSupport.stream( diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java index 8522260..3b4ee97 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java @@ -8,7 +8,7 @@ protected Scalar(T value) { this.value = value; } - T convert() { + public T convert() { return value; } From f26814720226f76b05dea6a8ade9039d2c840ac0 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:42:04 +0200 Subject: [PATCH 09/28] sdk: serve a target on first use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Java program reaches a target through bindings the generator wrote, but nothing has asked the engine to load that target. Add the two pieces that do. ModuleTarget is the descriptor the generator emits: a final module name plus either a workspace-relative path or a git reference with the commit it resolved to at generation time. It is sealed over one record per kind, so a descriptor that is half workspace and half git cannot be constructed; the alternative, one record with nullable fields and a runtime check, would move the same error from the compiler to the first serve. ModuleTargets.serve(session, target) is what every way into a generated client package calls before it builds anything: until the target is served, the field the caller is about to select does not exist in the session. It sends moduleSource(refString:…, refPin:…) for a git target currentWorkspace {moduleSource(path:…)} for a workspace one then withName(name:…), asModule and serve. withName pins the name the schema was generated under, so a change in how the engine derives a target's name cannot silently produce a root field the bindings do not have. The descriptor is passed in rather than looked up. A client package owns the target it was generated against, and holds it as a constant; a client generated against a target the engine serves on its own carries no descriptor and no call to this class at all. Whether a target is served is therefore decided when the bindings are written, not by what happens to be on the class path when they run. The memo is per session, not per JVM: two sessions in one process each serve. It is keyed on the session's GraphQLClient and held weakly, so a closed session is collectable, and behind a synchronized map because serve can be called concurrently. Each target has its own holder, whose serve is synchronized and marks itself done only on success. Concurrent callers for one target therefore send one query between them, a failed serve is retried rather than remembered, and neither blocks a serve of another target. Failure surfaces as an unchecked exception naming the target, with the engine's refusal as its cause. The generated accessor it runs from returns a lazy object and declares no checked exception, so there is nowhere to put a checked one; and swallowing it would replace a message that says why the target could not be loaded with an "unknown field" from the next query, which says nothing. The session is a parameter rather than a global, so a client obtained from Dagger.connect() serves into its own session instead of into whichever one Dagger.dag() happens to hold. Signed-off-by: Yves Brissaud --- .../main/java/io/dagger/client/AtGitRef.java | 13 ++ .../java/io/dagger/client/InWorkspace.java | 12 ++ .../java/io/dagger/client/ModuleTarget.java | 27 ++++ .../java/io/dagger/client/ModuleTargets.java | 93 ++++++++++++ .../java/io/dagger/client/FakeEngine.java | 64 ++++++++ .../io/dagger/client/ModuleTargetsTest.java | 141 ++++++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/AtGitRef.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/InWorkspace.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTarget.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTargets.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/client/FakeEngine.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/client/ModuleTargetsTest.java diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AtGitRef.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AtGitRef.java new file mode 100644 index 0000000..5134893 --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AtGitRef.java @@ -0,0 +1,13 @@ +package io.dagger.client; + +import java.util.Objects; + +/** A {@link ModuleTarget} in a git repository, at the commit its reference resolved to. */ +record AtGitRef(String name, String ref, String pin) implements ModuleTarget { + + AtGitRef { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(ref, "ref"); + Objects.requireNonNull(pin, "pin"); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InWorkspace.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InWorkspace.java new file mode 100644 index 0000000..996bab4 --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InWorkspace.java @@ -0,0 +1,12 @@ +package io.dagger.client; + +import java.util.Objects; + +/** A {@link ModuleTarget} in the caller's workspace, at a workspace-relative path. */ +record InWorkspace(String name, String path) implements ModuleTarget { + + InWorkspace { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(path, "path"); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTarget.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTarget.java new file mode 100644 index 0000000..202536e --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTarget.java @@ -0,0 +1,27 @@ +package io.dagger.client; + +/** + * Where a target lives, as the generator recorded it. + * + *

A target is reached either through the caller's workspace or from a git reference, never both. + * The two are separate implementations of a sealed type rather than one descriptor with nullable + * fields, so a half-filled descriptor cannot be built in the first place. + */ +public sealed interface ModuleTarget permits InWorkspace, AtGitRef { + + /** The final module name the bindings were generated against. */ + String name(); + + /** A target in the caller's workspace, at a workspace-relative path. */ + static ModuleTarget inWorkspace(String name, String path) { + return new InWorkspace(name, path); + } + + /** + * A target in a git repository, at the commit its reference resolved to when the bindings were + * generated. + */ + static ModuleTarget atGitRef(String name, String ref, String pin) { + return new AtGitRef(name, ref, pin); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTargets.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTargets.java new file mode 100644 index 0000000..55f8da8 --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ModuleTargets.java @@ -0,0 +1,93 @@ +package io.dagger.client; + +import io.dagger.client.exception.DaggerQueryException; +import io.dagger.client.graphql.GraphQLClient; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; + +/** + * Serves a target into the session the first time generated code reaches for it. + * + *

Every way into a generated client package calls {@link #serve(QueryBuilder, ModuleTarget)} + * before it builds anything: until the target is served, the field the caller is about to select + * does not exist in the session. The package carries the descriptor it was generated against, so a + * plain program and a Dagger module take the same path. + * + *

A client generated against a target the engine serves on its own carries no descriptor and + * never reaches this class. + */ +public final class ModuleTargets { + + // Weak in the session: a closed session must not be pinned by what it served. + private static final Map> SERVED = + Collections.synchronizedMap(new WeakHashMap<>()); + + private ModuleTargets() {} + + /** + * Make {@code target} resolvable in {@code root}'s session, at most once per session. + * + *

The session is passed in rather than read from a global, so a client from {@link + * Dagger#connect()} serves into its own session rather than into the one {@link Dagger#dag()} + * happens to hold. + * + * @throws RuntimeException when the engine refuses to serve the target + */ + public static void serve(QueryBuilder root, ModuleTarget target) { + SERVED + .computeIfAbsent(root.client(), session -> new ConcurrentHashMap<>()) + .computeIfAbsent(target.name(), name -> new Serve()) + .once(root, target); + } + + /** One target's serve in one session: at most one round trip, whoever asks and however often. */ + private static final class Serve { + + private boolean done; + + synchronized void once(QueryBuilder root, ModuleTarget target) { + if (done) { + return; + } + try { + source(root, target) + .chain("withName", Arguments.newBuilder().add("name", target.name()).build()) + .chain("asModule") + .chain("serve") + .executeQuery(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw refused(target, e); + } catch (ExecutionException | DaggerQueryException e) { + throw refused(target, e); + } + done = true; + } + + private static QueryBuilder source(QueryBuilder root, ModuleTarget target) { + if (target instanceof AtGitRef git) { + return root.chain( + "moduleSource", + Arguments.newBuilder().add("refString", git.ref()).add("refPin", git.pin()).build()); + } + if (target instanceof InWorkspace local) { + return root.chain("currentWorkspace") + .chain("moduleSource", Arguments.newBuilder().add("path", local.path()).build()); + } + throw new IllegalStateException("no way to reach the module target " + target); + } + + /** + * The accessor this runs from returns a lazy object and declares no checked exception, so the + * refusal has to be unchecked. It is not swallowed: without it the next query would fail on an + * unknown field, which says nothing about why the target is missing. + */ + private static RuntimeException refused(ModuleTarget target, Exception cause) { + return new RuntimeException("could not serve the module target " + target, cause); + } + } +} diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/FakeEngine.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/FakeEngine.java new file mode 100644 index 0000000..8b07d14 --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/FakeEngine.java @@ -0,0 +1,64 @@ +package io.dagger.client; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.dagger.client.graphql.GraphQLClient; +import jakarta.json.Json; +import jakarta.json.JsonReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.function.Function; + +/** A GraphQL endpoint recording the documents it is sent and answering with canned JSON. */ +record FakeEngine(HttpServer http, GraphQLClient client, List queries) + implements AutoCloseable { + + static FakeEngine replying(String payload) throws IOException { + return replying(query -> payload); + } + + static FakeEngine replying(Function responder) throws IOException { + List queries = new CopyOnWriteArrayList<>(); + HttpServer http = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + http.createContext("/query", exchange -> respond(exchange, responder, queries)); + http.setExecutor(Executors.newCachedThreadPool()); + http.start(); + String url = "http://127.0.0.1:" + http.getAddress().getPort() + "/query"; + return new FakeEngine(http, new GraphQLClient(url, "token", Map.of()), queries); + } + + /** The last GraphQL document received. */ + String query() { + return queries.get(queries.size() - 1); + } + + // GraphQLClient sets no request timeout, so every path must send a response. + private static void respond( + HttpExchange exchange, Function responder, List queries) + throws IOException { + try (exchange) { + String request = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + String query; + try (JsonReader reader = Json.createReader(new StringReader(request))) { + query = reader.readObject().getString("query"); + } + queries.add(query); + byte[] payload = responder.apply(query).getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("content-type", "application/json"); + exchange.sendResponseHeaders(200, payload.length); + exchange.getResponseBody().write(payload); + } + } + + @Override + public void close() { + client.close(); + http.stop(0); + } +} diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/ModuleTargetsTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/ModuleTargetsTest.java new file mode 100644 index 0000000..54a6fcf --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/ModuleTargetsTest.java @@ -0,0 +1,141 @@ +package io.dagger.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; + +class ModuleTargetsTest { + + private static final String SERVED = "{\"data\":{\"moduleSource\":{}}}"; + + private static final ModuleTarget HELLO = + ModuleTarget.inWorkspace("workspaceTarget", "dagger/modules/hello"); + private static final ModuleTarget OTHER = + ModuleTarget.inWorkspace("otherTarget", "dagger/modules/other"); + private static final ModuleTarget GIT = + ModuleTarget.atGitRef("gitTarget", "github.com/dagger/hello@v1", "0123abc"); + private static final ModuleTarget GONE = + ModuleTarget.atGitRef("brokenTarget", "github.com/dagger/gone@v1", "deadbee"); + + @Test + void aGitTargetIsServedAtItsPinUnderItsGeneratedName() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + ModuleTargets.serve(new QueryBuilder(engine.client()), GIT); + + assertThat(engine.query()) + .startsWith("query {moduleSource(") + .contains("refString:\"github.com/dagger/hello@v1\"") + .contains("refPin:\"0123abc\"") + .endsWith(") {withName(name:\"gitTarget\") {asModule {serve}}}}") + .doesNotContain("currentWorkspace"); + } + } + + @Test + void aWorkspaceTargetIsServedByItsPath() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + ModuleTargets.serve(new QueryBuilder(engine.client()), HELLO); + + assertThat(engine.query()) + .isEqualTo( + "query {currentWorkspace {moduleSource(path:\"dagger/modules/hello\")" + + " {withName(name:\"workspaceTarget\") {asModule {serve}}}}}"); + } + } + + @Test + void aSecondCallForTheSameTargetSendsNothing() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + QueryBuilder root = new QueryBuilder(engine.client()); + ModuleTargets.serve(root, HELLO); + ModuleTargets.serve(root, HELLO); + + assertThat(engine.queries()).hasSize(1); + } + } + + @Test + void anotherSessionServesAgain() throws Exception { + try (FakeEngine first = FakeEngine.replying(SERVED); + FakeEngine second = FakeEngine.replying(SERVED)) { + ModuleTargets.serve(new QueryBuilder(first.client()), HELLO); + ModuleTargets.serve(new QueryBuilder(second.client()), HELLO); + + assertThat(first.queries()).hasSize(1); + assertThat(second.queries()).hasSize(1); + } + } + + @Test + void aTargetTheEngineRefusesDoesNotStopAnother() throws Exception { + try (FakeEngine engine = + FakeEngine.replying( + query -> + query.contains("gone") + ? "{\"errors\":[{\"message\":\"module gone: no such ref\"}]}" + : SERVED)) { + QueryBuilder root = new QueryBuilder(engine.client()); + + assertThatThrownBy(() -> ModuleTargets.serve(root, GONE)) + .hasMessageContaining("brokenTarget") + .hasRootCauseMessage("module gone: no such ref"); + + ModuleTargets.serve(root, OTHER); + assertThat(engine.query()) + .isEqualTo( + "query {currentWorkspace {moduleSource(path:\"dagger/modules/other\")" + + " {withName(name:\"otherTarget\") {asModule {serve}}}}}"); + } + } + + @Test + void concurrentCallsForOneTargetServeOnce() throws Exception { + try (FakeEngine engine = + FakeEngine.replying( + query -> { + sleep(); + return SERVED; + })) { + QueryBuilder root = new QueryBuilder(engine.client()); + CountDownLatch start = new CountDownLatch(1); + List callers = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + Thread caller = + new Thread( + () -> { + await(start); + ModuleTargets.serve(root, HELLO); + }); + caller.start(); + callers.add(caller); + } + start.countDown(); + for (Thread caller : callers) { + caller.join(); + } + + assertThat(engine.queries()).hasSize(1); + } + } + + private static void await(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + private static void sleep() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} From 2fb5902eeb49e5ece72b88bd8593e48f413f7b32 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:42:04 +0200 Subject: [PATCH 10/28] sdk: open a session when there is none Connection.get read DAGGER_SESSION_PORT and DAGGER_SESSION_TOKEN and threw when they were absent, so a plain `java -jar app.jar` could not reach an engine at all. It also took a loadWorkspaceModules parameter and did nothing with it. Both are fixed here. CLISession starts `dagger session` when the environment carries no session. It finds the CLI at _EXPERIMENTAL_DAGGER_CLI_BIN or, failing that, as `dagger` on the PATH, and when neither resolves it says so and names both. It does not download one: provisioning has its own release, checksum and mirror questions, and the design names it a non-goal. The session announces itself as one line of JSON on standard output, read under a bounded timeout. A timeout, a line this SDK cannot parse, a port outside 1..65535, an empty token and an exit before the announcement are all errors, and every one of them quotes what the process wrote to standard error, because that is where the reason is. Capture stops once the handshake succeeds; after that standard error is forwarded to the SDK logger on a daemon thread, so engine progress reaches the user without the buffer growing for the life of the session. Standard output is drained on a thread of its own for the same reason the CLI needs it drained at all: an unread pipe eventually blocks the writer. Shutdown closes the CLI's standard input first, which is how the CLI is meant to be stopped, waits, and only then destroys forcibly. The same shutdown runs from a JVM hook, so a session cannot outlive the process that opened it, and close is idempotent so the hook and an explicit close cannot fight. Connection falls back to CLISession, carries it, and closes it when the connection closes; it passes --load-workspace-modules through when the caller asked for it. A generated standalone client does not ask: it serves its own targets, which is narrower and does not depend on what [modules] happens to list. The parameter was public API that did nothing, and now it does what it says. Dagger.dag() becomes synchronized. It was an unsynchronized lazy singleton, and now that the first call can start an engine, two threads racing it would start two. System.getenv cannot be set from inside a test JVM, so the environment reads sit in the public entry points and the work sits behind package-private seams the tests drive directly: Connection.get with the two values as parameters, CLISession.resolveCLI with the configured binary and the search path, and CLISession.start with an explicit CLI and handshake timeout. The fake CLI is a shell script in a temp directory that exits when its standard input closes, like the real one. Signed-off-by: Yves Brissaud --- .../main/java/io/dagger/client/Dagger.java | 5 +- .../dagger/client/engineconn/CLISession.java | 345 ++++++++++++++++++ .../dagger/client/engineconn/Connection.java | 43 ++- .../client/engineconn/CLISessionTest.java | 194 ++++++++++ .../client/engineconn/ConnectionTest.java | 45 +++ 5 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/CLISessionTest.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/ConnectionTest.java diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java index 163545b..5975117 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java @@ -10,11 +10,12 @@ public class Dagger { * Returns the global Dagger client instance. * *

Contrary to {@code connect}, this is managed as a singleton. It will always return the same - * instance. + * instance. Synchronized because the first call may start an engine session, and two threads + * racing it would start two. * * @return Global Dagger client */ - public static Client dag() { + public static synchronized Client dag() { if (dag == null) { try { dag = new Client(Connection.get(System.getProperty("user.dir"))); diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java new file mode 100644 index 0000000..b3a265d --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java @@ -0,0 +1,345 @@ +package io.dagger.client.engineconn; + +import io.dagger.client.Version; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@code dagger session} this process started, for code that runs with no session in its + * environment: a standalone client, a test, an application. + * + *

The CLI comes from {@code _EXPERIMENTAL_DAGGER_CLI_BIN} or, failing that, {@code dagger} on + * the {@code PATH}. Nothing is downloaded: this uses whatever Dagger the host has and says so + * clearly when there is none. Provisioning has its own release, checksum and mirror questions and + * is deliberately not part of this SDK. + */ +final class CLISession implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(CLISession.class); + + private static final Duration HANDSHAKE_TIMEOUT = Duration.ofMinutes(5); + + // A cache export packs its layers while the session shuts down, so the CLI gets as long to leave + // on its own as sdk/go/engineconn/session.go in dagger/dagger gives it. Killing it earlier + // truncates the export. + private static final Duration SHUTDOWN_GRACE = Duration.ofMinutes(5); + + /** How long a killed CLI has to actually die before its owner stops waiting for it. */ + private static final Duration FORCED_EXIT_TIMEOUT = Duration.ofSeconds(10); + + /** How long a failure message waits for the standard error it quotes. */ + private static final Duration STDERR_DRAIN_TIMEOUT = Duration.ofSeconds(5); + + private final Process process; + private final int port; + private final String sessionToken; + private final Duration shutdownGrace; + private final AtomicBoolean closed = new AtomicBoolean(); + private volatile Thread shutdownHook; + + private CLISession(Process process, int port, String sessionToken, Duration shutdownGrace) { + this.process = process; + this.port = port; + this.sessionToken = sessionToken; + this.shutdownGrace = shutdownGrace; + } + + /** Start a session rooted at {@code workingDir} and wait for it to announce how to reach it. */ + static CLISession start(Path workingDir, boolean loadWorkspaceModules) throws IOException { + Path cli = resolveCLI(System.getenv("_EXPERIMENTAL_DAGGER_CLI_BIN"), System.getenv("PATH")); + return start(cli, workingDir, loadWorkspaceModules, HANDSHAKE_TIMEOUT, SHUTDOWN_GRACE); + } + + static CLISession start( + Path cli, + Path workingDir, + boolean loadWorkspaceModules, + Duration handshakeTimeout, + Duration shutdownGrace) + throws IOException { + List command = new ArrayList<>(List.of(cli.toString(), "session")); + if (!Version.VERSION.isBlank()) { + command.add("--version"); + command.add(Version.VERSION); + } + if (loadWorkspaceModules) { + command.add("--load-workspace-modules"); + } + LOG.debug("opening a dagger session: {}", command); + + Process process = new ProcessBuilder(command).directory(workingDir.toFile()).start(); + Stderr stderr = Stderr.pumping(process); + CompletableFuture announcement = readStdout(process); + + String line; + try { + line = announcement.get(handshakeTimeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + throw failed( + cli, + process, + stderr, + "said nothing within " + handshakeTimeout.toSeconds() + "s", + e, + shutdownGrace); + } catch (ExecutionException e) { + throw failed(cli, process, stderr, "could not be read from", e, shutdownGrace); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw failed(cli, process, stderr, "was interrupted while starting", e, shutdownGrace); + } + if (line == null) { + throw failed( + cli, process, stderr, "exited with code " + exitCode(process), null, shutdownGrace); + } + return announced(cli, process, stderr, line, shutdownGrace); + } + + /** + * The CLI to run: {@code _EXPERIMENTAL_DAGGER_CLI_BIN}, else {@code dagger} on the {@code PATH}. + */ + static Path resolveCLI(String configured, String searchPath) throws IOException { + if (configured != null && !configured.isBlank() && Files.isExecutable(Path.of(configured))) { + return Path.of(configured); + } + for (String dir : (searchPath == null ? "" : searchPath).split(File.pathSeparator)) { + Path candidate = Path.of(dir).resolve("dagger"); + if (Files.isExecutable(candidate)) { + return candidate; + } + } + throw new IOException( + "no Dagger session in the environment (DAGGER_SESSION_PORT and DAGGER_SESSION_TOKEN) and no" + + " dagger CLI to start one: point _EXPERIMENTAL_DAGGER_CLI_BIN at a dagger binary, or" + + " put dagger on the PATH. This SDK does not download one."); + } + + int port() { + return port; + } + + String sessionToken() { + return sessionToken; + } + + boolean isAlive() { + return process.isAlive(); + } + + int exitValue() { + return process.exitValue(); + } + + /** + * Stop the session. Idempotent, and also run at JVM exit so a session never outlives its owner. + */ + @Override + public void close() { + if (!closed.compareAndSet(false, true)) { + return; + } + removeShutdownHook(); + stop(process, shutdownGrace); + } + + private static CLISession announced( + Path cli, Process process, Stderr stderr, String line, Duration shutdownGrace) + throws IOException { + int port; + String sessionToken; + try (JsonReader reader = Json.createReader(new StringReader(line))) { + JsonObject params = reader.readObject(); + port = params.getInt("port"); + sessionToken = params.getString("session_token"); + } catch (RuntimeException e) { + throw failed( + cli, process, stderr, "announced a line this SDK cannot read: " + line, e, shutdownGrace); + } + if (port < 1 || port > 65535) { + throw failed( + cli, + process, + stderr, + "announced " + port + ", which is not a TCP port", + null, + shutdownGrace); + } + if (sessionToken.isEmpty()) { + throw failed(cli, process, stderr, "announced an empty session token", null, shutdownGrace); + } + + CLISession session = new CLISession(process, port, sessionToken, shutdownGrace); + // Past the handshake the rest of standard error is progress, for the user rather than for a + // failure message. + stderr.stopCapturing(); + session.shutdownHook = new Thread(session::close, "dagger-session-shutdown"); + Runtime.getRuntime().addShutdownHook(session.shutdownHook); + return session; + } + + /** + * The first line the session writes, plus a drain of everything after it: an unread pipe would + * eventually block the CLI. + */ + private static CompletableFuture readStdout(Process process) { + CompletableFuture announcement = new CompletableFuture<>(); + Thread reader = + new Thread( + () -> { + try (BufferedReader stdout = + new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line = stdout.readLine(); + announcement.complete(line); + while ((line = stdout.readLine()) != null) { + LOG.debug(line); + } + } catch (IOException e) { + announcement.completeExceptionally(e); + } finally { + announcement.complete(null); + } + }, + "dagger-session-stdout"); + reader.setDaemon(true); + reader.start(); + return announcement; + } + + /** + * The real reason a session failed to start is on its standard error, so every failure carries + * it. The process is stopped first: it has no owner yet, and its stream has to reach its end + * before there is anything to quote. + */ + private static IOException failed( + Path cli, + Process process, + Stderr stderr, + String what, + Throwable cause, + Duration shutdownGrace) { + stop(process, shutdownGrace); + return new IOException( + "`" + cli + " session` " + what + ", and wrote to standard error:\n" + stderr.captured(), + cause); + } + + private static int exitCode(Process process) { + try { + return process.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return -1; + } + } + + // The CLI shuts a session down cleanly when its standard input closes; killing it outright would + // leave the engine to notice on its own. + private static void stop(Process process, Duration grace) { + try { + process.getOutputStream().close(); + } catch (IOException alreadyGone) { + // the process is no longer reachable, so there is nothing left to ask nicely + } + try { + if (process.waitFor(grace.toMillis(), TimeUnit.MILLISECONDS)) { + return; + } + LOG.warn("the dagger session did not exit within {}s; killing it", grace.toSeconds()); + process.destroyForcibly().waitFor(FORCED_EXIT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + } + } + + private void removeShutdownHook() { + Thread hook = shutdownHook; + if (hook == null) { + return; + } + shutdownHook = null; + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException shuttingDown) { + // close() is running from the hook itself, or alongside it + } + } + + /** Forwards the session's standard error to the log, and keeps its start for diagnostics. */ + private static final class Stderr { + + private static final int CAPTURE_LIMIT = 8192; + + private final StringBuilder captured = new StringBuilder(); + private volatile Thread pump; + private volatile boolean capturing = true; + + static Stderr pumping(Process process) { + Stderr stderr = new Stderr(); + stderr.pump = new Thread(() -> stderr.forward(process), "dagger-session-stderr"); + stderr.pump.setDaemon(true); + stderr.pump.start(); + return stderr; + } + + void stopCapturing() { + capturing = false; + } + + String captured() { + try { + pump.join(STDERR_DRAIN_TIMEOUT.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + synchronized (captured) { + return captured.toString(); + } + } + + private void forward(Process process) { + try (BufferedReader stderr = + new BufferedReader( + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = stderr.readLine()) != null) { + LOG.info(line); + capture(line); + } + } catch (IOException gone) { + // the process is gone, and with it anything it had left to say + } + } + + private void capture(String line) { + if (!capturing) { + return; + } + synchronized (captured) { + if (captured.length() < CAPTURE_LIMIT) { + captured.append(line).append(System.lineSeparator()); + } + } + } + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/Connection.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/Connection.java index 09be6ca..a915ea2 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/Connection.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/Connection.java @@ -4,6 +4,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import java.io.IOException; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; @@ -14,9 +15,11 @@ public final class Connection { static final Logger LOG = LoggerFactory.getLogger(Connection.class); private final GraphQLClient graphQLClient; + private final CLISession session; - Connection(GraphQLClient graphQLClient) { + Connection(GraphQLClient graphQLClient, CLISession session) { this.graphQLClient = graphQLClient; + this.session = session; } public GraphQLClient getGraphQLClient() { @@ -24,7 +27,13 @@ public GraphQLClient getGraphQLClient() { } public void close() throws Exception { - this.graphQLClient.close(); + try { + this.graphQLClient.close(); + } finally { + if (this.session != null) { + this.session.close(); + } + } } public static Connection get(String workingDir) throws IOException { @@ -32,22 +41,33 @@ public static Connection get(String workingDir) throws IOException { } public static Connection get(String workingDir, boolean loadWorkspaceModules) throws IOException { - String portStr = System.getenv("DAGGER_SESSION_PORT"); - String sessionToken = System.getenv("DAGGER_SESSION_TOKEN"); + return get( + workingDir, + loadWorkspaceModules, + System.getenv("DAGGER_SESSION_PORT"), + System.getenv("DAGGER_SESSION_TOKEN")); + } + + static Connection get( + String workingDir, boolean loadWorkspaceModules, String portStr, String sessionToken) + throws IOException { if (portStr == null || sessionToken == null) { - throw new IOException( - "DAGGER_SESSION_PORT and DAGGER_SESSION_TOKEN must be set. The Java SDK runtime only " - + "connects to an existing Dagger session; run it through the Dagger engine " - + "(dagger call) or an externally provided session."); + CLISession session = CLISession.start(Path.of(workingDir), loadWorkspaceModules); + return getConnection(session.port(), session.sessionToken(), session); } try { - return getConnection(Integer.parseInt(portStr), sessionToken); + return getConnection(Integer.parseInt(portStr), sessionToken, null); } catch (NumberFormatException nfe) { throw new IOException("invalid port value in DAGGER_SESSION_PORT", nfe); } } - private static Connection getConnection(int port, String token) { + /** The session this connection started, or null when it attached to one already running. */ + CLISession session() { + return this.session; + } + + static Connection getConnection(int port, String token, CLISession session) { // Inject OpenTelemetry context into headers Map headers = new HashMap<>(); GlobalOpenTelemetry.getPropagators() @@ -55,6 +75,7 @@ private static Connection getConnection(int port, String token) { .inject(Context.current(), headers, (carrier, key, value) -> carrier.put(key, value)); return new Connection( - new GraphQLClient(String.format("http://127.0.0.1:%d/query", port), token, headers)); + new GraphQLClient(String.format("http://127.0.0.1:%d/query", port), token, headers), + session); } } diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/CLISessionTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/CLISessionTest.java new file mode 100644 index 0000000..c8933bd --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/CLISessionTest.java @@ -0,0 +1,194 @@ +package io.dagger.client.engineconn; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class CLISessionTest { + + private static final Duration SOON = Duration.ofMillis(500); + + // The real CLI exits when its standard input closes, and so does every fake here. + private static final String WAIT_FOR_STDIN = "cat > /dev/null\n"; + + @TempDir Path dir; + + @Test + void aStartedSessionAnnouncesWhereToReachIt() throws Exception { + Path cli = + fakeCli( + "echo \"$@\" > \"$PWD/args\"\n" + + "echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + + WAIT_FOR_STDIN); + + CLISession session = CLISession.start(cli, dir, false, SOON, SOON); + + assertThat(session.port()).isEqualTo(54321); + assertThat(session.sessionToken()).isEqualTo("tok"); + assertThat(session.isAlive()).isTrue(); + assertThat(Files.readString(dir.resolve("args"))) + .contains("session") + .doesNotContain("--load-workspace-modules"); + session.close(); + } + + @Test + void workspaceModulesAreLoadedOnlyWhenAsked() throws Exception { + Path cli = + fakeCli( + "echo \"$@\" > \"$PWD/args\"\n" + + "echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + + WAIT_FOR_STDIN); + + try (CLISession session = CLISession.start(cli, dir, true, SOON, SOON)) { + assertThat(Files.readString(dir.resolve("args"))).contains("--load-workspace-modules"); + } + } + + @Test + void closeIsIdempotentAndLeavesNoProcessBehind() throws Exception { + Path cli = fakeCli("echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + WAIT_FOR_STDIN); + + CLISession session = CLISession.start(cli, dir, false, SOON, SOON); + session.close(); + assertThat(session.isAlive()).isFalse(); + session.close(); + assertThat(session.isAlive()).isFalse(); + } + + @Test + void anAnnouncementThisSdkCannotReadIsAnError() throws Exception { + Path cli = failing("echo '{\"session_token\" oops}'\n"); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("cannot read") + .hasMessageContaining("engine says no"); + } + + @Test + void anAnnouncementWithoutAPortIsAnError() throws Exception { + Path cli = failing("echo '{\"session_token\":\"tok\"}'\n"); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("cannot read") + .hasMessageContaining("engine says no"); + } + + @Test + void aPortOutsideTheTcpRangeIsAnError() throws Exception { + Path cli = failing("echo '{\"port\":70000,\"session_token\":\"tok\"}'\n"); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("70000, which is not a TCP port") + .hasMessageContaining("engine says no"); + } + + @Test + void anEmptySessionTokenIsAnError() throws Exception { + Path cli = failing("echo '{\"port\":54321,\"session_token\":\"\"}'\n"); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("empty session token") + .hasMessageContaining("engine says no"); + } + + @Test + void sayingNothingBeforeTheTimeoutIsAnError() throws Exception { + Path cli = failing(""); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("said nothing within") + .hasMessageContaining("engine says no"); + } + + @Test + void exitingBeforeAnnouncingIsAnError() throws Exception { + Path cli = fakeCli("echo 'engine says no' >&2\nexit 3\n"); + + assertThatThrownBy(() -> CLISession.start(cli, dir, false, SOON, SOON)) + .isInstanceOf(IOException.class) + .hasMessageContaining("exited with code 3") + .hasMessageContaining("engine says no"); + } + + /** + * The CLI packs a cache export while it shuts down, so closing a session waits for it rather than + * killing it. A session that exits on its own is never forced. + */ + @Test + void aSessionThatTakesItsTimeShuttingDownIsNotKilled() throws Exception { + Path cli = + fakeCli( + "echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + + WAIT_FOR_STDIN + + "sleep 1\n" + + "exit 0\n"); + + CLISession session = CLISession.start(cli, dir, false, SOON, Duration.ofSeconds(30)); + session.close(); + + assertThat(session.isAlive()).isFalse(); + assertThat(session.exitValue()).isZero(); + } + + @Test + void aSessionThatWillNotExitIsKilledOnceTheGraceIsUp() throws Exception { + Path cli = + fakeCli( + "echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + + "while true; do sleep 0.1; done\n"); + + CLISession session = CLISession.start(cli, dir, false, SOON, SOON); + session.close(); + + assertThat(session.isAlive()).isFalse(); + assertThat(session.exitValue()).isNotZero(); + } + + @Test + void theConfiguredBinaryWins() throws Exception { + Path cli = fakeCli(""); + + assertThat(CLISession.resolveCLI(cli.toString(), "/nowhere")).isEqualTo(cli); + } + + @Test + void theBinaryIsOtherwiseLookedUpOnThePath() throws Exception { + Path cli = fakeCli(""); + + assertThat(CLISession.resolveCLI(null, "/nowhere:" + dir)).isEqualTo(cli); + } + + @Test + void noBinaryAnywhereNamesBothPlacesItLookedAndDownloadsNothing() { + assertThatThrownBy(() -> CLISession.resolveCLI(dir.resolve("absent").toString(), "/nowhere")) + .isInstanceOf(IOException.class) + .hasMessageContaining("_EXPERIMENTAL_DAGGER_CLI_BIN") + .hasMessageContaining("PATH") + .hasMessageContaining("does not download"); + } + + /** A CLI that says something on standard error, then waits to be shut down. */ + private Path failing(String body) throws IOException { + return fakeCli("echo 'engine says no' >&2\n" + body + WAIT_FOR_STDIN); + } + + private Path fakeCli(String body) throws IOException { + Path cli = dir.resolve("dagger"); + Files.writeString(cli, "#!/bin/sh\n" + body); + Files.setPosixFilePermissions(cli, PosixFilePermissions.fromString("rwxr-xr-x")); + return cli; + } +} diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/ConnectionTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/ConnectionTest.java new file mode 100644 index 0000000..47c2fdc --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/client/engineconn/ConnectionTest.java @@ -0,0 +1,45 @@ +package io.dagger.client.engineconn; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ConnectionTest { + + @TempDir Path dir; + + @Test + void aSessionInTheEnvironmentIsAttachedToAndNothingIsStarted() throws Exception { + Connection connection = Connection.get(dir.toString(), false, "54321", "tok"); + + assertThat(connection.session()).isNull(); + assertThat(connection.getGraphQLClient()).isNotNull(); + connection.close(); + } + + @Test + void closingAConnectionClosesTheSessionItStarted() throws Exception { + Path cli = fakeCli("echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + "cat > /dev/null\n"); + CLISession session = + CLISession.start(cli, dir, false, Duration.ofMillis(500), Duration.ofMillis(500)); + + Connection connection = + Connection.getConnection(session.port(), session.sessionToken(), session); + connection.close(); + + assertThat(session.isAlive()).isFalse(); + } + + private Path fakeCli(String body) throws IOException { + Path cli = dir.resolve("dagger"); + Files.writeString(cli, "#!/bin/sh\n" + body); + Files.setPosixFilePermissions(cli, PosixFilePermissions.fromString("rwxr-xr-x")); + return cli; + } +} From 3404869694399f91fd1e3a1e1227d01d7bdf79c6 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:34:09 +0200 Subject: [PATCH 11/28] codegen: generate every package a plan names in one pass One invocation now takes a plan directory holding a core schema and one schema per target, and emits the core package plus one package per target against a single type registry. A target's own types and the fields it contributes to core types both go into its package; core keeps neither, and names no client package. Each contributed field becomes a static method on the target's root type, named after the field. A field on Query has no receiver, so it comes in two forms: one taking the session to reach the target in, one over the session Dagger.dag() holds. A field on any other core type takes that type as its receiver and reads the session off it, because serving into a session the receiver does not belong to lands the target beside the caller's query rather than in it. An entry point asks for its target to be served first. Outside a module nothing has served it and the field the entry point selects does not exist; inside one the engine served it before the module ran, no descriptor is registered, and the call returns without a query. Signed-off-by: Yves Brissaud --- .../io/dagger/codegen/DaggerCodegenMojo.java | 21 + .../io/dagger/codegen/GenerationPlan.java | 75 ++++ .../java/io/dagger/codegen/Generator.java | 141 ++++++ .../codegen/introspection/CodegenVisitor.java | 10 +- .../introspection/InterfaceVisitor.java | 1 + .../introspection/ModuleTargetRef.java | 44 ++ .../codegen/introspection/ObjectVisitor.java | 236 +++++++++- .../codegen/introspection/ScalarVisitor.java | 1 + .../codegen/introspection/TypeRegistry.java | 33 +- .../java/io/dagger/codegen/GeneratorTest.java | 417 ++++++++++++++++++ .../NullableObjectCodegenTest.java | 3 +- .../OptionalArgsCodegenTest.java | 2 + sdk/dagger-java-sdk/pom.xml | 1 + sdk/pom.xml | 1 + 14 files changed, 955 insertions(+), 31 deletions(-) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/GenerationPlan.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ModuleTargetRef.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java index cfb0417..346e11d 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java @@ -42,6 +42,13 @@ public class DaggerCodegenMojo extends AbstractMojo { @Parameter(property = "dagger.introspectionJson") protected String introspectionJson; + /** + * A {@link GenerationPlan} directory. When set, every package the plan names is generated in this + * one invocation and {@code introspectionJson} is ignored. + */ + @Parameter(property = "dagger.plan") + protected String plan; + /** Specify output directory where the Java files are generated. */ @Parameter(defaultValue = "${project.build.directory}/generated-sources/dagger") private File outputDirectory; @@ -60,12 +67,26 @@ public void execute() throws MojoExecutionException, MojoFailureException { } Path dest = outputDir.toPath(); + if (plan != null && !plan.isEmpty()) { + try { + new Generator(dest, Charset.forName(outputEncoding), version) + .generate(GenerationPlan.read(Path.of(plan))); + } catch (IOException | IllegalArgumentException e) { + throw new MojoFailureException(e.getMessage(), e); + } + if (project != null) { + project.addCompileSourceRoot(getOutputDirectory().getPath()); + } + return; + } try (InputStream in = getInstrospectionJson()) { Schema schema = Schema.initialize(in, version); SchemaVisitor codegen = new CodegenVisitor( schema, TypeRegistry.singlePackage("io.dagger.client"), + null, + null, dest, Charset.forName(outputEncoding)); schema.visit( diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/GenerationPlan.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/GenerationPlan.java new file mode 100644 index 0000000..8fb49ba --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/GenerationPlan.java @@ -0,0 +1,75 @@ +package io.dagger.codegen; + +import io.dagger.codegen.introspection.ModuleTargetRef; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * What one run of the generator emits: the core package, and one package per target. + * + *

A plan is a directory the caller assembles: + * + *

+ *   <plan>/core/schema.json        the schema core is generated from
+ *   <plan>/target-*/schema.json    one target's client-facing schema
+ *   <plan>/target-*/module         that target's module name, one line
+ *   <plan>/target-*/source         where that module lives, when the client serves it
+ * 
+ * + *

A target with no {@code source} is one the engine serves on its own, and its bindings ask for + * nothing. A target with one carries it into the generated code as a constant, so what a client + * loads is decided when its bindings are written rather than by what is on the class path when they + * run. + * + *

The directory names carry no meaning beyond ordering the targets; a module name can hold + * characters a path cannot, so it travels in a file. Targets are read in module-name order, so a + * plan generates the same bytes however its caller happened to lay it out. + */ +public record GenerationPlan(Path coreSchema, List targets) { + + /** + * One target: the module to generate bindings for, the schema to generate them from, and where + * the bindings should load it from — null when the engine serves it already. + */ + public record Target(String module, Path schema, ModuleTargetRef source) {} + + private static final String CORE = "core"; + private static final String SCHEMA = "schema.json"; + private static final String MODULE = "module"; + private static final String SOURCE = "source"; + + public static GenerationPlan read(Path planDirectory) throws IOException { + Path coreSchema = planDirectory.resolve(CORE).resolve(SCHEMA); + if (!Files.isRegularFile(coreSchema)) { + throw new IOException("generation plan has no core schema at " + coreSchema); + } + List targets = new ArrayList<>(); + try (Stream entries = Files.list(planDirectory)) { + for (Path entry : entries.sorted().toList()) { + if (!Files.isDirectory(entry) || entry.getFileName().toString().equals(CORE)) { + continue; + } + Path module = entry.resolve(MODULE); + Path schema = entry.resolve(SCHEMA); + if (!Files.isRegularFile(module) || !Files.isRegularFile(schema)) { + throw new IOException( + "generation plan entry " + entry + " needs both a " + MODULE + " and a " + SCHEMA); + } + Path source = entry.resolve(SOURCE); + targets.add( + new Target( + Files.readString(module, StandardCharsets.UTF_8).trim(), + schema, + Files.isRegularFile(source) ? ModuleTargetRef.read(source) : null)); + } + } + targets.sort(Comparator.comparing(Target::module)); + return new GenerationPlan(coreSchema, List.copyOf(targets)); + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java new file mode 100644 index 0000000..b88889b --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java @@ -0,0 +1,141 @@ +package io.dagger.codegen; + +import io.dagger.codegen.introspection.ClientEntryPoint; +import io.dagger.codegen.introspection.CodegenVisitor; +import io.dagger.codegen.introspection.ModuleTargetRef; +import io.dagger.codegen.introspection.Schema; +import io.dagger.codegen.introspection.SchemaPartition; +import io.dagger.codegen.introspection.TypeRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Emits every package a {@link GenerationPlan} names, in one pass over one type registry. */ +public final class Generator { + + /** The package core and the hand-written runtime share. */ + public static final String CORE_PACKAGE = "io.dagger.client"; + + private final Path outputDirectory; + private final Charset encoding; + private final String engineVersion; + + public Generator(Path outputDirectory, Charset encoding, String engineVersion) { + this.outputDirectory = outputDirectory; + this.encoding = encoding; + this.engineVersion = engineVersion; + } + + public void generate(GenerationPlan plan) throws IOException { + Schema coreSchema = read(plan.coreSchema()); + Map packages = + ModulePackage.packagesFor( + plan.targets().stream().map(GenerationPlan.Target::module).toList()); + + // Built before anything is written, because each target names core's types and the core + // receivers it enters on: no package can be emitted until every type's home is known. + Map packageByTypeName = new LinkedHashMap<>(); + Map targetByTypeName = new LinkedHashMap<>(); + Map schemas = new LinkedHashMap<>(); + Map entryPoints = new LinkedHashMap<>(); + for (GenerationPlan.Target target : plan.targets()) { + Schema schema = read(target.schema()); + schemas.put(target.module(), schema); + String pkg = packages.get(target.module()); + SchemaPartition partition = SchemaPartition.client(schema, target.module()); + entryPoints.put(target.module(), new ClientEntryPoint(partition)); + for (String owned : partition.typeNames()) { + requireUnclaimed(targetByTypeName, owned, target.module()); + packageByTypeName.put(owned, pkg); + } + } + + requireEveryOwnedModulePlanned(coreSchema, packages.keySet()); + requireNoTargetShadowsCore(coreSchema, targetByTypeName); + + TypeRegistry registry = TypeRegistry.acrossPackages(CORE_PACKAGE, packageByTypeName); + emit(SchemaPartition.core(coreSchema), registry.emittingInto(CORE_PACKAGE), null, null); + for (GenerationPlan.Target target : plan.targets()) { + String pkg = packages.get(target.module()); + emit( + SchemaPartition.client(schemas.get(target.module()), target.module()), + registry.emittingInto(pkg), + entryPoints.get(target.module()), + target.source()); + } + } + + /** + * Type names are the keys the whole run resolves through, so two targets owning one name would + * hand the second silent ownership of every reference to it. The engine namespaces a target's + * local types under the target's name, which keeps most target sets apart but not all: a target + * {@code foo} contributes {@code FooBar}, and so does a target named {@code foo-bar}. Their + * package segments differ, so {@link ModulePackage} never sees it. + */ + private static void requireUnclaimed( + Map targetByTypeName, String typeName, String module) { + String previous = targetByTypeName.putIfAbsent(typeName, module); + if (previous != null && !previous.equals(module)) { + throw new IllegalArgumentException( + String.format( + "targets %s and %s both own the type %s; rename or alias one of them", + previous, module, typeName)); + } + } + + /** The same collision against a core type, which no target may take over. */ + private static void requireNoTargetShadowsCore( + Schema coreSchema, Map targetByTypeName) { + for (String typeName : SchemaPartition.core(coreSchema).typeNames()) { + String module = targetByTypeName.get(typeName); + if (module != null) { + throw new IllegalArgumentException( + String.format( + "target %s owns the type %s, which core also has; rename or alias the target", + module, typeName)); + } + } + } + + /** + * A core schema attributing a type or field to a module with no target would generate code + * referring to a package this run does not write. + */ + private static void requireEveryOwnedModulePlanned(Schema coreSchema, Set planned) { + List unplanned = + SchemaPartition.ownedModules(coreSchema).stream() + .filter(m -> !planned.contains(m)) + .toList(); + if (!unplanned.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "the core schema attributes types or fields to %s, which the plan has no target for;" + + " every module the schema mentions has to be generated", + unplanned)); + } + } + + private void emit( + SchemaPartition partition, + TypeRegistry registry, + ClientEntryPoint entryPoint, + ModuleTargetRef source) + throws IOException { + Files.createDirectories(outputDirectory); + partition.visit( + new CodegenVisitor( + partition.schema(), registry, entryPoint, source, outputDirectory, encoding)); + } + + private Schema read(Path schemaFile) throws IOException { + try (InputStream in = Files.newInputStream(schemaFile)) { + return Schema.initialize(in, engineVersion); + } + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java index 1af89af..c629451 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/CodegenVisitor.java @@ -16,11 +16,17 @@ public class CodegenVisitor implements SchemaVisitor { private final IDAbleVisitor idAbleVisitor; public CodegenVisitor( - Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + Schema schema, + TypeRegistry registry, + ClientEntryPoint entryPoint, + ModuleTargetRef source, + Path targetDirectory, + Charset encoding) { this.scalarVisitor = new ScalarVisitor(schema, registry, targetDirectory, encoding); this.inputVisitor = new InputVisitor(schema, registry, targetDirectory, encoding); this.enumVisitor = new EnumVisitor(schema, registry, targetDirectory, encoding); - this.objectVisitor = new ObjectVisitor(schema, registry, targetDirectory, encoding); + this.objectVisitor = + new ObjectVisitor(schema, registry, entryPoint, source, targetDirectory, encoding); this.interfaceVisitor = new InterfaceVisitor(schema, registry, targetDirectory, encoding); this.versionVisitor = new VersionVisitor(registry.targetPackage(), targetDirectory, encoding); this.idAbleVisitor = new IDAbleVisitor(schema, registry, targetDirectory, encoding); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java index 259ed7a..af2796d 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java @@ -117,6 +117,7 @@ TypeSpec generateClientType(Type type) { // Constructor MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) .addParameter(registry().runtime("QueryBuilder"), "queryBuilder") .addCode("this.queryBuilder = queryBuilder;") .build(); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ModuleTargetRef.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ModuleTargetRef.java new file mode 100644 index 0000000..a60ba1b --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ModuleTargetRef.java @@ -0,0 +1,44 @@ +package io.dagger.codegen.introspection; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Where a client's module lives, as the caller resolved it at generation time. + * + *

A client that carries one of these serves its own module: the generated bindings ask the + * engine to load it before they select anything on it. A client generated against a module the + * engine serves on its own carries none, and asks for nothing. + */ +public sealed interface ModuleTargetRef { + + /** A module in the caller's workspace, at a workspace-root-absolute path. */ + record InWorkspace(String path) implements ModuleTargetRef {} + + /** A module in git, at the commit its reference resolved to when the bindings were written. */ + record AtGitRef(String ref, String pin) implements ModuleTargetRef {} + + /** + * Read a descriptor from a plan entry: a kind on the first line, its fields on the lines after. + * One field per line rather than a delimiter, because a path or a git reference may legally hold + * whatever separator would otherwise be chosen. + */ + static ModuleTargetRef read(Path file) throws IOException { + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + String kind = lines.isEmpty() ? "" : lines.get(0); + if (kind.equals("workspace") && lines.size() == 2) { + return new InWorkspace(lines.get(1)); + } + if (kind.equals("git") && lines.size() == 3) { + return new AtGitRef(lines.get(1), lines.get(2)); + } + throw new IOException( + file + + " is not a module descriptor: expected \"workspace\" and a path, or \"git\"," + + " a reference and a commit; got " + + lines); + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java index ec53087..19a1b20 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java @@ -1,6 +1,7 @@ package io.dagger.codegen.introspection; import static org.apache.commons.lang3.StringUtils.capitalize; +import static org.apache.commons.lang3.StringUtils.uncapitalize; import com.palantir.javapoet.*; import jakarta.json.bind.annotation.JsonbTypeDeserializer; @@ -17,9 +18,43 @@ import javax.lang.model.element.Modifier; class ObjectVisitor extends AbstractVisitor { + + /** The constant each entry point serves before it selects anything. */ + private static final String TARGET = "TARGET"; + + private final ClientEntryPoint entryPoint; + private final ModuleTargetRef source; + public ObjectVisitor( - Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + Schema schema, + TypeRegistry registry, + ClientEntryPoint entryPoint, + ModuleTargetRef source, + Path targetDirectory, + Charset encoding) { super(schema, registry, targetDirectory, encoding); + this.entryPoint = entryPoint; + this.source = source; + } + + /** + * A field a module contributes to a core type, emitted as a static method on the module's root + * type because the core class it belongs to is in another package and Java has no partial + * classes. + * + * @param receiverType the core type the field was reached through, or null on {@code Query}, + * whose receiver is the session and is therefore named rather than passed + */ + private record Entry(String module, ClassName receiverType, String receiverName) { + + boolean onQuery() { + return receiverType == null; + } + + /** Every entry lands in one class, so two fields of the same name need telling apart. */ + String argumentsPrefix() { + return onQuery() ? "" : receiverType.simpleName(); + } } @Override @@ -42,6 +77,7 @@ TypeSpec generateType(Type type) { if ("Query".equals(type.getName())) { MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) .addParameter(registry().runtime("engineconn", "Connection"), "connection") .addStatement("this.connection = connection") .addStatement("this.queryBuilder = new QueryBuilder(connection.getGraphQLClient())") @@ -152,18 +188,33 @@ TypeSpec generateType(Type type) { // Object constructor for query building MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) .addParameter(registry().runtime("QueryBuilder"), "queryBuilder") .addCode("this.queryBuilder = queryBuilder;") .build(); classBuilder.addMethod(constructor); + // A client package chains from a core object it did not generate: the session a module is + // served into, and the receiver of every field the module contributes to a core type. + classBuilder.addMethod( + MethodSpec.methodBuilder("queryBuilder") + .addModifiers(Modifier.PUBLIC) + .returns(registry().runtime("QueryBuilder")) + .addJavadoc("The query builder this object chains from.\n") + .addStatement("return this.queryBuilder") + .build()); + for (Field field : type.getFields()) { if (field.hasOptionalArgs()) { - buildFieldArgumentsHelpers(classBuilder, field, type); - buildFieldMethod(classBuilder, field, true); + buildFieldArgumentsHelpers(classBuilder, field, type, null); + buildFieldMethod(classBuilder, field, true, null); } - buildFieldMethod(classBuilder, field, false); + buildFieldMethod(classBuilder, field, false, null); + } + + if (entryPoint != null && entryPoint.rootTypeName().equals(type.getName())) { + buildEntryPoints(classBuilder, type); } if (List.of("Container", "Directory").contains(type.getName())) { @@ -180,6 +231,89 @@ TypeSpec generateType(Type type) { return classBuilder.build(); } + /** + * The way into this client package: the module's {@code Query} field, and every field it + * contributes to another core type. + */ + private void buildEntryPoints(TypeSpec.Builder classBuilder, Type type) { + if (source != null) { + classBuilder.addField(targetConstant()); + } + Entry onQuery = new Entry(entryPoint.module(), null, null); + buildEntry(classBuilder, entryPoint.entryField(), type, onQuery); + entryPoint + .shims() + .forEach( + (typeName, fields) -> { + ClassName receiverType = registry().forType(typeName); + Entry shim = new Entry(entryPoint.module(), receiverType, uncapitalize(typeName)); + fields.forEach(field -> buildEntry(classBuilder, field, type, shim)); + }); + } + + /** + * One entry, and — on {@code Query} alone — a second form over the session {@code Dagger.dag()} + * holds. A field on any other core type takes that type as its receiver and reads the session off + * it, so there is no session left for a caller to choose. + */ + private void buildEntry(TypeSpec.Builder classBuilder, Field field, Type type, Entry entry) { + if (field.hasOptionalArgs()) { + buildFieldArgumentsHelpers(classBuilder, field, type, entry); + MethodSpec withOptArgs = buildFieldMethod(classBuilder, field, true, entry); + if (entry.onQuery()) { + classBuilder.addMethod(ambient(withOptArgs, field)); + } + } + MethodSpec method = buildFieldMethod(classBuilder, field, false, entry); + if (entry.onQuery()) { + classBuilder.addMethod(ambient(method, field)); + } + } + + /** + * Where this client's module lives, held by the package that talks to it. A client that carries + * one loads its own module; one generated against a module the engine serves already carries + * none, and the entry points below ask for nothing. + */ + private FieldSpec targetConstant() { + ClassName target = registry().runtime("ModuleTarget"); + CodeBlock initializer; + if (source instanceof ModuleTargetRef.InWorkspace workspace) { + initializer = + CodeBlock.of("$T.inWorkspace($S, $S)", target, entryPoint.module(), workspace.path()); + } else if (source instanceof ModuleTargetRef.AtGitRef git) { + initializer = + CodeBlock.of( + "$T.atGitRef($S, $S, $S)", target, entryPoint.module(), git.ref(), git.pin()); + } else { + throw new IllegalStateException("no way to reach the module target " + source); + } + return FieldSpec.builder(target, TARGET, Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(initializer) + .build(); + } + + /** + * The same entry over the ambient session, so a caller that never named one still has a way in. + */ + private MethodSpec ambient(MethodSpec entry, Field field) { + List withoutSession = entry.parameters().subList(1, entry.parameters().size()); + CodeBlock.Builder call = + CodeBlock.builder().add("return $L($T.dag()", entry.name(), registry().runtime("Dagger")); + withoutSession.forEach(parameter -> call.add(", $L", parameter.name())); + call.add(")"); + return MethodSpec.methodBuilder(entry.name()) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addAnnotations(entry.annotations()) + .returns(entry.returnType()) + .addParameters(withoutSession) + .addExceptions(entry.exceptions()) + .addJavadoc(Helpers.escapeJavadoc(field.getDescription())) + .addJavadoc("\n@see $T#dag()\n", registry().runtime("Dagger")) + .addStatement(call.build()) + .build(); + } + private TypeName resolveArgType(InputObject arg, Field field) { // For Query.node(id: ID!), keep as raw ID scalar type if ("Query".equals(field.getParentObject().getName()) && "id".equals(arg.getName())) { @@ -202,10 +336,24 @@ private TypeName resolveReturnType(Field field) { return field.getTypeRef().formatInput(registry(), expectedType); } - private void buildFieldMethod( - TypeSpec.Builder classBuilder, Field field, boolean withOptionalArgs) { + private MethodSpec buildFieldMethod( + TypeSpec.Builder classBuilder, Field field, boolean withOptionalArgs, Entry entry) { MethodSpec.Builder fieldMethodBuilder = MethodSpec.methodBuilder(Helpers.formatName(field)).addModifiers(Modifier.PUBLIC); + if (entry != null) { + fieldMethodBuilder.addModifiers(Modifier.STATIC); + if (entry.onQuery()) { + fieldMethodBuilder.addParameter( + ParameterSpec.builder(registry().forType("Query"), "dag") + .addJavadoc("the session to reach the target in\n") + .build()); + } else { + fieldMethodBuilder.addParameter( + ParameterSpec.builder(entry.receiverType(), entry.receiverName()) + .addJavadoc("the $L to chain from\n", entry.receiverType().simpleName()) + .build()); + } + } TypeName returnType = resolveReturnType(field); TypeName objectReturnType = returnType; boolean nullableObject = @@ -231,15 +379,37 @@ private void buildFieldMethod( fieldMethodBuilder.addParameters(mandatoryParams); if (withOptionalArgs && field.hasOptionalArgs()) { fieldMethodBuilder.addParameter( - ParameterSpec.builder( - ClassName.bestGuess(capitalize(Helpers.formatName(field)) + "Arguments"), - "optArgs") + ParameterSpec.builder(argumentsClass(field, entry), "optArgs") .addJavadoc("$L optional arguments\n", Helpers.formatName(field)) .build()); } fieldMethodBuilder.addJavadoc(Helpers.escapeJavadoc(field.getDescription())); - if (field.getTypeRef().isScalar() + String chainFrom = "this.queryBuilder"; + if (entry != null) { + // The field this method selects does not exist in the session until the module is served, + // so a client that carries a descriptor serves before it builds anything. + if (entry.onQuery()) { + if (source != null) { + fieldMethodBuilder.addStatement( + "$T.serve(dag.queryBuilder(), $L)", registry().runtime("ModuleTargets"), TARGET); + } + chainFrom = "dag.queryBuilder()"; + } else { + // The receiver names the session this has to serve into, and its builder is mid-chain: + // serving off the session Dagger.dag() holds would land the module beside the query the + // caller is building rather than in it. + if (source != null) { + fieldMethodBuilder.addStatement( + "$T.serve(new $T($L.queryBuilder().client()), $L)", + registry().runtime("ModuleTargets"), + registry().runtime("QueryBuilder"), + entry.receiverName(), + TARGET); + } + chainFrom = entry.receiverName() + ".queryBuilder()"; + } + } else if (field.getTypeRef().isScalar() && !Helpers.isIdToConvert(field) && !"Query".equals(field.getParentObject().getName())) { fieldMethodBuilder.beginControlFlow("if (this.$L != null)", Helpers.formatName(field)); @@ -247,7 +417,8 @@ private void buildFieldMethod( fieldMethodBuilder.endControlFlow(); } if (field.hasArgs()) { - fieldMethodBuilder.addStatement("Arguments.Builder builder = Arguments.newBuilder()"); + fieldMethodBuilder.addStatement( + "$1T.Builder builder = $1T.newBuilder()", registry().runtime("Arguments")); } field .getRequiredArgs() @@ -256,18 +427,24 @@ private void buildFieldMethod( fieldMethodBuilder.addStatement( "builder.add($1S, $2L)", arg.getName(), Helpers.formatName(arg))); if (field.hasArgs()) { - fieldMethodBuilder.addStatement("Arguments fieldArgs = builder.build()"); + fieldMethodBuilder.addStatement( + "$T fieldArgs = builder.build()", registry().runtime("Arguments")); } if (withOptionalArgs && field.hasOptionalArgs()) { fieldMethodBuilder.addStatement("fieldArgs = fieldArgs.merge(optArgs.toArguments())"); } if (field.hasArgs()) { fieldMethodBuilder.addStatement( - "QueryBuilder nextQueryBuilder = this.queryBuilder.chain($S, fieldArgs)", + "$T nextQueryBuilder = $L.chain($S, fieldArgs)", + registry().runtime("QueryBuilder"), + chainFrom, field.getName()); } else { fieldMethodBuilder.addStatement( - "QueryBuilder nextQueryBuilder = this.queryBuilder.chain($S)", field.getName()); + "$T nextQueryBuilder = $L.chain($S)", + registry().runtime("QueryBuilder"), + chainFrom, + field.getName()); } if (field.getTypeRef().isListOfObject()) { @@ -280,7 +457,9 @@ private void buildFieldMethod( fieldMethodBuilder.addStatement( "nextQueryBuilder = nextQueryBuilder.chain(List.of($S))", "id"); fieldMethodBuilder.addStatement( - "List builders = nextQueryBuilder.executeObjectListQuery($S)", objName); + "List<$T> builders = nextQueryBuilder.executeObjectListQuery($S)", + registry().runtime("QueryBuilder"), + objName); fieldMethodBuilder.addStatement( "return builders.stream().map(qb -> new $T(qb)).toList()", clientClass); fieldMethodBuilder @@ -297,7 +476,8 @@ private void buildFieldMethod( .addException(registry().runtime("exception", "DaggerQueryException")); } else if (Helpers.isIdToConvert(field)) { fieldMethodBuilder.addStatement("nextQueryBuilder.executeQuery()"); - fieldMethodBuilder.addStatement("return this"); + fieldMethodBuilder.addStatement( + "return $L", entry == null ? "this" : entry.onQuery() ? "dag" : entry.receiverName()); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) @@ -309,7 +489,8 @@ private void buildFieldMethod( ? registry().forInterfaceClient(graphqlTypeName) : objectReturnType; fieldMethodBuilder.addStatement( - "QueryBuilder objectQueryBuilder = nextQueryBuilder.executeNullableObjectQuery($S)", + "$T objectQueryBuilder = nextQueryBuilder.executeNullableObjectQuery($S)", + registry().runtime("QueryBuilder"), graphqlTypeName); fieldMethodBuilder.addStatement( "return Optional.ofNullable(objectQueryBuilder).map(qb -> new $T(qb))", clientClass); @@ -343,7 +524,9 @@ private void buildFieldMethod( fieldMethodBuilder.addJavadoc("@deprecated $L\n", field.getDeprecationReason()); } - classBuilder.addMethod(fieldMethodBuilder.build()); + MethodSpec method = fieldMethodBuilder.build(); + classBuilder.addMethod(method); + return method; } /** @@ -353,12 +536,13 @@ private void buildFieldMethod( * @param field * @param type */ - private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field field, Type type) { - String fieldArgumentsClassName = capitalize(Helpers.formatName(field)) + "Arguments"; + private void buildFieldArgumentsHelpers( + TypeSpec.Builder classBuilder, Field field, Type type, Entry entry) { + ClassName fieldArgumentsClassName = argumentsClass(field, entry); /* Inner class XXXArguments */ TypeSpec.Builder fieldArgumentsClassBuilder = - TypeSpec.classBuilder(fieldArgumentsClassName) + TypeSpec.classBuilder(fieldArgumentsClassName.simpleName()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC); List optionalArgFields = field.getOptionalArgs().stream() @@ -377,7 +561,7 @@ private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field fie Helpers.withSetter( arg, resolveArgType(arg, field), - ClassName.bestGuess(fieldArgumentsClassName), + fieldArgumentsClassName, arg.getDescription())) .toList(); fieldArgumentsClassBuilder.addMethods(optionalArgFieldWithMethods); @@ -396,7 +580,7 @@ private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field fie MethodSpec toArguments = MethodSpec.methodBuilder("toArguments") .returns(registry().runtime("Arguments")) - .addStatement("Arguments.Builder builder = Arguments.newBuilder()") + .addStatement("$1T.Builder builder = $1T.newBuilder()", registry().runtime("Arguments")) .addCode(CodeBlock.join(blocks, "\n")) .addStatement("\nreturn builder.build()") .build(); @@ -407,4 +591,10 @@ private void buildFieldArgumentsHelpers(TypeSpec.Builder classBuilder, Field fie Helpers.formatName(field)); classBuilder.addType(fieldArgumentsClassBuilder.build()); } + + /** The nested class holding a field's optional arguments, as the enclosing class names it. */ + private ClassName argumentsClass(Field field, Entry entry) { + String prefix = entry == null ? "" : entry.argumentsPrefix(); + return ClassName.bestGuess(prefix + capitalize(Helpers.formatName(field)) + "Arguments"); + } } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java index 09d40bc..1404292 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java @@ -33,6 +33,7 @@ TypeSpec generateType(Type type) { MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) .addParameter(ClassName.get(String.class), "value") .addStatement("super(value)") .build(); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java index 5e8bf28..68891db 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java @@ -1,6 +1,7 @@ package io.dagger.codegen.introspection; import com.palantir.javapoet.ClassName; +import java.util.Map; /** * Where every Java class a generated package refers to lives. @@ -14,15 +15,34 @@ public final class TypeRegistry { private final String targetPackage; private final String corePackage; + private final Map packageByTypeName; - private TypeRegistry(String targetPackage, String corePackage) { + private TypeRegistry( + String targetPackage, String corePackage, Map packageByTypeName) { this.targetPackage = targetPackage; this.corePackage = corePackage; + this.packageByTypeName = packageByTypeName; } /** Everything in one package. */ public static TypeRegistry singlePackage(String pkg) { - return new TypeRegistry(pkg, pkg); + return new TypeRegistry(pkg, pkg, Map.of()); + } + + /** + * Core in one package, and every type a module owns in that module's own package. + * + *

Built once for a whole plan, so a module's package can name a core type and core can name a + * module's type without either knowing where the other landed. + */ + public static TypeRegistry acrossPackages( + String corePackage, Map packageByTypeName) { + return new TypeRegistry(corePackage, corePackage, Map.copyOf(packageByTypeName)); + } + + /** The same resolution, writing into a different package. */ + public TypeRegistry emittingInto(String pkg) { + return new TypeRegistry(pkg, corePackage, packageByTypeName); } /** The package this registry emits into. */ @@ -31,8 +51,9 @@ public String targetPackage() { } /** - * The Java class generated for a GraphQL type. {@code Query} is {@code Client}, and the builtin - * scalars are their {@code java.lang} counterparts. + * The Java class generated for a GraphQL type. {@code Query} is {@code Client}, the builtin + * scalars are their {@code java.lang} counterparts, and a type a module owns is in that module's + * package. */ public ClassName forType(String graphqlName) { switch (graphqlName) { @@ -45,7 +66,9 @@ public ClassName forType(String graphqlName) { case "Float": return ClassName.get(Float.class); default: - return ClassName.get(corePackage, Helpers.formatName(graphqlName)); + return ClassName.get( + packageByTypeName.getOrDefault(graphqlName, corePackage), + Helpers.formatName(graphqlName)); } } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java new file mode 100644 index 0000000..e09b2c3 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java @@ -0,0 +1,417 @@ +package io.dagger.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class GeneratorTest { + + @TempDir Path plan; + @TempDir Path out; + + @Test + void everyPackageThePlanNamesIsEmittedOnce() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + assertThat(emitted()) + .contains( + "io/dagger/client/Client.java", + "io/dagger/client/Container.java", + "io/dagger/client/modules/alpha/Alpha.java", + "io/dagger/client/modules/alpha/AlphaReport.java", + "io/dagger/client/modules/beta/Beta.java"); + assertThat(emitted()).doesNotContain("io/dagger/client/Alpha.java"); + } + + /** A target is reached from its own package, so no core source names one. */ + @Test + void coreNamesNoClientPackage() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + for (String source : emitted()) { + if (source.startsWith("io/dagger/client/modules/")) { + continue; + } + assertThat(read(source)) + .as("core source %s", source) + .doesNotContain("io.dagger.client.modules"); + } + assertThat(read("io/dagger/client/Client.java")).doesNotContain("Alpha").doesNotContain("Beta"); + } + + /** The way into a target is a static method on its root type, taking the session it runs in. */ + @Test + void aTargetIsEnteredFromItsOwnPackage() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) + .contains("import io.dagger.client.Client;") + .contains("public static Alpha alpha(Client dag, String source)") + .contains("public static Alpha alpha(String source)") + .contains("return alpha(Dagger.dag(), source)"); + assertThat(read("io/dagger/client/modules/beta/Beta.java")) + .contains("public static Beta beta(Client dag)") + .contains("public static Beta beta()"); + } + + /** A target's optional constructor arguments travel with the method that takes them. */ + @Test + void theArgumentsHolderOfAnEntryPointIsNestedInTheRootType() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) + .contains("public static class AlphaArguments") + .contains("public static Alpha alpha(Client dag, String source, AlphaArguments optArgs)") + .contains("public static Alpha alpha(String source, AlphaArguments optArgs)"); + assertThat(read("io/dagger/client/Client.java")).doesNotContain("AlphaArguments"); + } + + /** A field a module contributes to another core type moves with it, receiver and all. */ + @Test + void aTargetsContributionToACoreTypeIsEnteredFromItsOwnPackage() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) + .contains("import io.dagger.client.Binding;") + .contains("public static Alpha asAlpha(Binding binding)") + .contains("binding.queryBuilder().chain(\"asAlpha\")"); + assertThat(read("io/dagger/client/Binding.java")).doesNotContain("asAlpha"); + assertThat(emitted()).doesNotContain("io/dagger/client/modules/alpha/Binding.java"); + } + + /** + * The receiver names the session the target has to be served into. Serving into whichever one + * {@code Dagger.dag()} holds would land the target beside the caller's query rather than in it, + * and the engine would then refuse the field with no hint as to why. So the receiver is the only + * argument, and the session is read off it. + */ + @Test + void aContributionToACoreTypeIsServedIntoItsReceiversSession() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + String alpha = read("io/dagger/client/modules/alpha/Alpha.java"); + assertThat(shimOf(alpha, "asAlpha")) + .isEqualTo( + """ + public static Alpha asAlpha(Binding binding) { + ModuleTargets.serve(new QueryBuilder(binding.queryBuilder().client()), TARGET); + QueryBuilder nextQueryBuilder = binding.queryBuilder().chain("asAlpha"); + return new Alpha(nextQueryBuilder); + } + """ + .stripTrailing()); + assertThat(alpha).doesNotContain("asAlpha(Client dag"); + } + + @Test + void aTargetsPackageReachesCoreTypesInTheCorePackage() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) + .contains("import io.dagger.client.Container;"); + } + + /** + * The field a target is reached through does not exist until something serves the target, so + * every entry point asks for it first, off the descriptor its own package holds. + */ + @Test + void anEntryPointServesItsTargetFirst() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), gitTarget("beta", BETA)); + + generate(); + + assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) + .contains( + "private static final ModuleTarget TARGET = ModuleTarget.inWorkspace(\"alpha\"," + + " \"/dagger/modules/alpha\");") + .contains("ModuleTargets.serve(dag.queryBuilder(), TARGET);"); + assertThat(read("io/dagger/client/modules/beta/Beta.java")) + .contains( + "private static final ModuleTarget TARGET = ModuleTarget.atGitRef(\"beta\"," + + " \"github.com/dagger/beta@v1\", \"0123abc\");") + .contains("ModuleTargets.serve(dag.queryBuilder(), TARGET);"); + assertThat(read("io/dagger/client/Client.java")).doesNotContain("ModuleTargets"); + } + + /** + * A target the engine serves already — a local module reached from inside another module — gets + * bindings that ask for nothing. The descriptor is what decides, so the two cases differ by one + * file in the plan rather than by a switch in the generated code. + */ + @Test + void aTargetTheEngineServesCarriesNoDescriptorAndAsksForNothing() throws Exception { + writePlan( + CORE_WITH_TWO_TARGETS, servedByTheEngine("alpha", ALPHA), servedByTheEngine("beta", BETA)); + + generate(); + + String alpha = read("io/dagger/client/modules/alpha/Alpha.java"); + assertThat(alpha).doesNotContain("ModuleTarget").doesNotContain("TARGET"); + assertThat(alpha).contains("public static Alpha alpha(Client dag, String source)"); + assertThat(shimOf(alpha, "asAlpha")) + .isEqualTo( + """ + public static Alpha asAlpha(Binding binding) { + QueryBuilder nextQueryBuilder = binding.queryBuilder().chain("asAlpha"); + return new Alpha(nextQueryBuilder); + } + """ + .stripTrailing()); + } + + @Test + void aCoreTypeIsNotServed() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + String container = read("io/dagger/client/Container.java"); + assertThat(container).doesNotContain("ModuleTargets"); + } + + @Test + void theSamePlanGeneratesTheSameBytesWhateverOrderItsEntriesAreLaidOutIn() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + generate(); + String first = read("io/dagger/client/Client.java"); + + Path reversed = Files.createTempDirectory("plan-reversed"); + writePlanAt(reversed, CORE_WITH_TWO_TARGETS, target("beta", BETA), target("alpha", ALPHA)); + Path secondOut = Files.createTempDirectory("out-reversed"); + new Generator(secondOut, StandardCharsets.UTF_8, VERSION) + .generate(GenerationPlan.read(reversed)); + + assertThat(Files.readString(secondOut.resolve("io/dagger/client/Client.java"))) + .isEqualTo(first); + } + + @Test + void aModuleTheCoreSchemaMentionsButThePlanDoesNotGenerateIsRefused() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA)); + + assertThatThrownBy(this::generate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("beta"); + } + + @Test + void aPlanWithNoCoreSchemaIsRefused() throws Exception { + assertThatThrownBy(() -> GenerationPlan.read(plan)) + .isInstanceOf(IOException.class) + .hasMessageContaining("no core schema"); + } + + /** + * A target's local types are namespaced under its own name, so {@code foo} and {@code foo-bar} + * can both claim {@code FooBar} while their packages, {@code foo} and {@code foobar}, do not + * collide. + */ + @Test + void twoTargetsOwningOneTypeNameAreRefused() throws Exception { + writePlan( + CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA_OWNING_ALPHAS_TYPE)); + + assertThatThrownBy(this::generate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("alpha") + .hasMessageContaining("beta") + .hasMessageContaining("AlphaReport"); + } + + @Test + void aTargetOwningATypeNameCoreAlsoHasIsRefused() throws Exception { + writePlan( + CORE_WITH_TWO_TARGETS, target("alpha", ALPHA_OWNING_A_CORE_TYPE), target("beta", BETA)); + + assertThatThrownBy(this::generate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("alpha") + .hasMessageContaining("Container"); + } + + private void generate() throws IOException { + new Generator(out, StandardCharsets.UTF_8, VERSION).generate(GenerationPlan.read(plan)); + } + + private record Entry(String module, String schema, String source) {} + + /** A target the client loads itself, from the workspace. */ + private static Entry target(String module, String schema) { + return new Entry(module, schema, "workspace\n/dagger/modules/" + module); + } + + /** The same, from git at a pin. */ + private static Entry gitTarget(String module, String schema) { + return new Entry(module, schema, "git\ngithub.com/dagger/" + module + "@v1\n0123abc"); + } + + /** A target the engine serves on its own, so the client asks for nothing. */ + private static Entry servedByTheEngine(String module, String schema) { + return new Entry(module, schema, null); + } + + private void writePlan(String coreSchema, Entry... targets) throws IOException { + writePlanAt(plan, coreSchema, targets); + } + + private static void writePlanAt(Path root, String coreSchema, Entry... targets) + throws IOException { + Files.createDirectories(root.resolve("core")); + Files.writeString(root.resolve("core/schema.json"), coreSchema); + for (int i = 0; i < targets.length; i++) { + Path entry = root.resolve("target-" + i); + Files.createDirectories(entry); + Files.writeString(entry.resolve("schema.json"), targets[i].schema()); + Files.writeString(entry.resolve("module"), targets[i].module()); + if (targets[i].source() != null) { + Files.writeString(entry.resolve("source"), targets[i].source()); + } + } + } + + private List emitted() throws IOException { + try (Stream files = Files.walk(out)) { + return files + .filter(Files::isRegularFile) + .map(path -> out.relativize(path).toString()) + .sorted() + .toList(); + } + } + + /** One generated method, from its signature to the line that closes it. */ + private static String shimOf(String source, String name) { + List lines = source.lines().toList(); + int start = + IntStream.range(0, lines.size()) + .filter(i -> lines.get(i).contains(" " + name + "(")) + .findFirst() + .orElseThrow(() -> new AssertionError("no method named " + name)); + int end = start; + while (!lines.get(end).equals(" }")) { + end++; + } + return String.join("\n", lines.subList(start, end + 1)); + } + + private String read(String relative) throws IOException { + return Files.readString(out.resolve(relative)); + } + + private static final String VERSION = "v1.0.0-beta.13"; + + private static String owned(String module) { + return "\"directives\": [{\"name\": \"sourceMap\", \"args\": [{\"name\": \"module\"," + + " \"value\": \"\\\"" + + module + + "\\\"\"}]}]"; + } + + private static final String CORE_WITH_TWO_TARGETS = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Container"}}, "args": []}, + {"name": "alpha", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Alpha"}}, "args": [{"name": "source", "type": {"kind": "NON_NULL", "ofType": {"kind": "SCALAR", "name": "String"}}}, {"name": "tag", "type": {"kind": "SCALAR", "name": "String"}}], %s}, + {"name": "beta", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Beta"}}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Binding", "fields": [ + {"name": "asAlpha", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Alpha"}}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Alpha", "fields": [], %s}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], %s}, + {"kind": "OBJECT", "name": "Beta", "fields": [], %s} + ]}} + """ + .formatted( + owned("alpha"), + owned("beta"), + owned("alpha"), + owned("alpha"), + owned("alpha"), + owned("beta")); + + private static final String ALPHA = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Container"}}, "args": []}, + {"name": "alpha", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Alpha"}}, "args": [{"name": "source", "type": {"kind": "NON_NULL", "ofType": {"kind": "SCALAR", "name": "String"}}}, {"name": "tag", "type": {"kind": "SCALAR", "name": "String"}}], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Binding", "fields": [ + {"name": "asAlpha", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Alpha"}}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Alpha", "fields": [ + {"name": "base", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Container"}}, "args": []} + ], %s}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], %s} + ]}} + """ + .formatted(owned("alpha"), owned("alpha"), owned("alpha"), owned("alpha")); + + /** Beta owns a type alpha owns too, as two targets whose namespaced names meet would. */ + private static final String BETA_OWNING_ALPHAS_TYPE = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "container", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Container"}}, "args": []}, + {"name": "beta", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Beta"}}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": []}, + {"kind": "OBJECT", "name": "Beta", "fields": [], %s}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], %s} + ]}} + """ + .formatted(owned("beta"), owned("beta"), owned("beta")); + + /** Alpha owns a type core has, which no target may take over. */ + private static final String ALPHA_OWNING_A_CORE_TYPE = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "alpha", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Alpha"}}, "args": [{"name": "source", "type": {"kind": "NON_NULL", "ofType": {"kind": "SCALAR", "name": "String"}}}, {"name": "tag", "type": {"kind": "SCALAR", "name": "String"}}], %s} + ]}, + {"kind": "OBJECT", "name": "Container", "fields": [], %s}, + {"kind": "OBJECT", "name": "Alpha", "fields": [], %s}, + {"kind": "OBJECT", "name": "AlphaReport", "fields": [], %s} + ]}} + """ + .formatted(owned("alpha"), owned("alpha"), owned("alpha"), owned("alpha")); + + private static final String BETA = + """ + {"__schema": {"queryType": {"name": "Query"}, "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "beta", "type": {"kind": "NON_NULL", "ofType": {"kind": "OBJECT", "name": "Beta"}}, "args": [], %s} + ]}, + {"kind": "OBJECT", "name": "Beta", "fields": [], %s} + ]}} + """ + .formatted(owned("beta"), owned("beta")); +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java index 00bdca9..d4899c9 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/NullableObjectCodegenTest.java @@ -199,7 +199,8 @@ private Map sources(Type... types) throws Exception { sources.put( qualifiedName, javaFile( - new ObjectVisitor(schema, REGISTRY, Path.of("."), StandardCharsets.UTF_8) + new ObjectVisitor( + schema, REGISTRY, null, null, Path.of("."), StandardCharsets.UTF_8) .generateType(type))); } } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java index 49da79c..047e485 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/OptionalArgsCodegenTest.java @@ -69,6 +69,8 @@ private static String generateQuery(Field field) throws Exception { new ObjectVisitor( schema, TypeRegistry.singlePackage("io.dagger.client"), + null, + null, Path.of("."), StandardCharsets.UTF_8) .generateType(query); diff --git a/sdk/dagger-java-sdk/pom.xml b/sdk/dagger-java-sdk/pom.xml index bc41b55..c9468dc 100644 --- a/sdk/dagger-java-sdk/pom.xml +++ b/sdk/dagger-java-sdk/pom.xml @@ -81,6 +81,7 @@ ${daggerengine.version} ${daggerengine.schema} + ${daggerengine.plan} diff --git a/sdk/pom.xml b/sdk/pom.xml index bb7b189..fa8eaa9 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -252,6 +252,7 @@ UTF-8 0.21.4 + ", start) + 3; + if (depth == 2) { + commentStart = start; + commentIsMarker = reader.getText().contains(MARKER); + } + } + case XMLStreamConstants.CDATA -> cursor = text.indexOf("]]>", cursor) + 3; + case XMLStreamConstants.PROCESSING_INSTRUCTION -> + cursor = text.indexOf("?>", text.indexOf('<', cursor)) + 2; + case XMLStreamConstants.CHARACTERS, XMLStreamConstants.SPACE -> { + if (id != null) { + id.append(reader.getText()); + } + } + case XMLStreamConstants.START_ELEMENT -> { + depth++; + startTag = text.indexOf('<', cursor); + cursor = startTagEnd(text, startTag); + selfClosed = text.charAt(cursor - 2) == '/'; + String name = reader.getLocalName(); + if (depth == 1) { + if (!name.equals("project")) { + // This writes into a file the SDK does not own; splicing a into + // something that is not a pom would be a silent, destructive mistake. + throw new IllegalArgumentException( + "the root element is <" + name + ">, not ; this is not a Maven pom"); + } + scan.projectTagEnd = cursor; + } else if (depth == 2 && name.equals("profiles")) { + scan.profilesStart = startTag; + scan.profilesSelfClosed = selfClosed; + } else if (depth == 3 && name.equals("profile") && scan.profilesStart >= 0) { + profileStart = startTag; + if (scan.firstProfileStart < 0) { + scan.firstProfileStart = startTag; + } + } else if (depth == 4 && name.equals("id") && profileStart >= 0 && profileId == null) { + id = new StringBuilder(); + } + } + case XMLStreamConstants.END_ELEMENT -> { + int start = startTag; + if (!selfClosed) { + start = text.indexOf('<', cursor); + cursor = text.indexOf('>', start) + 1; + } + selfClosed = false; + String name = reader.getLocalName(); + if (depth == 4 && name.equals("id") && id != null) { + profileId = id.toString().trim(); + id = null; + } else if (depth == 3 && name.equals("profile") && profileStart >= 0) { + if (PROFILE_ID.equals(profileId)) { + if (commentIsMarker) { + scan.markedProfileStart = commentStart; + scan.markedProfileEnd = cursor; + } else { + scan.unmarkedProfile = true; + } + } + profileStart = -1; + profileId = null; + id = null; + commentStart = -1; + commentIsMarker = false; + } else if (depth == 2 && name.equals("profiles")) { + scan.profilesEndTagStart = start; + scan.profilesEnd = cursor; + } else if (depth == 1) { + scan.projectEndTagStart = start; + } + depth--; + } + default -> {} + } + } + return scan; + } + + /** The offset just past a start tag, whose attribute values may hold a {@code '>'}. */ + private static int startTagEnd(String text, int start) { + char quote = 0; + for (int at = start; at < text.length(); at++) { + char c = text.charAt(at); + if (quote != 0) { + if (c == quote) { + quote = 0; + } + } else if (c == '"' || c == '\'') { + quote = c; + } else if (c == '>') { + return at + 1; + } + } + throw new IllegalStateException("unterminated start tag at " + start); + } + + private static final String DEFAULT_INDENT = " "; + + private static final String PROFILE = + """ + + + dagger-clients + + + dagger/src/main/java + + + + + + io.opentelemetry + opentelemetry-bom + 1.61.0 + pom + import + + + + + + jakarta.json + jakarta.json-api + 2.1.3 + + + jakarta.json.bind + jakarta.json.bind-api + 3.0.1 + + + org.slf4j + slf4j-api + 2.0.17 + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry + opentelemetry-exporter-sender-okhttp + + + + + io.opentelemetry + opentelemetry-exporter-sender-jdk + runtime + + + org.eclipse + yasson + 3.0.4 + runtime + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-dagger-client-sources + + add-source + + + + dagger/src/main/java + + + + + + + + """; +} diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ClientPomMojo.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ClientPomMojo.java new file mode 100644 index 0000000..2dc4efc --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ClientPomMojo.java @@ -0,0 +1,34 @@ +package io.dagger.codegen; + +import java.io.File; +import java.io.IOException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + * Registers the generated Dagger client sources with a {@code pom.xml} the SDK does not own, by + * writing a single {@code dagger-clients} profile into it. Everything else in the file is left as + * it was. + */ +@Mojo(name = "client-pom", requiresProject = false, threadSafe = true) +public class ClientPomMojo extends AbstractMojo { + + /** The {@code pom.xml} of the scope to register the generated sources with. */ + @Parameter(property = "dagger.pom", defaultValue = "${basedir}/pom.xml", required = true) + private File pomFile; + + @Override + public void execute() throws MojoFailureException { + try { + if (ClientPom.insertInto(pomFile.toPath())) { + getLog().info(String.format("Wrote the %s profile to %s", ClientPom.PROFILE_ID, pomFile)); + } else { + getLog().info(String.format("%s is already up to date", pomFile)); + } + } catch (IOException | IllegalStateException e) { + throw new MojoFailureException(e.getMessage(), e); + } + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ClientPomTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ClientPomTest.java new file mode 100644 index 0000000..e0726d5 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ClientPomTest.java @@ -0,0 +1,396 @@ +package io.dagger.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.DocumentBuilderFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +class ClientPomTest { + + @TempDir Path scope; + + @Test + void aMinimalPomGainsTheProfile() throws Exception { + Path pom = write(MINIMAL); + + ClientPom.insertInto(pom); + + Element profile = profile(pom, "dagger-clients"); + assertThat(text(profile, "activation/file/exists")).isEqualTo("dagger/src/main/java"); + assertThat(text(profile, "build/plugins/plugin/artifactId")) + .isEqualTo("build-helper-maven-plugin"); + assertThat(text(profile, "build/plugins/plugin/version")).isEqualTo("3.3.0"); + assertThat(all(profile, "source")).containsExactly("dagger/src/main/java"); + // One source root and nothing else: the generated tree is all Java, so a + // resource root would be a directory the SDK never writes. + assertThat(all(profile, "goal")).containsExactly("add-source"); + } + + @Test + void theProfileDeclaresTheSdkRuntimeDependenciesAtExplicitVersions() throws Exception { + Path pom = write(MINIMAL); + + ClientPom.insertInto(pom); + + Element dependencies = child(profile(pom, "dagger-clients"), "dependencies"); + assertThat(coordinates(dependencies)) + .containsExactly( + "jakarta.json:jakarta.json-api:2.1.3", + "jakarta.json.bind:jakarta.json.bind-api:3.0.1", + "org.slf4j:slf4j-api:2.0.17", + "io.opentelemetry:opentelemetry-api:", + "io.opentelemetry:opentelemetry-sdk:", + "io.opentelemetry:opentelemetry-exporter-otlp:", + "io.opentelemetry:opentelemetry-exporter-sender-jdk:", + "org.eclipse:yasson:3.0.4"); + assertThat(all(profile(pom, "dagger-clients"), "exclusion")) + .containsExactly("io.opentelemetryopentelemetry-exporter-sender-okhttp"); + assertThat( + text( + profile(pom, "dagger-clients"), + "dependencyManagement/dependencies/dependency/version")) + .isEqualTo("1.61.0"); + } + + /** slf4j-simple is the application's choice, not the SDK's. */ + @Test + void theProfileDoesNotChooseALoggingImplementation() throws Exception { + Path pom = write(MINIMAL); + + ClientPom.insertInto(pom); + + assertThat(Files.readString(pom)).doesNotContain("slf4j-simple"); + } + + @Test + void insertingTwiceChangesNothingTheSecondTime() throws Exception { + Path pom = write(MINIMAL); + assertThat(ClientPom.insertInto(pom)).isTrue(); + byte[] once = Files.readAllBytes(pom); + + assertThat(ClientPom.insertInto(pom)).isFalse(); + + assertThat(Files.readAllBytes(pom)).isEqualTo(once); + } + + @Test + void anUnrelatedProfileIsKept() throws Exception { + Path pom = write(WITH_OTHER_PROFILE); + + ClientPom.insertInto(pom); + + assertThat(profileIds(pom)).containsExactly("release", "dagger-clients"); + assertThat(text(profile(pom, "release"), "properties/skipTests")).isEqualTo("true"); + } + + @Test + void aProfileTheGoalWroteIsReplacedNotDuplicated() throws Exception { + Path pom = write(MINIMAL); + ClientPom.insertInto(pom); + Files.writeString(pom, Files.readString(pom).replace("2.0.17", "1.7.36")); + + assertThat(ClientPom.insertInto(pom)).isTrue(); + + assertThat(profileIds(pom)).containsExactly("dagger-clients"); + assertThat(Files.readString(pom)).contains("2.0.17").doesNotContain("1.7.36"); + assertThat(occurrences(Files.readString(pom), ClientPom.MARKER)).isEqualTo(1); + } + + @Test + void aProfileTheUserWroteIsRefused() throws Exception { + Path pom = write(WITH_HAND_WRITTEN_PROFILE); + String before = Files.readString(pom); + + assertThatThrownBy(() -> ClientPom.insertInto(pom)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(pom.toString()) + .hasMessageContaining("dagger-clients"); + + assertThat(Files.readString(pom)).isEqualTo(before); + } + + @Test + void anXmlFileThatIsNotAPomIsRefused() throws Exception { + Path settings = write("\n"); + String before = Files.readString(settings); + + assertThatThrownBy(() -> ClientPom.insertInto(settings)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a Maven pom"); + + assertThat(Files.readString(settings)).isEqualTo(before); + } + + @Test + void aPomWithNoProfilesElementGainsOne() throws Exception { + Path pom = write(MINIMAL); + + ClientPom.insertInto(pom); + + assertThat(document(pom).getElementsByTagName("profiles").getLength()).isEqualTo(1); + assertThat(profileIds(pom)).containsExactly("dagger-clients"); + } + + @Test + void anEmptyProfilesElementIsFilledIn() throws Exception { + Path pom = write(MINIMAL.replace("", " \n")); + + ClientPom.insertInto(pom); + + assertThat(profileIds(pom)).containsExactly("dagger-clients"); + assertThat(document(pom).getElementsByTagName("profiles").getLength()).isEqualTo(1); + } + + @Test + void theRootElementsAttributesAreUntouched() throws Exception { + Path pom = write(NAMESPACED); + + ClientPom.insertInto(pom); + + Element project = document(pom).getDocumentElement(); + assertThat(project.getAttribute("xmlns")).isEqualTo("http://maven.apache.org/POM/4.0.0"); + assertThat(project.getAttribute("xmlns:xsi")) + .isEqualTo("http://www.w3.org/2001/XMLSchema-instance"); + assertThat(project.getAttribute("xsi:schemaLocation")) + .isEqualTo("http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"); + assertThat(Files.readString(pom)).startsWith(NAMESPACED.substring(0, NAMESPACED.indexOf('>'))); + } + + @Test + void theUsersCommentsAreKept() throws Exception { + Path pom = write(WITH_COMMENTS); + + ClientPom.insertInto(pom); + + assertThat(Files.readString(pom)) + .contains("") + .contains(""); + } + + @Test + void crlfLineEndingsAreNotRewritten() throws Exception { + Path pom = write(MINIMAL.replace("\n", "\r\n")); + + ClientPom.insertInto(pom); + + String written = Files.readString(pom); + assertThat(written).contains("\r\n"); + assertThat(written.replace("\r\n", "")).doesNotContain("\n"); + } + + @Test + void theResultIsAPomMavenCanStillRead() throws Exception { + Path pom = write(NAMESPACED); + + ClientPom.insertInto(pom); + + Document document = document(pom); + assertThat(document.getDocumentElement().getNodeName()).isEqualTo("project"); + assertThat(text(document.getDocumentElement(), "modelVersion")).isEqualTo("4.0.0"); + assertThat(profileIds(pom)).containsExactly("dagger-clients"); + assertThat(profile(pom, "dagger-clients").getParentNode().getNodeName()).isEqualTo("profiles"); + } + + /** Maven reads a pom that starts with a byte-order mark, so this goal has to as well. */ + @Test + void aPomWithAByteOrderMarkKeepsIt() throws Exception { + Path pom = write("\uFEFF" + MINIMAL); + + assertThat(ClientPom.insertInto(pom)).isTrue(); + + assertThat(Files.readAllBytes(pom)).startsWith((byte) 0xEF, (byte) 0xBB, (byte) 0xBF); + assertThat(profileIds(pom)).containsExactly("dagger-clients"); + } + + @Test + void theUsersFileModeIsKept() throws Exception { + Path pom = write(MINIMAL); + Files.setPosixFilePermissions(pom, PosixFilePermissions.fromString("rw-r-----")); + + ClientPom.insertInto(pom); + + assertThat(PosixFilePermissions.toString(Files.getPosixFilePermissions(pom))) + .isEqualTo("rw-r-----"); + } + + private Path write(String pom) throws Exception { + Path path = scope.resolve("pom.xml"); + Files.write(path, pom.getBytes(StandardCharsets.UTF_8)); + return path; + } + + private static Document document(Path pom) throws Exception { + return DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new ByteArrayInputStream(Files.readAllBytes(pom))); + } + + private static List profileIds(Path pom) throws Exception { + NodeList profiles = document(pom).getElementsByTagName("profile"); + List ids = new ArrayList<>(); + for (int i = 0; i < profiles.getLength(); i++) { + ids.add(text((Element) profiles.item(i), "id")); + } + return ids; + } + + private static Element profile(Path pom, String id) throws Exception { + NodeList profiles = document(pom).getElementsByTagName("profile"); + for (int i = 0; i < profiles.getLength(); i++) { + Element profile = (Element) profiles.item(i); + if (id.equals(text(profile, "id"))) { + return profile; + } + } + throw new AssertionError("no profile with the id " + id); + } + + private static Element child(Element parent, String name) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(name)) { + return (Element) node; + } + } + throw new AssertionError("no child element " + name); + } + + private static String text(Element parent, String path) { + Element element = parent; + for (String name : path.split("/")) { + element = child(element, name); + } + return element.getTextContent().trim(); + } + + private static List all(Element parent, String name) { + NodeList nodes = parent.getElementsByTagName(name); + List values = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + values.add(nodes.item(i).getTextContent().trim().replaceAll("\\s+", "")); + } + return values; + } + + private static List coordinates(Element dependencies) { + NodeList nodes = dependencies.getElementsByTagName("dependency"); + List coordinates = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + Element dependency = (Element) nodes.item(i); + coordinates.add( + text(dependency, "groupId") + + ":" + + text(dependency, "artifactId") + + ":" + + optional(dependency, "version")); + } + return coordinates; + } + + private static String optional(Element parent, String name) { + NodeList nodes = parent.getElementsByTagName(name); + return nodes.getLength() == 0 ? "" : nodes.item(0).getTextContent().trim(); + } + + private static int occurrences(String text, String needle) { + int count = 0; + for (int at = text.indexOf(needle); at >= 0; at = text.indexOf(needle, at + 1)) { + count++; + } + return count; + } + + private static final String MINIMAL = + """ + + + 4.0.0 + com.example + app + 1.0-SNAPSHOT + + """; + + private static final String NAMESPACED = + """ + + + 4.0.0 + com.example + app + 1.0-SNAPSHOT + + """; + + private static final String WITH_OTHER_PROFILE = + """ + + + 4.0.0 + com.example + app + 1.0-SNAPSHOT + + + release + + true + + + + + """; + + private static final String WITH_HAND_WRITTEN_PROFILE = + """ + + + 4.0.0 + com.example + app + 1.0-SNAPSHOT + + + dagger-clients + + yes + + + + + """; + + private static final String WITH_COMMENTS = + """ + + + 4.0.0 + com.example + + app + 1.0-SNAPSHOT + + + + release + + + + """; +} From ad3585ab5dd1ee1433dd4c7aca2efd93be9790b1 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Sun, 13 Sep 2026 23:51:33 +0200 Subject: [PATCH 14/28] prebuilt: rebuild the codegen plugin Generation seeds the local Maven repository from prebuilt/m2 whenever it exists and never compiles the plugin sources in that case, so the driver patches that follow would run the old generator without this. Signed-off-by: Yves Brissaud --- .../dagger-codegen-maven-plugin-0.21.4.jar | Bin 71775 -> 113746 bytes .../0.21.4/dagger-sdk-parent-0.21.4.pom | 1 + 2 files changed, 1 insertion(+) diff --git a/prebuilt/m2/io/dagger/dagger-codegen-maven-plugin/0.21.4/dagger-codegen-maven-plugin-0.21.4.jar b/prebuilt/m2/io/dagger/dagger-codegen-maven-plugin/0.21.4/dagger-codegen-maven-plugin-0.21.4.jar index 63104ca930a8f7280c872a363411061e99bb3708..ef17e4752afa63f7f69b814510f1c9d943edbe21 100644 GIT binary patch delta 91331 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

L~UlaWR;)rWuBVqAz>(V|Oe)*{KJ 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⪻Z4|$JJGtJadm7j1 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 delta 50287 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 0.21.4 + + io.dagger.e2e + standalone-client + 1.0-SNAPSHOT + + + 17 + UTF-8 + + diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 58a006b..89d0abc 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -30,6 +30,18 @@ type E2e { let nonJavaModulePath: String! = fixtureRoot + "/lookup/not-java" let clientDepPath: String! = fixtureRoot + "/clients/dep" + # A Maven project that is not a Dagger module: the standalone scope. + let standalonePath: String! = fixtureRoot + "/clients/standalone" + let standaloneSourceRoot: String! = standalonePath + "/dagger/src/main/java" + + """ + The standalone scope's targets: one in the workspace and one at a git ref, so + both descriptor kinds are generated and both packages land. + """ + let standaloneClients(ws: Workspace!): [ModuleSource!]! { + [ws.moduleSource("/" + clientDepPath), moduleSource(gitClientRef, refPin: gitClientPin)] + } + # A git client, because a local module source carries no commit and so can # never show a pin, at an immutable tag so the commit asserted on cannot move. # It gets a module name of its own rather than sharing one: the dependency @@ -325,6 +337,194 @@ type E2e { null } + """ + A scope with no module of its own gets the same bindings for the same target + as a module does, under a dagger/ directory of its own, plus a profile in the + project's own pom so its build sees the sources. Every target it names is one + the generated package loads itself, local or git alike. + """ + generateScopeStandaloneCheck(ws: Workspace!): Void @check { + let scoped = ws.withWorkdir(standalonePath) + let generated = javaSdk.generateScope( + scoped, + isModule: false, + name: "", + clients: standaloneClients(ws), + ) + + assert(generated.cwd == scoped.cwd, "generateScope must not change the workspace cwd") + + let clientPackage = standaloneSourceRoot + "/io/dagger/client/modules/clientdep" + assert( + contains(generated.directory("/" + clientPackage).entries, "ClientDep.java"), + "no ClientDep.java under " + clientPackage, + ) + # One source root, so the hand-written runtime and core sit beside the + # target's package rather than in a second tree the user's pom would have to + # be told about. + assert( + generated.directory("/" + standaloneSourceRoot).exists("io/dagger/client/Dagger.java"), + "the standalone tree should carry the SDK runtime", + ) + + # Two targets, one core: the git one is there because a client that travels + # outside its workspace can only be a git one. + assert( + contains( + generated.directory("/" + standaloneSourceRoot + "/io/dagger/client/modules/sdkhelpers").entries, + "SdkHelpers.java", + ), + "no SdkHelpers.java in the second target's package", + ) + # Core names no client package: a target is reached through its own package + # and its own entry point, never by extending the global client. + assertNotContains( + generated.file("/" + standaloneSourceRoot + "/io/dagger/client/Client.java").contents, + "io.dagger.client.modules", + "core should not name a client package", + ) + + # Each package carries the descriptor it was generated against and loads its + # own module. This is the whole of what used to be a separate provider class + # and a service registration. + assertContainsAll( + generated.file("/" + clientPackage + "/ClientDep.java").contents, + [ + # Workspace-root-absolute: the engine resolves a bare path from wherever + # the program was run, and only a leading slash means the same thing + # every time. + "ModuleTarget.inWorkspace(\"client-dep\", \"/" + clientDepPath + "\")", + "ModuleTargets.serve(", + ], + ) + assertContainsAll( + generated + .file("/" + standaloneSourceRoot + "/io/dagger/client/modules/sdkhelpers/SdkHelpers.java") + .contents, + [ + # A git target records the commit it resolved to, so the program reaches + # the module the bindings were generated from. + "ModuleTarget.atGitRef(\"" + gitClientName + "\", \"" + gitClientRef + "\", \"" + + gitClientPin + "\")", + "ModuleTargets.serve(", + ], + ) + + let pom = generated.file("/" + standalonePath + "/pom.xml").contents + assertContainsAll(pom, [ + "dagger-clients", + "dagger/src/main/java", + "dagger/src/main/java", + ]) + let again = javaSdk.generateScope( + generated.withWorkdir(standalonePath), + isModule: false, + name: "", + clients: standaloneClients(ws), + ) + assert( + again.file("/" + standalonePath + "/pom.xml").contents == pom, + "generating twice should not write the profile twice", + ) + + null + } + + """ + The generated standalone project is buildable Java. `mvn package` compiles it + in a container with no engine in reach and no Dagger CLI on PATH. + + This is the only check that compiles a standalone scope, and it catches what + no assertion on file contents can: a generated source that names a type the + generator emitted as bare text and never imported. Every other standalone + check reads the files rather than building them. + """ + standaloneCompilesCheck(ws: Workspace!): Void @check { + let generated = javaSdk.generateScope( + ws.withWorkdir(standalonePath), + isModule: false, + name: "", + clients: standaloneClients(ws), + ) + container + .from("maven:3.9.9-eclipse-temurin-21-alpine@sha256:4cbb8bf76c46b97e028998f2486ed014759a8e932480431039bdb93dffe6813e") + .withoutEntrypoint + .withMountedCache("/root/.m2", cacheVolume("e2e-standalone-m2")) + .withDirectory("/app", generated.directory("/" + standalonePath)) + .withWorkdir("/app") + .withExec(["mvn", "package", "-Dmaven.test.skip=true", "--no-transfer-progress"]) + # Asserted, because a profile that failed to activate would compile + # nothing and `mvn package` would still succeed. Both client packages and + # a core class, so a build that saw only the runtime cannot pass either. + .withExec([ + "test", + "-f", + "target/classes/io/dagger/client/modules/clientdep/ClientDep.class", + ]) + .withExec([ + "test", + "-f", + "target/classes/io/dagger/client/modules/sdkhelpers/SdkHelpers.class", + ]) + .withExec(["test", "-f", "target/classes/io/dagger/client/Client.class"]) + .sync + + null + } + + """ + The payoff: the bindings for a target are the same artifact wherever they are + generated, and do not depend on what else is generated beside them. + + The two scopes deliberately differ in both ways at once. One is a module and + one is not, and the module has this client alone while the standalone project + has it alongside another. The package still has to come out identical, down to + the descriptor it loads its module from. + + The target compared is the git one, because it is the one both scopes can load + for themselves today. A module runtime has no session to resolve a workspace + path against, so a local target inside a module is still served by the engine + and its package is the one place the two scopes have left to differ. Closing + that is an engine change, not an SDK one; when it lands, modulePlan passes + servesWorkspacePaths: true and this check covers both kinds. + """ + clientsAreOneArtifactCheck(ws: Workspace!): Void @check { + let packageSubpath = "io/dagger/client/modules/sdkhelpers" + + let inModule = javaSdk.generateScope( + ws.withWorkdir(gitAppPath), + isModule: true, + name: gitAppName, + clients: [moduleSource(gitClientRef, refPin: gitClientPin)], + ) + let standalone = javaSdk.generateScope( + ws.withWorkdir(standalonePath), + isModule: false, + name: "", + clients: standaloneClients(ws), + ) + + let fromModule = inModule.directory( + "/" + gitAppPath + "/sdk/src/generated/java/" + packageSubpath, + ) + let fromStandalone = standalone.directory("/" + standaloneSourceRoot + "/" + packageSubpath) + # Asserted so that two empty directories, which would also have one digest, + # cannot pass for two identical packages. + assert( + contains(fromModule.entries, "SdkHelpers.java"), + "the module scope generated no client package to compare", + ) + assert( + fromModule.digest == fromStandalone.digest, + "a target's package differs between a module and a standalone project: " + + fromModule.entries.join(", ") + + " against " + + fromStandalone.entries.join(", "), + ) + + null + } + """ A client constructor argument a caller may omit — non-null with a default the engine fills in, or nullable — is bound as an optional argument, not as a diff --git a/client.dang b/client.dang new file mode 100644 index 0000000..b56091d --- /dev/null +++ b/client.dang @@ -0,0 +1,92 @@ +""" +A Maven project that is not a Dagger module, and the modules it calls. + +The bindings it gets for a target are the ones a module would get for the same +target: the same plan entry, the same generator, the same package. The plan +entry carries where the target lives, so the generated package loads it itself; +what this adds is a way for the project's own build to see those sources. +""" +type ClientScope { + """ + Workspace-root-relative path of the Maven project this generates into. + """ + rootPath: String! + + """ + Workspace containing the project. + """ + let ws: Workspace! + + """ + The modules this project calls. + """ + let targets: [ModuleSource!]! + + """ + The version the SDK jars are installed under while generating. + """ + let sdkVersion: String! + + let codegen: Codegen! { Codegen() } + + """ + The workspace with the generated clients under /dagger, and the scope's + own pom registering them. + """ + generated: Workspace! { + let plan = codegen.clientPlan(targets) + let vendored = codegen.vendoredClients(plan, sdkVersion) + let daggerDir = workspaceRef(joinPath(rootPath, "dagger")) + + # Replaced rather than merged: a target that is no longer requested has to + # lose its package, and an overlay would leave it behind. + ws + .withoutDirectory(daggerDir) + .withNewDirectory(daggerDir, vendored) + .withFile( + workspaceRef(joinPath(rootPath, "pom.xml")), + registeredPom(ws.file(workspaceRef(joinPath(rootPath, "pom.xml")))), + ) + } + + """ + The version the codegen plugin is installed under. Fixed, and independent of + the per-scope version the SDK jars take. + """ + let codegenPluginVersion: String! { "0.21.4" } + + """ + The scope's pom with the profile that compiles and packages the generated + clients. The pom belongs to the user, so the goal writes one marked element + and leaves everything else alone. + + Run from an empty directory rather than from the pom's own: Maven would + otherwise read the user's pom as the project it is building, and a parent or + plugin it cannot resolve here would fail generation for no reason. + """ + let registeredPom(pom: File!): File! { + codegen.pluginBase + .withFile("/scope/pom.xml", pom) + .withExec(["mkdir", "-p", "/work"]) + .withWorkdir("/work") + .withExec([ + "mvn", + "io.dagger:dagger-codegen-maven-plugin:" + codegenPluginVersion + ":client-pom", + "-Ddagger.pom=/scope/pom.xml", + "--no-transfer-progress", + ]) + .file("/scope/pom.xml") + } + + """ + A path as a workspace-root-absolute path, the form Workspace resolves from the + workspace root rather than from the client's cwd. + """ + let workspaceRef(path: String!): String! { + if (path == ".") { "/" } else { "/" + path } + } + + let joinPath(path: String!, sub: String!): String! { + if (path == ".") { sub } else { path + "/" + sub } + } +} diff --git a/codegen.dang b/codegen.dang index 50a999e..6774279 100644 --- a/codegen.dang +++ b/codegen.dang @@ -1,9 +1,10 @@ """ -Code generation, shared by every Java scope. +Turning a generation plan into a vendored tree. A module's vendored SDK and a standalone project's client tree come out of the -same plan and the same Maven invocation. What differs is the plan: a module has -a schema of its own for core, a standalone project has only its targets. +same plan and the same Maven invocation. What differs is the plan — a module has +a schema of its own for core, a standalone project has only its targets — and +what is laid out around the generated sources. """ type Codegen { """ @@ -128,19 +129,24 @@ type Codegen { } """ - A maven container with the codegen plugin available in the local repository, - and the plan mounted at /plan. + The codegen container with a plan mounted at /plan. + """ + let codegenBase(plan: Directory!): Container! { + pluginBase.withMountedDirectory("/plan", plan) + } + + """ + A maven container with the codegen plugin available in the local repository. Fast path: when the packager module has committed the plugin's local Maven repository under prebuilt/m2, drop it into ~/.m2/repository with a plain copy — no maven invocation. Otherwise fall back to installing a committed plugin jar, and finally to compiling the plugin from the vendored sources. """ - let codegenBase(plan: Directory!): Container! { + let pluginBase: Container! { let base = mvn .withoutEntrypoint .withMountedCache("/root/.m2", mavenRepo, sharing: CacheSharingMode.LOCKED) - .withMountedDirectory("/plan", plan) .withDirectory("/dagger-io", sdkSourceDir) .withWorkdir("/dagger-io") diff --git a/main.dang b/main.dang index c672dfe..ffeb4ff 100644 --- a/main.dang +++ b/main.dang @@ -58,8 +58,10 @@ type JavaSdk { Every module then gets a dagger-module.toml from the manifest builder and is generated; a pre-1.0 dagger.json is migrated into it and removed, so two manifest files cannot disagree. The scope's module clients become the module's - dependencies, so the generated bindings include their types. Standalone - clients, in a scope without a module, are not generated yet. + dependencies, so the generated bindings include their types. A scope with no + module gets the same bindings for the same clients under its own dagger/ + directory, and the descriptors and pom profile it needs to reach them and + build them. """ generateScope( ws: Workspace!, @@ -69,7 +71,16 @@ type JavaSdk { ): Workspace! { if (isModule == false) { if (clients.length > 0) { - raise "java-sdk does not generate standalone module clients yet" + let scope = normalizePath(ws.cwd) + # The same reason as the module branch: the engine resolves a target's + # local path relative to Workspace.cwd, and rejects a result whose cwd + # is not the scope. + ClientScope( + rootPath: scope, + ws: ws.withWorkdir("."), + targets: clients, + sdkVersion: clientSdkVersion(scope, name), + ).generated.withWorkdir(scope) } else { ws } @@ -97,6 +108,27 @@ type JavaSdk { } } + """ + The version the SDK jars are installed under while a client scope generates. + + Derived from the scope rather than shared, because the jars are cached under + this coordinate and two scopes that resolve to different schemas must not + collide on it. A scope the engine gave a name uses that name. + """ + let clientSdkVersion(scope: String!, name: String!): String! { + if (name != "") { + name + } else if (scope == ".") { + "client-root" + } else { + # A separator becomes a dash, and a dash already in the path is doubled, + # so two scopes cannot derive one version and install different jars under + # the same Maven coordinate. Scope paths are normalized, so a run of + # separators — the one shape this would not separate — cannot occur. + "client-" + scope.split("-").join("--").split("/").join("-") + } + } + """ Write the module's dagger-module.toml from the manifest it already has and the complete client set, and drop a pre-1.0 dagger.json once its contents have diff --git a/main.dang.tmpl b/main.dang.tmpl index 049a5a3..e08f2de 100644 --- a/main.dang.tmpl +++ b/main.dang.tmpl @@ -58,8 +58,10 @@ type JavaSdk { Every module then gets a dagger-module.toml from the manifest builder and is generated; a pre-1.0 dagger.json is migrated into it and removed, so two manifest files cannot disagree. The scope's module clients become the module's - dependencies, so the generated bindings include their types. Standalone - clients, in a scope without a module, are not generated yet. + dependencies, so the generated bindings include their types. A scope with no + module gets the same bindings for the same clients under its own dagger/ + directory, and the descriptors and pom profile it needs to reach them and + build them. """ generateScope( ws: Workspace!, @@ -69,7 +71,16 @@ type JavaSdk { ): Workspace! { if (isModule == false) { if (clients.length > 0) { - raise "java-sdk does not generate standalone module clients yet" + let scope = normalizePath(ws.cwd) + # The same reason as the module branch: the engine resolves a target's + # local path relative to Workspace.cwd, and rejects a result whose cwd + # is not the scope. + ClientScope( + rootPath: scope, + ws: ws.withWorkdir("."), + targets: clients, + sdkVersion: clientSdkVersion(scope, name), + ).generated.withWorkdir(scope) } else { ws } @@ -97,6 +108,27 @@ type JavaSdk { } } + """ + The version the SDK jars are installed under while a client scope generates. + + Derived from the scope rather than shared, because the jars are cached under + this coordinate and two scopes that resolve to different schemas must not + collide on it. A scope the engine gave a name uses that name. + """ + let clientSdkVersion(scope: String!, name: String!): String! { + if (name != "") { + name + } else if (scope == ".") { + "client-root" + } else { + # A separator becomes a dash, and a dash already in the path is doubled, + # so two scopes cannot derive one version and install different jars under + # the same Maven coordinate. Scope paths are normalized, so a run of + # separators — the one shape this would not separate — cannot occur. + "client-" + scope.split("-").join("--").split("/").join("-") + } + } + """ Write the module's dagger-module.toml from the manifest it already has and the complete client set, and drop a pre-1.0 dagger.json once its contents have From d4ffaaf3000b3977226725d3dd841a7a0be30651 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Mon, 14 Sep 2026 00:12:01 +0200 Subject: [PATCH 17/28] README: document standalone clients Signed-off-by: Yves Brissaud --- README.md | 106 ++++++++++++++++-- .../2026-08-17-nullable-object-returns.md | 6 + .../2026-09-04-sdk-module-interface.md | 5 + 3 files changed, 108 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e6e8e27..8fe12e1 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ time** — the runtime just builds and packages the module. > [!IMPORTANT] > This SDK implements the module-scope interface from > [dagger/dagger#13992](https://github.com/dagger/dagger/pull/13992) and needs an -> engine that has it. On the released engine (`v1.0.0-beta.11`) the module loads -> but every call into it fails. +> engine that has it. That change merged, and `v1.0.0-beta.13` carries it; on an +> earlier engine the module loads but every call into it fails. > > It writes module manifests through > [`github.com/dagger/sdk-helpers`](https://github.com/dagger/sdk-helpers), the @@ -114,13 +114,103 @@ Module dependencies are replaced by generated module clients: dagger module client add java ``` +Each client's types are generated into a package of their own, +`io.dagger.client.modules.`, from that client's own schema and nothing +else. The way in is a static method on the client's own root type, so one import +is the whole of the integration: + +```java +import static io.dagger.client.modules.sdkhelpers.SdkHelpers.sdkHelpers; + +sdkHelpers().moduleManifest().generate(); +``` + +The core client is not extended with an accessor for it. A client package is +self-contained: it reaches core types where they live, and nothing in core names +it. Pass a session explicitly when you have one — `sdkHelpers(dag)` — or let the +no-argument form use the ambient one. + In a module scope the client set becomes the module's dependency set. Each client is recorded in the manifest the module has — `dagger-module.toml`, or the -`dagger.json` of a pre-1.0 module — and its types are part of the generated -bindings; a client that is removed is dropped from both. +`dagger.json` of a pre-1.0 module — and a client that is removed is dropped from +both the manifest and the bindings. + +> [!WARNING] +> A client's types moved out of `io.dagger.client` in this release, and so did +> the way in. `dag().sdkHelpers()` becomes `sdkHelpers()` after a static import +> of `io.dagger.client.modules.sdkhelpers.SdkHelpers.sdkHelpers`, and each type +> is imported from `io.dagger.client.modules.` rather than from +> `io.dagger.client`. The `Arguments` holder moves with the method, onto +> the client's root type. + +## Standalone clients + +A Maven project that is no Dagger module can call modules too. Run the same +command inside it: + +```sh +cd my-java-app # any directory with a pom.xml +dagger module client add java github.com/dagger/sdk-helpers@v1.0.2 +dagger generate +``` + +`dagger generate` writes the client tree under `dagger/`, all of it SDK-owned +and regenerated whole: + +``` +my-java-app/ + pom.xml # gains one profile, see below + dagger/src/main/java/io/dagger/client/** # the SDK runtime and the core API + dagger/src/main/java/io/dagger/client/modules//** # one package per client +``` + +The bindings under `io.dagger.client.modules.` are the same files a +module gets for the same client. Only what surrounds them differs. + +Your own code then reads exactly as a module's does: + +```java +import static io.dagger.client.modules.sdkhelpers.SdkHelpers.sdkHelpers; + +public class App { + public static void main(String[] args) throws Exception { + System.out.println(sdkHelpers().moduleManifest().generate()); + } +} +``` + +Run it with a `dagger` binary on `PATH` and no wrapper command: + +```sh +mvn package +java -jar target/my-java-app-1.0-SNAPSHOT.jar +``` + +There is no session to join, so the SDK starts one with `dagger session`, and +each client asks the engine to load its module the first time your code reaches +for it. Set `_EXPERIMENTAL_DAGGER_CLI_BIN` to point at a specific binary. The +SDK does not download a CLI; install one first. + +The generated code needs Java 17, so the project's `maven.compiler.release` +(or `maven.compiler.source` and `maven.compiler.target`) has to be 17 or later. + +### The one thing written into your pom + +Your `pom.xml` is yours, so `dagger generate` adds exactly one element to it: a +profile with the id `dagger-clients`, carrying a comment that says what wrote +it. The profile adds `dagger/src/main/java` as a source root, along with the SDK's +own run-time dependencies. It activates on the presence of the generated tree, so deleting +`dagger/` makes it inert and deleting the profile removes the integration. + +Generation refuses to touch a `dagger-clients` profile that does not carry that +comment, on the assumption that you wrote it. + +### What a client is pinned to -Standalone clients — in a scope that has no Java module — are not generated yet. -Adding one is refused and the workspace is left unchanged. +A client added at a git ref records the commit it resolved to, and the generated +code asks for that commit. A client that is a path in your workspace records the +path, so a jar built from it only works inside that workspace; a git client is +the form that travels. > [!WARNING] > The client set is the *whole* dependency set. A module that recorded @@ -131,9 +221,7 @@ Adding one is refused and the workspace is left unchanged. > dagger module client add java > ``` > -> Then check that each one landed in `dagger.toml` before you generate. On the -> `sdk-ux-module-max` engine builds this SDK currently needs, -> `dagger module client add` reports success and writes nothing. +> Then check that each one landed in `dagger.toml` before you generate. ## Pre-1.0 modules diff --git a/hack/designs/2026-08-17-nullable-object-returns.md b/hack/designs/2026-08-17-nullable-object-returns.md index ef76e86..3fa3c7d 100644 --- a/hack/designs/2026-08-17-nullable-object-returns.md +++ b/hack/designs/2026-08-17-nullable-object-returns.md @@ -3,6 +3,12 @@ Status: proposed Date: 2026-08-17 +> One decision below was later reversed. This document rejects making the query +> transport public API because it would be permanent surface added for one test. +> `hack/designs/2026-09-13-unified-client-generation.md` makes it public for a +> different reason: generated code now lives outside `io.dagger.client` and has +> to be able to build a query. + ## Problem GraphQL fields that return a nullable object or interface (`field: Directory`, not diff --git a/hack/designs/2026-09-04-sdk-module-interface.md b/hack/designs/2026-09-04-sdk-module-interface.md index 2c2449f..775e709 100644 --- a/hack/designs/2026-09-04-sdk-module-interface.md +++ b/hack/designs/2026-09-04-sdk-module-interface.md @@ -3,6 +3,11 @@ Status: proposed Date: 2026-09-04 +> Superseded in part by `hack/designs/2026-09-13-unified-client-generation.md`, +> which is the separate design this one asked for. Standalone clients are no +> longer refused, and a client's types no longer land in the flat +> `io.dagger.client` package. + ## Reviewed baselines Every claim in this document was checked against these exact revisions. From 85cf17c276a0812e0ccdf5298c9ee6b4578c6830 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Mon, 14 Sep 2026 01:41:08 +0200 Subject: [PATCH 18/28] hack/designs: archive the unified-client-generation design Signed-off-by: Yves Brissaud --- .../2026-08-17-nullable-object-returns.md | 2 +- .../2026-09-04-sdk-module-interface.md | 2 +- .../2026-09-13-unified-client-generation.md | 39 ++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) rename hack/designs/{ => done}/2026-09-13-unified-client-generation.md (95%) diff --git a/hack/designs/2026-08-17-nullable-object-returns.md b/hack/designs/2026-08-17-nullable-object-returns.md index 3fa3c7d..eec23f5 100644 --- a/hack/designs/2026-08-17-nullable-object-returns.md +++ b/hack/designs/2026-08-17-nullable-object-returns.md @@ -5,7 +5,7 @@ Date: 2026-08-17 > One decision below was later reversed. This document rejects making the query > transport public API because it would be permanent surface added for one test. -> `hack/designs/2026-09-13-unified-client-generation.md` makes it public for a +> `hack/designs/done/2026-09-13-unified-client-generation.md` makes it public for a > different reason: generated code now lives outside `io.dagger.client` and has > to be able to build a query. diff --git a/hack/designs/2026-09-04-sdk-module-interface.md b/hack/designs/2026-09-04-sdk-module-interface.md index 775e709..496b414 100644 --- a/hack/designs/2026-09-04-sdk-module-interface.md +++ b/hack/designs/2026-09-04-sdk-module-interface.md @@ -3,7 +3,7 @@ Status: proposed Date: 2026-09-04 -> Superseded in part by `hack/designs/2026-09-13-unified-client-generation.md`, +> Superseded in part by `hack/designs/done/2026-09-13-unified-client-generation.md`, > which is the separate design this one asked for. Standalone clients are no > longer refused, and a client's types no longer land in the flat > `io.dagger.client` package. diff --git a/hack/designs/2026-09-13-unified-client-generation.md b/hack/designs/done/2026-09-13-unified-client-generation.md similarity index 95% rename from hack/designs/2026-09-13-unified-client-generation.md rename to hack/designs/done/2026-09-13-unified-client-generation.md index 4be175d..f6b30f3 100644 --- a/hack/designs/2026-09-13-unified-client-generation.md +++ b/hack/designs/done/2026-09-13-unified-client-generation.md @@ -1,6 +1,6 @@ # Unified client generation -Status: proposed +Status: implemented Date: 2026-09-13 ## Terms @@ -643,6 +643,26 @@ migration, and the README documents the move. **Size.** This changes the code generator, the runtime library, the generation driver and the test suite together. See **On shipping this as one change**. +## Left for later + +**`ClientPom` infers the indentation of the block it inserts** from the file +around it, which is roughly sixty lines more than the problem needs. It is well +covered by tests. Emitting the profile at a fixed indentation would delete that +machinery, at the cost of a block indented differently from its surroundings, +which Maven does not care about. + +**The packager copies whole directories out of a shared Maven cache volume.** +`codegenPluginRepo` in `.dagger/modules/packager/main.dang` exports the +committed plugin repository with `cp -r` of three names, so anything another +run left beside them under `io/dagger` is swept into the committed tree. That is +what makes `packager:generate` sensitive to the history of a cache volume that +outlives any one job. Copying only the files it publishes would make it immune. + +**The generator still has a single-schema path.** `-Ddaggerengine.schema=` +is exactly a plan with only a core entry, so the branch in `DaggerCodegenMojo` +and the schema walk it uses could both go once the packager writes a one-entry +plan instead. + ## Generation, end to end ```mermaid @@ -870,3 +890,20 @@ rather than eager and per session. `templates` module, and editing the output without the source leaves `templates:generate` reporting unapplied changes. Both patches that touch `main.dang` now carry the matching template edit. +- **Phases 6 to 8, done.** Draft pull request `dagger/java-sdk#23`, opened on + head `c62af4d7f45007bfbef194a8d3575310620e6945` with base + `24f430a529a5aa07b0d3ca64417d8f460394f004`. Every check green. + + One check, `packager:generate`, was red for the first two runs and was not + this change: it rebuilds the committed codegen plugin jar out of a Maven cache + volume that persists across jobs, and something already in that volume was + swept into the comparison. Three measurements settled it. A rebuild against a + never-used cache volume reproduced the committed bytes exactly. A rebuild + against a volume deliberately primed by a real module generation reproduced + them too, which ruled out the first suspected mechanism. And the same check + was green on `24f430a5` itself, so the environment does reproduce a correctly + committed jar. A cache-busting re-run then passed in 25.3s. + + The remaining hardening is recorded above under residual work: the packager + copies whole directories out of that shared volume rather than the files it + publishes, which is what makes it sensitive to the volume's history at all. From 1e50190ca0e43cc78af8304783cd8d1ae4fc5d69 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Mon, 14 Sep 2026 01:57:40 +0200 Subject: [PATCH 19/28] packager: hold the Maven lock across install and export The cache mount is LOCKED per exec, not across a chain of them, so with the install and the export in two execs the lock is released in between. Under one `dagger check` the unit-test check installs the same plugin concurrently, without the fixed output timestamp, and can overwrite the jar in the shared repository before the export copies it. What landed under prebuilt/ then carried wall-clock entry timestamps and never matched the committed bytes, so packager:generate failed on a race rather than on anything in the diff. The race is inferred rather than observed: the check is red and green on the same tree in CI, a rebuild against a never-used cache volume reproduces the committed bytes exactly, and a rebuild against one primed by a real module generation does too. What is left is a concurrent writer, and the unit-test check is the only one there is. Signed-off-by: Yves Brissaud --- .dagger/modules/packager/main.dang | 20 ++- .../2026-09-13-unified-client-generation.md | 128 +++++++++++++----- 2 files changed, 107 insertions(+), 41 deletions(-) diff --git a/.dagger/modules/packager/main.dang b/.dagger/modules/packager/main.dang index 2cfb15c..709bef0 100644 --- a/.dagger/modules/packager/main.dang +++ b/.dagger/modules/packager/main.dang @@ -44,20 +44,28 @@ type Packager { .withMountedCache("/root/.m2", mavenRepo, sharing: CacheSharingMode.LOCKED) .withDirectory("/dagger-io", sdkSource(ws)) .withWorkdir("/dagger-io") - .withExec(["mvn", "--projects", "dagger-codegen-maven-plugin", "--also-make", "install", "-T1C", "-Dmaven.test.skip=true", "-Dfmt.skip=true", "-Dproject.build.outputTimestamp=2024-01-01T00:00:00Z", "--no-transfer-progress"]) - # Export the two artifacts this module publishes, named one by one rather - # than copying io/dagger wholesale. The local repository is shared with - # module generation, which installs io/dagger/dagger-java-sdk/ and + # Install and export in one exec, because the mount is LOCKED per exec and + # not across a chain of them. Split in two, the lock is released in + # between, and packager:unit-tests — which runs concurrently under one + # `dagger check` and installs the same plugin without the fixed output + # timestamp — can overwrite the jar in the shared repository before the + # export copies it. What then landed under prebuilt/ carried wall-clock + # entry timestamps and never matched the committed bytes, so this check + # failed on a race rather than on anything in the diff. + # + # The export names the two artifacts this module publishes rather than + # copying io/dagger wholesale. The local repository is shared with module + # generation, which installs io/dagger/dagger-java-sdk/ and # io/dagger/dagger-java-annotation-processor/ into it under a # per-module version; a wholesale copy sweeps those in too, so what landed # under prebuilt/ depended on whether a generation had run first. # - # Then strip Maven's install-time timestamps so the committed repo is + # It then strips Maven's install-time timestamps so the committed repo is # byte-reproducible: drop the comment lines from _remote.repositories # (keeping the ">=" local-install markers resolution needs) and pin # . Otherwise every generate re-timestamps these files and # the check reports perpetual drift. - .withExec(["sh", "-c", "set -e; rm -rf /out; mkdir -p /out/io/dagger; cd /root/.m2/repository/io/dagger; cp -r dagger-codegen-maven-plugin dagger-sdk-parent maven-metadata-local.xml /out/io/dagger/; find /out -name _remote.repositories -exec sed -i '/>=/!d' {} ';'; find /out -name maven-metadata-local.xml -exec sed -i 's|[0-9]*|20240101000000|' {} ';'"]) + .withExec(["sh", "-c", "set -e; mvn --projects dagger-codegen-maven-plugin --also-make install -T1C -Dmaven.test.skip=true -Dfmt.skip=true -Dproject.build.outputTimestamp=2024-01-01T00:00:00Z --no-transfer-progress; rm -rf /out; mkdir -p /out/io/dagger; cd /root/.m2/repository/io/dagger; cp -r dagger-codegen-maven-plugin dagger-sdk-parent maven-metadata-local.xml /out/io/dagger/; find /out -name _remote.repositories -exec sed -i '/>=/!d' {} ';'; find /out -name maven-metadata-local.xml -exec sed -i 's|[0-9]*|20240101000000|' {} ';'"]) .directory("/out") } diff --git a/hack/designs/done/2026-09-13-unified-client-generation.md b/hack/designs/done/2026-09-13-unified-client-generation.md index f6b30f3..86dea50 100644 --- a/hack/designs/done/2026-09-13-unified-client-generation.md +++ b/hack/designs/done/2026-09-13-unified-client-generation.md @@ -653,10 +653,11 @@ which Maven does not care about. **The packager copies whole directories out of a shared Maven cache volume.** `codegenPluginRepo` in `.dagger/modules/packager/main.dang` exports the -committed plugin repository with `cp -r` of three names, so anything another -run left beside them under `io/dagger` is swept into the committed tree. That is -what makes `packager:generate` sensitive to the history of a cache volume that -outlives any one job. Copying only the files it publishes would make it immune. +committed plugin repository with `cp -r` of three names, so anything another run +left beside them under `io/dagger` is swept into the committed tree. Copying +only the files it publishes would make it immune to the volume's history. The +race that made the same export non-deterministic under one `dagger check` is +fixed here; this narrower sensitivity is not. **The generator still has a single-schema path.** `-Ddaggerengine.schema=` is exactly a plan with only a core entry, so the branch in `DaggerCodegenMojo` @@ -730,9 +731,9 @@ reviewer who wants the halves separately can take the cut at patch 12 as given. ## The patch series Built with Stacked Git on `24f430a529a5aa07b0d3ca64417d8f460394f004`. Every -patch carries `Signed-off-by: Yves Brissaud `. Patches 1 to 12 +patch carries `Signed-off-by: Yves Brissaud `. Patches 1 to 13 are the code generator and the runtime library, and build and test with `mvn` -alone; 13 onwards are the generation driver, the standalone scope, the checks +alone; 14 onwards are the generation driver, the standalone scope, the checks and the documentation. 1. **`hack/designs: spec unified client generation`** — this document. @@ -744,58 +745,73 @@ and the documentation. `Field` learn which module the engine attributes a type or field to. The argument arrives JSON-encoded, so the quotation marks are part of the value. 4. **`codegen: partition a schema into core and one module`** — `SchemaPartition`. - Core keeps every unowned type with all its fields; a client keeps only the - types its module owns. An empty client partition is refused, which is also - how a target with no runtime SDK is caught. -5. **`codegen: map a module name to a Java package, and refuse a set it cannot + Core keeps every unowned type with every module-owned field stripped; a + client keeps the types its module owns plus the fields that module + contributes to core types. An empty client partition is refused, which is + also how a target with no runtime SDK is caught. +5. **`codegen: read a client's entry points off its schema`** — + `ClientEntryPoint`. A contributed field has no class of its own, so it + becomes a static method on the module's root type, carrying the core receiver + it was reached through. The root type is read off the schema rather than + derived from the module name, which would give `E2e` where the engine says + `E2E`. +6. **`codegen: map a module name to a Java package, and refuse a set it cannot separate`** — `ModulePackage`. The comparison is case-insensitive, because a case-sensitive filesystem is not the only kind these packages are written to. -6. **`codegen: resolve type references through a registry`** — `TypeRegistry`, +7. **`codegen: resolve type references through a registry`** — `TypeRegistry`, threaded through every visitor and `CodeWriter`. Behaviour-preserving, and measured: generating from a real `v1.0.0-beta.13` schema before and after differs in exactly one way across 114 files, `executeQuery(java.lang.String.class)` becoming `executeQuery(String.class)`, because a `ClassName` lets javapoet elide the implicit `java.lang` import. -7. **`sdk: make the query transport public API`** — generated code outside +8. **`sdk: make the query transport public API`** — generated code outside `io.dagger.client` has to be able to build a query. -8. **`sdk: serve a target on first use`** — `ModuleTarget` and +9. **`sdk: serve a target on first use`** — `ModuleTarget` and `ModuleTargets.serve`, which takes the descriptor its caller holds rather than looking one up. -9. **`sdk: open a session when there is none`** — `CLISession`, the `Connection` - fallback, `--load-workspace-modules` wired through, and a `Dagger.dag()` that - two threads cannot race into starting two engines. -10. **`codegen: generate every package a plan names in one pass`** — +10. **`sdk: open a session when there is none`** — `CLISession`, the `Connection` + fallback, `--load-workspace-modules` wired through, and a `Dagger.dag()` that + two threads cannot race into starting two engines. +11. **`codegen: generate every package a plan names in one pass`** — `GenerationPlan`, `Generator`, the `-Ddagger.plan` parameter, and the descriptor a plan entry carries emitted as a constant its entry points serve. Generated constructors become public here: package-private was correct only while everything was one package. -11. **`codegen: merge core from the targets when a scope has none`** — +12. **`codegen: take core from the targets when a scope has none`** — `SchemaMerge`, and the refusal when targets disagree. -12. **`codegen: add a client-pom goal to register generated clients`** — the +13. **`codegen: add a client-pom goal to register generated clients`** — the goal that writes one marked profile into a pom the SDK does not own. It splices text rather than re-serializing, so nothing else in the file moves. -13. **`prebuilt: rebuild the codegen plugin`** — before the first patch that +14. **`prebuilt: rebuild the codegen plugin`** — before the first patch that generates with it. Generation seeds the local Maven repository from `prebuilt/m2` whenever it exists and never compiles the plugin sources in that case, so a driver change without this would run the old generator. -14. **`java-sdk: generate one package per target`** — `codegen.dang`, and +15. **`java-sdk: generate one package per target`** — `codegen.dang`, and `mod.dang` driving it. The generated layout changes here, and the checks that cover the move land with it. -15. **`java-sdk: generate standalone client scopes`** — `client.dang`, the - routing in `main.dang`, the descriptors, the pom registration, and the two - checks that matter most: that a standalone scope generates, and that the - package it generates has the same digest as the module scope's. -16. **`README: document standalone clients`**. - -Three things differ from what this document first planned, and the reasons are +16. **`java-sdk: generate standalone client scopes`** — `client.dang`, the + routing in `main.dang`, the pom registration, and the two checks that matter + most: that a standalone scope generates, and that the package it generates + has the same digest as the module scope's. +17. **`README: document standalone clients`**. +18. **`hack/designs: archive the unified-client-generation design`** — this + document moves under `done/` once the series is complete. +19. **`packager: hold the Maven lock across install and export`** — the `LOCKED` + cache mount is per-exec, so installing the plugin and exporting it as two + execs let a concurrent check overwrite the jar in between. One exec closes + it. + +Four things differ from what this document first planned, and the reasons are worth keeping. The formatter patch was not planned; it was added because the drift made every other patch noisy. `codegen.dang` and the per-target layout landed as one patch rather than two, because the intermediate — a driver refactor that changes no output — does not exist once the plan format itself is -what changes. And the plan's last patch, installing the `e2e` module in -`dagger.toml` so its checks run against the released engine, was dropped: the -finding behind it is real and recorded below, but acting on it changes what CI -runs, which is not this feature's business. +what changes. Patch 5 was not planned at all: it exists because the accessor was +moved off core, which this document originally proposed to keep. And the plan's +last patch, installing the `e2e` module in `dagger.toml` so its checks run +against the released engine, was dropped: the finding behind it is real and +recorded below, but acting on it changes what CI runs, which is not this +feature's business. ## What was taken from the abandoned attempt, and what was not @@ -904,6 +920,48 @@ rather than eager and per session. was green on `24f430a5` itself, so the environment does reproduce a correctly committed jar. A cache-busting re-run then passed in 25.3s. - The remaining hardening is recorded above under residual work: the packager - copies whole directories out of that shared volume rather than the files it - publishes, which is what makes it sensitive to the volume's history at all. + A further push, of documentation alone, then failed the same check again — on + a tree whose code had not changed and where that check had just passed. That + ruled out the diff entirely and pointed at a race, which the module's own code + shows: the cache mount is LOCKED per exec and not across a chain of them, and + the export ran in a second exec after the install released the lock. Under one + `dagger check` the unit-test check installs the same plugin concurrently and + without the fixed output timestamp, so it can overwrite the jar between the + two. The last patch in this series puts the install and the export in one + exec. The narrower sensitivity — copying whole directories rather than the + published files — is recorded above as work left for later. + +- **A client is autonomous, done.** A refinement after the pull request opened: + a client should be reached by importing its own package, not by an accessor + added to the global `dag()`, and a client should ask the engine to serve its + module — inside a module too, so that no `[[dependencies]]` entry is what + makes it work. + + The first half changed the partition. Core now has every module-owned field + stripped rather than kept, and each such field is re-homed onto the target's + root type as a static method taking the receiver it was reached through. Core + no longer names a client package at all, which is what makes core itself + independent of the target set — something this document had explicitly + conceded it would not be. + + The second half deleted a layer rather than adding one. The `ServiceLoader` + machinery — `ModuleTargetProvider`, a `META-INF/services` file, a name-keyed + registry, and the "two providers disagree about one target" failure it needed + to defend against — existed only because the entry point lived in core, and + core could not name a client package, so it had to find a descriptor by name + at run time. With the entry point inside the client package, that package owns + its target and holds the descriptor as a constant. Removing it also removed a + migration problem: a services file needs a resource directory declared in the + module's pom, and this SDK does not rewrite the pom of a module that already + exists. + + Whether a target is served became a per-target decision carried in the plan. + A per-scope one was tried first and was wrong for a reason the checks made + plain: it would have put a descriptor in a standalone package and none in the + module package for the same target, so `clientsAreOneArtifactCheck` — the + check that states this whole design's premise — could not have held. Per + target, a git target is served by the client in both scopes and its package is + byte-identical on both sides today. A workspace path is still served by the + engine inside a module, because a module runtime has no filesystem session + attachable to resolve one against. That is the last asymmetry, it is an engine + limitation, and closing it is `servesWorkspacePaths: true` in `modulePlan`. From 4e979b0d12911b4266989af5e6c28527213d5d6e Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:13:23 +0200 Subject: [PATCH 20/28] codegen: name the runtime package apart from the generated core The registry resolves two different things through one name: a schema type, which is about to move, and a hand-written runtime class, which is not. Hold them apart before either moves, so the move is a change of one constant. No generated output changes: both packages are io.dagger.client. Signed-off-by: Yves Brissaud --- .../java/io/dagger/codegen/Generator.java | 8 ++++-- .../codegen/introspection/TypeRegistry.java | 26 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java index bd6c726..a0b32b7 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java @@ -20,9 +20,12 @@ /** Emits every package a {@link GenerationPlan} names, in one pass over one type registry. */ public final class Generator { - /** The package core and the hand-written runtime share. */ + /** The package the generated core API goes into. */ public static final String CORE_PACKAGE = "io.dagger.client"; + /** The package the hand-written runtime lives in, which no generation writes to. */ + public static final String RUNTIME_PACKAGE = "io.dagger.client"; + private final Path outputDirectory; private final Charset encoding; private final String engineVersion; @@ -67,7 +70,8 @@ public void generate(GenerationPlan plan) throws IOException { (module, schema) -> SchemaMerge.requireCoreCovers(module, coreSchema, schema)); } - TypeRegistry registry = TypeRegistry.acrossPackages(CORE_PACKAGE, packageByTypeName); + TypeRegistry registry = + TypeRegistry.acrossPackages(CORE_PACKAGE, RUNTIME_PACKAGE, packageByTypeName); emit(SchemaPartition.core(coreSchema), registry.emittingInto(CORE_PACKAGE), null, null); for (GenerationPlan.Target target : plan.targets()) { String pkg = packages.get(target.module()); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java index 68891db..010e8a3 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java @@ -10,39 +10,49 @@ * QueryBuilder}, {@code Arguments}, ...), and itself. Every visitor used to name them by simple * name, which is only correct while everything lands in one package. Routing them through a * registry is the seam a second package needs. + * + *

Core and the runtime are two packages rather than one, because only core is generated: the + * runtime is hand-written and stays where it is however the generated packages are laid out. */ public final class TypeRegistry { private final String targetPackage; private final String corePackage; + private final String runtimePackage; private final Map packageByTypeName; private TypeRegistry( - String targetPackage, String corePackage, Map packageByTypeName) { + String targetPackage, + String corePackage, + String runtimePackage, + Map packageByTypeName) { this.targetPackage = targetPackage; this.corePackage = corePackage; + this.runtimePackage = runtimePackage; this.packageByTypeName = packageByTypeName; } /** Everything in one package. */ public static TypeRegistry singlePackage(String pkg) { - return new TypeRegistry(pkg, pkg, Map.of()); + return new TypeRegistry(pkg, pkg, pkg, Map.of()); } /** - * Core in one package, and every type a module owns in that module's own package. + * Core in one package, the hand-written runtime in another, and every type a module owns in that + * module's own package. * *

Built once for a whole plan, so a module's package can name a core type and core can name a * module's type without either knowing where the other landed. */ public static TypeRegistry acrossPackages( - String corePackage, Map packageByTypeName) { - return new TypeRegistry(corePackage, corePackage, Map.copyOf(packageByTypeName)); + String corePackage, String runtimePackage, Map packageByTypeName) { + return new TypeRegistry( + corePackage, corePackage, runtimePackage, Map.copyOf(packageByTypeName)); } /** The same resolution, writing into a different package. */ public TypeRegistry emittingInto(String pkg) { - return new TypeRegistry(pkg, corePackage, packageByTypeName); + return new TypeRegistry(pkg, corePackage, runtimePackage, packageByTypeName); } /** The package this registry emits into. */ @@ -80,11 +90,11 @@ public ClassName forInterfaceClient(String graphqlName) { /** A hand-written runtime class. */ public ClassName runtime(String simpleName) { - return ClassName.get(corePackage, simpleName); + return ClassName.get(runtimePackage, simpleName); } /** A hand-written runtime class in a subpackage of the runtime. */ public ClassName runtime(String subpackage, String simpleName) { - return ClassName.get(corePackage + "." + subpackage, simpleName); + return ClassName.get(runtimePackage + "." + subpackage, simpleName); } } From 6802910eb2f5e7a44dfce6edeb9b1a034641d40d Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:14:42 +0200 Subject: [PATCH 21/28] java-sdk: generate core as a client package Core was the one generated thing that was privileged: it landed flat in io.dagger.client beside the hand-written runtime, and it was reached as dag().container() while every other client is reached through a static method on its own root type. Two shapes for one idea. Core now goes to io.dagger.client.modules.core, under the same root as every other client, and is reached the same way: core(dag()).container(), or core() over the ambient session. What makes that possible is that Dagger.dag() stops returning a generated type. Session is hand-written, owns the connection and exposes the query builder every generated package chains from, and is what each entry point now takes. Nothing generated is privileged after this: core(dag()) and myModule(dag()) are the same shape, and io.dagger.client holds hand-written code alone. dag().container() no longer exists. Signed-off-by: Yves Brissaud --- .../io/dagger/codegen/DaggerCodegenMojo.java | 7 +- .../java/io/dagger/codegen/Generator.java | 12 +- .../java/io/dagger/codegen/ModulePackage.java | 3 + .../introspection/ClientEntryPoint.java | 183 +++++++++++------- .../dagger/codegen/introspection/Helpers.java | 2 +- .../introspection/InterfaceVisitor.java | 6 +- .../codegen/introspection/ObjectVisitor.java | 89 +++++---- .../java/io/dagger/codegen/GeneratorTest.java | 85 +++++--- .../introspection/ClientEntryPointTest.java | 6 +- .../DaggerModuleAnnotationProcessor.java | 64 +++--- .../annotation/processor/DaggerType.java | 18 +- .../annotation/processor/DaggerTypeTest.java | 15 +- .../io/dagger/client/AutoCloseableClient.java | 13 -- .../dagger/client/AutoCloseableSession.java | 9 + .../main/java/io/dagger/client/Dagger.java | 28 +-- .../main/java/io/dagger/client/Session.java | 30 +++ .../dagger/client/engineconn/CLISession.java | 2 +- .../io/dagger/client/telemetry/Telemetry.java | 6 +- .../io/dagger/module/annotation/Generate.java | 4 +- 19 files changed, 359 insertions(+), 223 deletions(-) delete mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableSession.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/client/Session.java diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java index 346e11d..5dc42b0 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCodegenMojo.java @@ -1,5 +1,6 @@ package io.dagger.codegen; +import io.dagger.codegen.introspection.ClientEntryPoint; import io.dagger.codegen.introspection.CodegenVisitor; import io.dagger.codegen.introspection.Schema; import io.dagger.codegen.introspection.SchemaVisitor; @@ -9,6 +10,7 @@ import java.nio.charset.Charset; import java.nio.file.Path; import java.util.List; +import java.util.Map; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -84,8 +86,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { SchemaVisitor codegen = new CodegenVisitor( schema, - TypeRegistry.singlePackage("io.dagger.client"), - null, + TypeRegistry.acrossPackages( + Generator.CORE_PACKAGE, Generator.RUNTIME_PACKAGE, Map.of()), + ClientEntryPoint.core(), null, dest, Charset.forName(outputEncoding)); diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java index a0b32b7..38d57cf 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java @@ -20,8 +20,8 @@ /** Emits every package a {@link GenerationPlan} names, in one pass over one type registry. */ public final class Generator { - /** The package the generated core API goes into. */ - public static final String CORE_PACKAGE = "io.dagger.client"; + /** The package the generated core API goes into, beside every other client package. */ + public static final String CORE_PACKAGE = ModulePackage.ROOT + "." + ModulePackage.CORE_SEGMENT; /** The package the hand-written runtime lives in, which no generation writes to. */ public static final String RUNTIME_PACKAGE = "io.dagger.client"; @@ -52,7 +52,7 @@ public void generate(GenerationPlan plan) throws IOException { schemas.put(target.module(), schema); String pkg = packages.get(target.module()); SchemaPartition partition = SchemaPartition.client(schema, target.module()); - entryPoints.put(target.module(), new ClientEntryPoint(partition)); + entryPoints.put(target.module(), ClientEntryPoint.module(partition)); for (String owned : partition.typeNames()) { requireUnclaimed(targetByTypeName, owned, target.module()); packageByTypeName.put(owned, pkg); @@ -72,7 +72,11 @@ public void generate(GenerationPlan plan) throws IOException { TypeRegistry registry = TypeRegistry.acrossPackages(CORE_PACKAGE, RUNTIME_PACKAGE, packageByTypeName); - emit(SchemaPartition.core(coreSchema), registry.emittingInto(CORE_PACKAGE), null, null); + emit( + SchemaPartition.core(coreSchema), + registry.emittingInto(CORE_PACKAGE), + ClientEntryPoint.core(), + null); for (GenerationPlan.Target target : plan.targets()) { String pkg = packages.get(target.module()); emit( diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java index aee7620..fea77ee 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java @@ -20,6 +20,9 @@ public final class ModulePackage { /** The package every module's bindings go under. */ public static final String ROOT = "io.dagger.client.modules"; + /** The segment the generated core API takes, which is why no module may take it. */ + public static final String CORE_SEGMENT = "core"; + private ModulePackage() {} /** diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java index 26991c4..d06e2d3 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java @@ -5,86 +5,139 @@ import java.util.Map; /** - * The way into a client package: the fields a module contributes to core types, and the class they - * are emitted on. + * The way into a generated package: the class a caller enters through, and the name they write. * - *

A contributed field has no class of its own — Java generates a type once, in one package, and - * {@code Query} and {@code Binding} belong to core. Each one becomes a static method on the - * module's root type, named after the field, taking the core receiver it was reached through as its - * first argument. {@code Query}'s receiver is the session, so it stays implicit. - * - *

The root type is read off the schema, as the return type of the one {@code Query} field the - * module owns. Deriving it from the module name instead would give {@code E2e} where the engine - * says {@code E2E}. + *

Two kinds, because a package is entered two ways. {@link Module} is entered through the one + * {@code Query} field the module owns, which has to be selected and may take arguments. {@link + * Core} is entered from the session itself: core owns most of {@code Query} and no one module owns + * it, so there is no field to single out and nothing to serve. */ -public final class ClientEntryPoint { +public sealed interface ClientEntryPoint { - private static final String QUERY = "Query"; + /** The GraphQL name of the type the entry points are emitted on. */ + String rootTypeName(); - private final SchemaPartition client; - private final Field entryField; + /** The name a caller writes to enter the package. */ + String entryName(); - public ClientEntryPoint(SchemaPartition client) { - if (client.module() == null) { - throw new IllegalArgumentException("an entry point needs a client partition, not core"); - } - this.client = client; - this.entryField = requireOneQueryField(client); - String root = entryField.getTypeRef().getTypeName(); - if (!client.typeNames().contains(root)) { - throw new IllegalArgumentException( - String.format( - "module %s is reached as the core type %s, which it does not own, so there is no" - + " class to put its entry points on: a module named after a core type collides" - + " with it. Rename the module, or alias the target.", - client.module(), root)); - } + /** Core, reached from the session itself. */ + static Core core() { + return new Core(); } - /** The module this enters. */ - public String module() { - return client.module(); + /** A module, reached through the {@code Query} field it owns. */ + static Module module(SchemaPartition client) { + return new Module(client); } - /** The {@code Query} field the module owns: how a caller constructs its root. */ - public Field entryField() { - return entryField; - } + /** + * Core. + * + *

A record with no components: what core is entered as is decided by core being core, not by + * anything in a schema. The session is the receiver, so the entry selects nothing and the package + * carries no descriptor — core is already there. + */ + record Core() implements ClientEntryPoint { + + private static final String QUERY = "Query"; - /** The GraphQL name of the root type the entry points are emitted on. */ - public String rootTypeName() { - return entryField.getTypeRef().getTypeName(); + @Override + public String rootTypeName() { + return QUERY; + } + + @Override + public String entryName() { + return "core"; + } } /** - * The module's fields on core types other than {@code Query}, by type name, in the partition's - * order so the emitted entry points come out the same on every run. + * A module: the fields it contributes to core types, and the class they are emitted on. + * + *

A contributed field has no class of its own — Java generates a type once, in one package, + * and {@code Query} and {@code Binding} belong to core. Each one becomes a static method on the + * module's root type, named after the field, taking the core receiver it was reached through as + * its first argument. {@code Query}'s receiver is the session, so it stays implicit. + * + *

The root type is read off the schema, as the return type of the one {@code Query} field the + * module owns. Deriving it from the module name instead would give {@code E2e} where the engine + * says {@code E2E}. */ - public Map> shims() { - Map> shims = new LinkedHashMap<>(); - client - .extensions() - .forEach( - (typeName, fields) -> { - if (!QUERY.equals(typeName)) { - shims.put(typeName, fields); - } - }); - return shims; - } + final class Module implements ClientEntryPoint { + + private static final String QUERY = "Query"; + + private final SchemaPartition client; + private final Field entryField; + + Module(SchemaPartition client) { + if (client.module() == null) { + throw new IllegalArgumentException("an entry point needs a client partition, not core"); + } + this.client = client; + this.entryField = requireOneQueryField(client); + String root = entryField.getTypeRef().getTypeName(); + if (!client.typeNames().contains(root)) { + throw new IllegalArgumentException( + String.format( + "module %s is reached as the core type %s, which it does not own, so there is no" + + " class to put its entry points on: a module named after a core type collides" + + " with it. Rename the module, or alias the target.", + client.module(), root)); + } + } + + /** The module this enters. */ + public String module() { + return client.module(); + } + + /** The {@code Query} field the module owns: how a caller constructs its root. */ + public Field entryField() { + return entryField; + } + + @Override + public String rootTypeName() { + return entryField.getTypeRef().getTypeName(); + } + + @Override + public String entryName() { + return Helpers.formatName(entryField); + } + + /** + * The module's fields on core types other than {@code Query}, by type name, in the partition's + * order so the emitted entry points come out the same on every run. + */ + public Map> shims() { + Map> shims = new LinkedHashMap<>(); + client + .extensions() + .forEach( + (typeName, fields) -> { + if (!QUERY.equals(typeName)) { + shims.put(typeName, fields); + } + }); + return shims; + } - private static Field requireOneQueryField(SchemaPartition client) { - List fields = client.extensions().getOrDefault(QUERY, List.of()); - if (fields.size() != 1) { - throw new IllegalArgumentException( - String.format( - "module %s owns %d fields on Query, expected exactly one; it owns the types %s and" - + " contributes the Query fields %s", - client.module(), - fields.size(), - client.typeNames(), - fields.stream().map(Field::getName).toList())); + private static Field requireOneQueryField(SchemaPartition client) { + List fields = client.extensions().getOrDefault(QUERY, List.of()); + if (fields.size() != 1) { + throw new IllegalArgumentException( + String.format( + "module %s owns %d fields on Query, expected exactly one; it owns the types %s and" + + " contributes the Query fields %s", + client.module(), + fields.size(), + client.typeNames(), + fields.stream().map(Field::getName).toList())); + } + return fields.get(0); } - return fields.get(0); } } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java index 9e65f79..b1157a5 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java @@ -131,7 +131,7 @@ static String formatName(Type type) { /** The Java simple name generated for a GraphQL type name. */ static String formatName(String graphqlName) { if ("Query".equals(graphqlName)) { - return "Client"; + return "Core"; } else { return capitalize(graphqlName); } diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java index af2796d..e9271a5 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/InterfaceVisitor.java @@ -161,7 +161,8 @@ private void buildFieldMethod( // Build the query if (field.hasArgs()) { - fieldMethodBuilder.addStatement("Arguments.Builder builder = Arguments.newBuilder()"); + fieldMethodBuilder.addStatement( + "$1T.Builder builder = $1T.newBuilder()", registry().runtime("Arguments")); } field .getRequiredArgs() @@ -170,7 +171,8 @@ private void buildFieldMethod( fieldMethodBuilder.addStatement( "builder.add($1S, $2L)", arg.getName(), Helpers.formatName(arg))); if (field.hasArgs()) { - fieldMethodBuilder.addStatement("Arguments fieldArgs = builder.build()"); + fieldMethodBuilder.addStatement( + "$T fieldArgs = builder.build()", registry().runtime("Arguments")); } if (field.hasArgs()) { fieldMethodBuilder.addStatement( diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java index 19a1b20..5c5a479 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java @@ -22,6 +22,9 @@ class ObjectVisitor extends AbstractVisitor { /** The constant each entry point serves before it selects anything. */ private static final String TARGET = "TARGET"; + /** What the two forms of the core entry say, which no schema field describes. */ + private static final String CORE_JAVADOC = "The core API.\n"; + private final ClientEntryPoint entryPoint; private final ModuleTargetRef source; @@ -75,26 +78,6 @@ TypeSpec generateType(Type type) { } if ("Query".equals(type.getName())) { - MethodSpec constructor = - MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addParameter(registry().runtime("engineconn", "Connection"), "connection") - .addStatement("this.connection = connection") - .addStatement("this.queryBuilder = new QueryBuilder(connection.getGraphQLClient())") - .build(); - classBuilder.addMethod(constructor); - classBuilder.addField( - FieldSpec.builder( - registry().runtime("engineconn", "Connection"), "connection", Modifier.PRIVATE) - .build()); - MethodSpec closeMethod = - MethodSpec.methodBuilder("close") - .addException(Exception.class) - .addModifiers(Modifier.PUBLIC) - .addStatement("this.connection.close()") - .build(); - classBuilder.addMethod(closeMethod); - // loadObjectFromID: load any object by its ID using node(id:) + inline fragment classBuilder.addMethod( MethodSpec.methodBuilder("loadObjectFromID") @@ -165,7 +148,7 @@ TypeSpec generateType(Type type) { .addStatement( "$T id = ctx.deserialize($T.class, parser)", String.class, String.class) .addStatement( - "$T o = new $T($T.dag().nodeQueryBuilder($S, new $T(id)))", + "$T o = new $T($T.dag().queryBuilder().chainNode($S, new $T(id)))", thisType, thisType, registry().runtime("Dagger"), @@ -231,22 +214,53 @@ TypeSpec generateType(Type type) { return classBuilder.build(); } + /** The way into this generated package, which core and a module reach differently. */ + private void buildEntryPoints(TypeSpec.Builder classBuilder, Type type) { + if (entryPoint instanceof ClientEntryPoint.Module module) { + buildModuleEntryPoints(classBuilder, type, module); + } else { + buildCoreEntryPoint(classBuilder); + } + } + /** - * The way into this client package: the module's {@code Query} field, and every field it - * contributes to another core type. + * Core: the session is the receiver and there is no field to single out, so the entry wraps the + * session's own builder rather than chaining anything onto it. Nothing is served either — core is + * what a session already answers. */ - private void buildEntryPoints(TypeSpec.Builder classBuilder, Type type) { + private void buildCoreEntryPoint(TypeSpec.Builder classBuilder) { + ClassName core = registry().forType("Query"); + MethodSpec entry = + MethodSpec.methodBuilder(entryPoint.entryName()) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(core) + .addParameter( + ParameterSpec.builder(registry().runtime("Session"), "dag") + .addJavadoc("the session to reach core in\n") + .build()) + .addJavadoc(CORE_JAVADOC) + .addStatement("return new $T(dag.queryBuilder())", core) + .build(); + classBuilder.addMethod(entry); + classBuilder.addMethod(ambient(entry, CORE_JAVADOC)); + } + + /** + * A module: the {@code Query} field it owns, and every field it contributes to another core type. + */ + private void buildModuleEntryPoints( + TypeSpec.Builder classBuilder, Type type, ClientEntryPoint.Module module) { if (source != null) { - classBuilder.addField(targetConstant()); + classBuilder.addField(targetConstant(module)); } - Entry onQuery = new Entry(entryPoint.module(), null, null); - buildEntry(classBuilder, entryPoint.entryField(), type, onQuery); - entryPoint + Entry onQuery = new Entry(module.module(), null, null); + buildEntry(classBuilder, module.entryField(), type, onQuery); + module .shims() .forEach( (typeName, fields) -> { ClassName receiverType = registry().forType(typeName); - Entry shim = new Entry(entryPoint.module(), receiverType, uncapitalize(typeName)); + Entry shim = new Entry(module.module(), receiverType, uncapitalize(typeName)); fields.forEach(field -> buildEntry(classBuilder, field, type, shim)); }); } @@ -261,12 +275,12 @@ private void buildEntry(TypeSpec.Builder classBuilder, Field field, Type type, E buildFieldArgumentsHelpers(classBuilder, field, type, entry); MethodSpec withOptArgs = buildFieldMethod(classBuilder, field, true, entry); if (entry.onQuery()) { - classBuilder.addMethod(ambient(withOptArgs, field)); + classBuilder.addMethod(ambient(withOptArgs, Helpers.escapeJavadoc(field.getDescription()))); } } MethodSpec method = buildFieldMethod(classBuilder, field, false, entry); if (entry.onQuery()) { - classBuilder.addMethod(ambient(method, field)); + classBuilder.addMethod(ambient(method, Helpers.escapeJavadoc(field.getDescription()))); } } @@ -275,16 +289,15 @@ private void buildEntry(TypeSpec.Builder classBuilder, Field field, Type type, E * one loads its own module; one generated against a module the engine serves already carries * none, and the entry points below ask for nothing. */ - private FieldSpec targetConstant() { + private FieldSpec targetConstant(ClientEntryPoint.Module module) { ClassName target = registry().runtime("ModuleTarget"); CodeBlock initializer; if (source instanceof ModuleTargetRef.InWorkspace workspace) { initializer = - CodeBlock.of("$T.inWorkspace($S, $S)", target, entryPoint.module(), workspace.path()); + CodeBlock.of("$T.inWorkspace($S, $S)", target, module.module(), workspace.path()); } else if (source instanceof ModuleTargetRef.AtGitRef git) { initializer = - CodeBlock.of( - "$T.atGitRef($S, $S, $S)", target, entryPoint.module(), git.ref(), git.pin()); + CodeBlock.of("$T.atGitRef($S, $S, $S)", target, module.module(), git.ref(), git.pin()); } else { throw new IllegalStateException("no way to reach the module target " + source); } @@ -296,7 +309,7 @@ private FieldSpec targetConstant() { /** * The same entry over the ambient session, so a caller that never named one still has a way in. */ - private MethodSpec ambient(MethodSpec entry, Field field) { + private MethodSpec ambient(MethodSpec entry, String javadoc) { List withoutSession = entry.parameters().subList(1, entry.parameters().size()); CodeBlock.Builder call = CodeBlock.builder().add("return $L($T.dag()", entry.name(), registry().runtime("Dagger")); @@ -308,7 +321,7 @@ private MethodSpec ambient(MethodSpec entry, Field field) { .returns(entry.returnType()) .addParameters(withoutSession) .addExceptions(entry.exceptions()) - .addJavadoc(Helpers.escapeJavadoc(field.getDescription())) + .addJavadoc(javadoc) .addJavadoc("\n@see $T#dag()\n", registry().runtime("Dagger")) .addStatement(call.build()) .build(); @@ -344,7 +357,7 @@ private MethodSpec buildFieldMethod( fieldMethodBuilder.addModifiers(Modifier.STATIC); if (entry.onQuery()) { fieldMethodBuilder.addParameter( - ParameterSpec.builder(registry().forType("Query"), "dag") + ParameterSpec.builder(registry().runtime("Session"), "dag") .addJavadoc("the session to reach the target in\n") .build()); } else { diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java index 850779b..5dc5f22 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java @@ -26,12 +26,12 @@ void everyPackageThePlanNamesIsEmittedOnce() throws Exception { assertThat(emitted()) .contains( - "io/dagger/client/Client.java", - "io/dagger/client/Container.java", + CORE + "Core.java", + CORE + "Container.java", "io/dagger/client/modules/alpha/Alpha.java", "io/dagger/client/modules/alpha/AlphaReport.java", "io/dagger/client/modules/beta/Beta.java"); - assertThat(emitted()).doesNotContain("io/dagger/client/Alpha.java"); + assertThat(emitted()).doesNotContain(CORE + "Alpha.java"); } /** A target is reached from its own package, so no core source names one. */ @@ -42,14 +42,38 @@ void coreNamesNoClientPackage() throws Exception { generate(); for (String source : emitted()) { - if (source.startsWith("io/dagger/client/modules/")) { + if (!source.startsWith(CORE)) { continue; } - assertThat(read(source)) + // Its own package taken out, so what is left is any client package core named. + assertThat(read(source).replace(CORE_PACKAGE, "")) .as("core source %s", source) .doesNotContain("io.dagger.client.modules"); } - assertThat(read("io/dagger/client/Client.java")).doesNotContain("Alpha").doesNotContain("Beta"); + assertThat(read(CORE + "Core.java")).doesNotContain("Alpha").doesNotContain("Beta"); + } + + /** + * Core is entered the way a target is: a static method on its own root type, over a session named + * or ambient. What differs is that the session is already core, so there is nothing to select and + * nothing to serve. + */ + @Test + void coreIsEnteredFromItsOwnPackage() throws Exception { + writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); + + generate(); + + String core = read(CORE + "Core.java"); + assertThat(core) + .contains("package io.dagger.client.modules.core;") + .contains("import io.dagger.client.Session;") + .contains("public static Core core(Session dag)") + .contains("return new Core(dag.queryBuilder())") + .contains("public static Core core()") + .contains("return core(Dagger.dag())"); + assertThat(core).doesNotContain("ModuleTarget").doesNotContain("Connection"); + assertThat(emitted()).doesNotContain("io/dagger/client/Client.java"); } /** The way into a target is a static method on its root type, taking the session it runs in. */ @@ -60,12 +84,12 @@ void aTargetIsEnteredFromItsOwnPackage() throws Exception { generate(); assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) - .contains("import io.dagger.client.Client;") - .contains("public static Alpha alpha(Client dag, String source)") + .contains("import io.dagger.client.Session;") + .contains("public static Alpha alpha(Session dag, String source)") .contains("public static Alpha alpha(String source)") .contains("return alpha(Dagger.dag(), source)"); assertThat(read("io/dagger/client/modules/beta/Beta.java")) - .contains("public static Beta beta(Client dag)") + .contains("public static Beta beta(Session dag)") .contains("public static Beta beta()"); } @@ -78,9 +102,9 @@ void theArgumentsHolderOfAnEntryPointIsNestedInTheRootType() throws Exception { assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) .contains("public static class AlphaArguments") - .contains("public static Alpha alpha(Client dag, String source, AlphaArguments optArgs)") + .contains("public static Alpha alpha(Session dag, String source, AlphaArguments optArgs)") .contains("public static Alpha alpha(String source, AlphaArguments optArgs)"); - assertThat(read("io/dagger/client/Client.java")).doesNotContain("AlphaArguments"); + assertThat(read(CORE + "Core.java")).doesNotContain("AlphaArguments"); } /** A field a module contributes to another core type moves with it, receiver and all. */ @@ -91,10 +115,10 @@ void aTargetsContributionToACoreTypeIsEnteredFromItsOwnPackage() throws Exceptio generate(); assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) - .contains("import io.dagger.client.Binding;") + .contains("import io.dagger.client.modules.core.Binding;") .contains("public static Alpha asAlpha(Binding binding)") .contains("binding.queryBuilder().chain(\"asAlpha\")"); - assertThat(read("io/dagger/client/Binding.java")).doesNotContain("asAlpha"); + assertThat(read(CORE + "Binding.java")).doesNotContain("asAlpha"); assertThat(emitted()).doesNotContain("io/dagger/client/modules/alpha/Binding.java"); } @@ -121,7 +145,7 @@ public static Alpha asAlpha(Binding binding) { } """ .stripTrailing()); - assertThat(alpha).doesNotContain("asAlpha(Client dag"); + assertThat(alpha).doesNotContain("asAlpha(Session dag"); } @Test @@ -131,7 +155,7 @@ void aTargetsPackageReachesCoreTypesInTheCorePackage() throws Exception { generate(); assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) - .contains("import io.dagger.client.Container;"); + .contains("import io.dagger.client.modules.core.Container;"); } /** @@ -154,7 +178,7 @@ void anEntryPointServesItsTargetFirst() throws Exception { "private static final ModuleTarget TARGET = ModuleTarget.atGitRef(\"beta\"," + " \"github.com/dagger/beta@v1\", \"0123abc\");") .contains("ModuleTargets.serve(dag.queryBuilder(), TARGET);"); - assertThat(read("io/dagger/client/Client.java")).doesNotContain("ModuleTargets"); + assertThat(read(CORE + "Core.java")).doesNotContain("ModuleTargets"); } /** @@ -171,7 +195,7 @@ void aTargetTheEngineServesCarriesNoDescriptorAndAsksForNothing() throws Excepti String alpha = read("io/dagger/client/modules/alpha/Alpha.java"); assertThat(alpha).doesNotContain("ModuleTarget").doesNotContain("TARGET"); - assertThat(alpha).contains("public static Alpha alpha(Client dag, String source)"); + assertThat(alpha).contains("public static Alpha alpha(Session dag, String source)"); assertThat(shimOf(alpha, "asAlpha")) .isEqualTo( """ @@ -189,7 +213,7 @@ void aCoreTypeIsNotServed() throws Exception { generate(); - String container = read("io/dagger/client/Container.java"); + String container = read(CORE + "Container.java"); assertThat(container).doesNotContain("ModuleTargets"); } @@ -197,7 +221,7 @@ void aCoreTypeIsNotServed() throws Exception { void theSamePlanGeneratesTheSameBytesWhateverOrderItsEntriesAreLaidOutIn() throws Exception { writePlan(CORE_WITH_TWO_TARGETS, target("alpha", ALPHA), target("beta", BETA)); generate(); - String first = read("io/dagger/client/Client.java"); + String first = read(CORE + "Core.java"); Path reversed = Files.createTempDirectory("plan-reversed"); writePlanAt(reversed, CORE_WITH_TWO_TARGETS, target("beta", BETA), target("alpha", ALPHA)); @@ -205,8 +229,7 @@ void theSamePlanGeneratesTheSameBytesWhateverOrderItsEntriesAreLaidOutIn() throw new Generator(secondOut, StandardCharsets.UTF_8, VERSION) .generate(GenerationPlan.read(reversed)); - assertThat(Files.readString(secondOut.resolve("io/dagger/client/Client.java"))) - .isEqualTo(first); + assertThat(Files.readString(secondOut.resolve(CORE + "Core.java"))).isEqualTo(first); } @Test @@ -229,11 +252,11 @@ void withNoCoreSchemaCoreIsTakenFromTheTargets() throws Exception { generate(); assertThat(read("io/dagger/client/modules/alpha/Alpha.java")) - .contains("public static Alpha alpha(Client dag, String source)"); + .contains("public static Alpha alpha(Session dag, String source)"); assertThat(read("io/dagger/client/modules/beta/Beta.java")) - .contains("public static Beta beta(Client dag)"); - assertThat(read("io/dagger/client/Client.java")).doesNotContain("Alpha").doesNotContain("Beta"); - assertThat(read("io/dagger/client/Container.java")).isNotEmpty(); + .contains("public static Beta beta(Session dag)"); + assertThat(read(CORE + "Core.java")).doesNotContain("Alpha").doesNotContain("Beta"); + assertThat(read(CORE + "Container.java")).isNotEmpty(); assertThat(emitted()) .contains( "io/dagger/client/modules/alpha/Alpha.java", "io/dagger/client/modules/beta/Beta.java"); @@ -243,7 +266,7 @@ void withNoCoreSchemaCoreIsTakenFromTheTargets() throws Exception { void mergingCoreDoesNotDependOnTheOrderTheTargetsAreRead() throws Exception { writePlanWithoutCore(target("alpha", ALPHA), target("beta", BETA)); generate(); - String first = read("io/dagger/client/Client.java"); + String first = read(CORE + "Core.java"); Path reversed = Files.createTempDirectory("plan-reversed-merge"); writePlanAt(reversed, null, target("beta", BETA), target("alpha", ALPHA)); @@ -251,8 +274,7 @@ void mergingCoreDoesNotDependOnTheOrderTheTargetsAreRead() throws Exception { new Generator(secondOut, StandardCharsets.UTF_8, VERSION) .generate(GenerationPlan.read(reversed)); - assertThat(Files.readString(secondOut.resolve("io/dagger/client/Client.java"))) - .isEqualTo(first); + assertThat(Files.readString(secondOut.resolve(CORE + "Core.java"))).isEqualTo(first); } /** @@ -310,7 +332,7 @@ void aTargetWhoseCoreIsWiderThanTheModuleScopesIsAccepted() throws Exception { generate(); assertThat(emitted()).contains("io/dagger/client/modules/alpha/Alpha.java"); - assertThat(emitted()).doesNotContain("io/dagger/client/Host.java"); + assertThat(emitted()).doesNotContain(CORE + "Host.java"); } /** @@ -341,6 +363,11 @@ void aTargetOwningATypeNameCoreAlsoHasIsRefused() throws Exception { .hasMessageContaining("Container"); } + /** Where core lands: a package under the modules root, like every other client. */ + private static final String CORE_PACKAGE = "io.dagger.client.modules.core"; + + private static final String CORE = CORE_PACKAGE.replace('.', '/') + "/"; + private void generate() throws IOException { new Generator(out, StandardCharsets.UTF_8, VERSION).generate(GenerationPlan.read(plan)); } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java index af2ff97..e62309c 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ClientEntryPointTest.java @@ -12,7 +12,7 @@ class ClientEntryPointTest { /** The engine camel-cases a module's name to namespace its types, and {@code e2e} becomes E2E. */ @Test void theRootTypeIsTheReturnTypeOfTheQueryFieldNotTheModuleName() throws Exception { - ClientEntryPoint entry = entryPoint(E2E, "e2e"); + ClientEntryPoint.Module entry = entryPoint(E2E, "e2e"); assertThat(entry.rootTypeName()).isEqualTo("E2E"); assertThat(entry.entryField().getName()).isEqualTo("e2e"); @@ -48,11 +48,11 @@ void aModuleReachedAsACoreTypeItDoesNotOwnIsRefused() throws Exception { .hasMessageContaining("Rename the module"); } - private static ClientEntryPoint entryPoint(String json, String module) throws Exception { + private static ClientEntryPoint.Module entryPoint(String json, String module) throws Exception { Schema schema = Schema.initialize( new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.13"); - return new ClientEntryPoint(SchemaPartition.client(schema, module)); + return ClientEntryPoint.module(SchemaPartition.client(schema, module)); } private static String owned(String module) { diff --git a/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java b/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java index 83fd5fe..a6f91ec 100644 --- a/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java +++ b/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerModuleAnnotationProcessor.java @@ -12,14 +12,15 @@ import com.palantir.javapoet.ParameterizedTypeName; import com.palantir.javapoet.TypeSpec; import io.dagger.client.Dagger; -import io.dagger.client.FunctionCall; -import io.dagger.client.FunctionCallArgValue; -import io.dagger.client.ID; -import io.dagger.client.JSON; -import io.dagger.client.JsonConverter; -import io.dagger.client.TypeDef; import io.dagger.client.exception.DaggerExecException; import io.dagger.client.exception.DaggerQueryException; +import io.dagger.client.modules.core.Core; +import io.dagger.client.modules.core.FunctionCall; +import io.dagger.client.modules.core.FunctionCallArgValue; +import io.dagger.client.modules.core.ID; +import io.dagger.client.modules.core.JSON; +import io.dagger.client.modules.core.JsonConverter; +import io.dagger.client.modules.core.TypeDef; import io.dagger.client.telemetry.Telemetry; import io.dagger.module.annotation.Check; import io.dagger.module.annotation.Default; @@ -286,7 +287,7 @@ ModuleInfo generateModuleInfo(Set annotations, RoundEnvir private List parseParameters(ExecutableElement elt) { return elt.getParameters().stream() - .filter(param -> !param.asType().toString().equals("io.dagger.client.Client")) + .filter(param -> !param.asType().toString().equals("io.dagger.client.modules.core.Core")) .map( param -> { TypeMirror tm = param.asType(); @@ -311,10 +312,10 @@ private List parseParameters(ExecutableElement elt) { if (hasDefaultPathAnnotation && !Set.of( - "io.dagger.client.Directory", - "io.dagger.client.File", - "io.dagger.client.GitRepository", - "io.dagger.client.GitRef") + "io.dagger.client.modules.core.Directory", + "io.dagger.client.modules.core.File", + "io.dagger.client.modules.core.GitRepository", + "io.dagger.client.modules.core.GitRef") .contains(tm.toString())) { throw new IllegalArgumentException( "Parameter " @@ -338,7 +339,8 @@ private List parseParameters(ExecutableElement elt) { Ignore ignoreAnnotation = param.getAnnotation(Ignore.class); var hasIgnoreAnnotation = ignoreAnnotation != null; - if (hasIgnoreAnnotation && !tm.toString().equals("io.dagger.client.Directory")) { + if (hasIgnoreAnnotation + && !tm.toString().equals("io.dagger.client.modules.core.Directory")) { throw new IllegalArgumentException( "Parameter " + param.getSimpleName() @@ -383,20 +385,22 @@ static JavaFile generate(ModuleInfo moduleInfo) { .addException(DaggerQueryException.class) .addException(InterruptedException.class) .addCode( - "$T module = $T.dag().module()", io.dagger.client.Module.class, Dagger.class); + "$T module = $T.core().module()", + io.dagger.client.modules.core.Module.class, + Core.class); if (isNotBlank(moduleInfo.description())) { rm.addCode("\n .withDescription($S)", moduleInfo.description()); } for (var objectInfo : moduleInfo.objects()) { rm.addCode("\n .withObject(") - .addCode("\n $T.dag().typeDef().withObject($S", Dagger.class, objectInfo.name()); + .addCode("\n $T.core().typeDef().withObject($S", Core.class, objectInfo.name()); if (isNotBlank(objectInfo.description())) { rm.addCode( ", new $T.WithObjectArguments().withDescription($S)", TypeDef.class, objectInfo.description()); } - rm.addCode(")"); // end of dag().TypeDef().withObject( + rm.addCode(")"); // end of core().TypeDef().withObject( for (var fnInfo : objectInfo.functions()) { rm.addCode("\n .withFunction(") .addCode(withFunction(moduleInfo.enumInfos().keySet(), objectInfo, fnInfo)) @@ -407,7 +411,7 @@ static JavaFile generate(ModuleInfo moduleInfo) { .addCode("$S, ", fieldInfo.name()) .addCode(DaggerType.of(fieldInfo.type()).toDaggerTypeDef()); if (isNotBlank(fieldInfo.description())) { - rm.addCode(", new $T.WithFieldArguments()", io.dagger.client.TypeDef.class) + rm.addCode(", new $T.WithFieldArguments()", TypeDef.class) .addCode(".withDescription($S)", fieldInfo.description()); } rm.addCode(")"); @@ -423,20 +427,20 @@ static JavaFile generate(ModuleInfo moduleInfo) { } for (var enumInfo : moduleInfo.enumInfos().values()) { rm.addCode("\n .withEnum(") - .addCode("\n $T.dag().typeDef().withEnum($S", Dagger.class, enumInfo.name()); + .addCode("\n $T.core().typeDef().withEnum($S", Core.class, enumInfo.name()); if (isNotBlank(enumInfo.description())) { rm.addCode( ", new $T.WithEnumArguments().withDescription($S)", TypeDef.class, enumInfo.description()); } - rm.addCode(")"); // end of dag().TypeDef().withEnum( + rm.addCode(")"); // end of core().TypeDef().withEnum( for (var enumValue : enumInfo.values()) { rm.addCode("\n .withEnumValue($S", enumValue.value()); if (isNotBlank(enumValue.description())) { rm.addCode( ", new $T.WithEnumValueArguments().withDescription($S)", - io.dagger.client.TypeDef.class, + TypeDef.class, enumValue.description()); } rm.addCode(")"); // end of .withEnumValue( @@ -522,8 +526,8 @@ static JavaFile generate(ModuleInfo moduleInfo) { .beginControlFlow( "try ($T telemetry = new $T())", Telemetry.class, Telemetry.class) .addStatement( - "new Entrypoint().dispatch($T.dag().currentFunctionCall())", - Dagger.class) + "new Entrypoint().dispatch($T.core().currentFunctionCall())", + Core.class) .nextControlFlow("finally") .addStatement("$T.dag().close()", Dagger.class) .endControlFlow() @@ -564,18 +568,18 @@ static JavaFile generate(ModuleInfo moduleInfo) { .addStatement("return null") .nextControlFlow("catch ($T e)", InvocationTargetException.class) .addStatement( - "fnCall.returnError($T.dag().error(e.getTargetException().getMessage()))", - Dagger.class) + "fnCall.returnError($T.core().error(e.getTargetException().getMessage()))", + Core.class) .addStatement("throw e") .nextControlFlow("catch ($T e)", DaggerExecException.class) .addStatement( - "fnCall.returnError($T.dag().error(e.getMessage())" + "fnCall.returnError($T.core().error(e.getMessage())" + ".withValue(\"stdout\", $T.toJSON(e.getStdOut()))" + ".withValue(\"stderr\", $T.toJSON(e.getStdErr()))" + ".withValue(\"cmd\", $T.toJSON(e.getCmd()))" + ".withValue(\"exitCode\", $T.toJSON(e.getExitCode()))" + ".withValue(\"path\", $T.toJSON(e.getPath())))", - Dagger.class, + Core.class, JsonConverter.class, JsonConverter.class, JsonConverter.class, @@ -584,8 +588,7 @@ static JavaFile generate(ModuleInfo moduleInfo) { .addStatement("throw e") .nextControlFlow("catch ($T e)", Exception.class) .addStatement( - "fnCall.returnError($T.dag().error(e.getMessage()))", - Dagger.class) + "fnCall.returnError($T.core().error(e.getMessage()))", Core.class) .addStatement("throw e") .endControlFlow() .build()) @@ -594,6 +597,7 @@ static JavaFile generate(ModuleInfo moduleInfo) { .build()) .addFileComment("This class has been generated by dagger-java-sdk. DO NOT EDIT.") .indent(" ") + .addStaticImport(Core.class, "core") .addStaticImport(Dagger.class, "dag") .build(); @@ -708,8 +712,8 @@ public static CodeBlock withFunction( CodeBlock.Builder code = CodeBlock.builder() .add( - "\n $T.dag().function($S,", - Dagger.class, + "\n $T.core().function($S,", + Core.class, isConstructor ? "" : fnInfo.name()) .add("\n ") .add( @@ -740,7 +744,7 @@ public static CodeBlock withFunction( boolean hasDefaultPath = parameterInfo.defaultPath().isPresent(); boolean hasIgnore = parameterInfo.ignore().isPresent(); if (hasDescription || hasDefaultValue || hasDefaultPath || hasIgnore) { - code.add(", new $T.WithArgArguments()", io.dagger.client.Function.class); + code.add(", new $T.WithArgArguments()", io.dagger.client.modules.core.Function.class); if (hasDescription) { code.add(".withDescription($S)", parameterInfo.description()); } diff --git a/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerType.java b/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerType.java index 95ae000..7eb045f 100644 --- a/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerType.java +++ b/sdk/dagger-java-annotation-processor/src/main/java/io/dagger/annotation/processor/DaggerType.java @@ -3,8 +3,8 @@ import com.palantir.javapoet.ClassName; import com.palantir.javapoet.CodeBlock; import com.palantir.javapoet.ParameterizedTypeName; -import io.dagger.client.Dagger; -import io.dagger.client.TypeDefKind; +import io.dagger.client.modules.core.Core; +import io.dagger.client.modules.core.TypeDefKind; import io.dagger.module.info.TypeInfo; import java.util.Set; import javax.lang.model.type.TypeKind; @@ -126,7 +126,7 @@ public Enum(String qualifiedName, String simpleName) { @Override CodeBlock toDaggerTypeDef() { - return CodeBlock.of("$T.dag().typeDef().withEnum($S)", Dagger.class, simpleName); + return CodeBlock.of("$T.core().typeDef().withEnum($S)", Core.class, simpleName); } @Override @@ -155,8 +155,8 @@ CodeBlock toDaggerTypeDef() { CodeBlock.Builder cb = CodeBlock.builder() .add( - "$T.dag().typeDef().withKind($T.$L)", - Dagger.class, + "$T.core().typeDef().withKind($T.$L)", + Core.class, TypeDefKind.class, "%s_KIND".formatted(name.toUpperCase())); if (isOptional) { @@ -202,7 +202,7 @@ public Scalar(String qualifiedName, String simpleName) { @Override CodeBlock toDaggerTypeDef() { - return CodeBlock.of("$T.dag().typeDef().withScalar($S)", Dagger.class, simpleName); + return CodeBlock.of("$T.core().typeDef().withScalar($S)", Core.class, simpleName); } @Override @@ -245,7 +245,7 @@ public Object(String qualifiedName, String simpleName) { @Override CodeBlock toDaggerTypeDef() { - return CodeBlock.of("$T.dag().typeDef().withObject($S)", Dagger.class, simpleName); + return CodeBlock.of("$T.core().typeDef().withObject($S)", Core.class, simpleName); } @Override @@ -265,7 +265,7 @@ public List(String innerName) { CodeBlock toDaggerTypeDef() { CodeBlock.Builder cb = CodeBlock.builder() - .add("$T.dag().typeDef().withListOf(", Dagger.class) + .add("$T.core().typeDef().withListOf(", Core.class) .add(of(innerName).toDaggerTypeDef()) .add(")"); return cb.build(); @@ -301,7 +301,7 @@ public Array(String innerName) { CodeBlock toDaggerTypeDef() { CodeBlock.Builder cb = CodeBlock.builder() - .add("$T.dag().typeDef().withListOf(", Dagger.class) + .add("$T.core().typeDef().withListOf(", Core.class) .add(of(innerName).toDaggerTypeDef()) .add(")"); return cb.build(); diff --git a/sdk/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/DaggerTypeTest.java b/sdk/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/DaggerTypeTest.java index 2911725..784b4af 100644 --- a/sdk/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/DaggerTypeTest.java +++ b/sdk/dagger-java-annotation-processor/src/test/java/io/dagger/annotation/processor/DaggerTypeTest.java @@ -11,22 +11,22 @@ class DaggerTypeTest { @Test void optionalObjectReturnsAreRegisteredAsOptionalAndUnwrappedForSerialization() { - DaggerType type = declared("java.util.Optional"); + DaggerType type = declared("java.util.Optional"); assertThat(type.toDaggerTypeDef().toString()) .isEqualTo( - "io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); + "io.dagger.client.modules.core.Core.core().typeDef().withObject(\"Container\").withOptional(true)"); assertThat(type.toJavaType().toString()) - .isEqualTo("java.util.Optional"); + .isEqualTo("java.util.Optional"); assertThat(type.valueForSerialization("result").toString()).isEqualTo("result.orElse(null)"); } @Test void nonOptionalReturnsSerializeAsThemselves() { - DaggerType type = declared("io.dagger.client.Container"); + DaggerType type = declared("io.dagger.client.modules.core.Container"); assertThat(type.toDaggerTypeDef().toString()) - .isEqualTo("io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\")"); + .isEqualTo("io.dagger.client.modules.core.Core.core().typeDef().withObject(\"Container\")"); assertThat(type.valueForSerialization("result").toString()).isEqualTo("result"); } @@ -41,11 +41,12 @@ void optionalObjectFieldsAreRegisteredAsOptional() { "maybeContainer", "", new TypeInfo( - "java.util.Optional", TypeKind.DECLARED.name())); + "java.util.Optional", + TypeKind.DECLARED.name())); assertThat(DaggerType.of(field.type()).toDaggerTypeDef().toString()) .isEqualTo( - "io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); + "io.dagger.client.modules.core.Core.core().typeDef().withObject(\"Container\").withOptional(true)"); } private static DaggerType declared(String typeName) { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java deleted file mode 100644 index 823f0e1..0000000 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.dagger.client; - -import io.dagger.client.engineconn.Connection; - -public class AutoCloseableClient extends Client implements AutoCloseable { - AutoCloseableClient(Connection connection) { - super(connection); - } - - AutoCloseableClient(QueryBuilder queryBuilder) { - super(queryBuilder); - } -} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableSession.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableSession.java new file mode 100644 index 0000000..982a68b --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableSession.java @@ -0,0 +1,9 @@ +package io.dagger.client; + +import io.dagger.client.engineconn.Connection; + +public class AutoCloseableSession extends Session implements AutoCloseable { + AutoCloseableSession(Connection connection) { + super(connection); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java index 5975117..3d7e514 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java @@ -4,21 +4,21 @@ import java.io.IOException; public class Dagger { - private static Client dag = null; + private static Session dag = null; /** - * Returns the global Dagger client instance. + * Returns the global Dagger session. * *

Contrary to {@code connect}, this is managed as a singleton. It will always return the same * instance. Synchronized because the first call may start an engine session, and two threads * racing it would start two. * - * @return Global Dagger client + * @return Global Dagger session */ - public static synchronized Client dag() { + public static synchronized Session dag() { if (dag == null) { try { - dag = new Client(Connection.get(System.getProperty("user.dir"))); + dag = new Session(Connection.get(System.getProperty("user.dir"))); } catch (IOException e) { throw new RuntimeException("Could not connect to Dagger engine", e); } @@ -29,10 +29,10 @@ public static synchronized Client dag() { /** * Opens connection with a Dagger engine. * - * @return The Dagger API entrypoint + * @return The Dagger session * @throws IOException */ - public static AutoCloseableClient connect() throws IOException { + public static AutoCloseableSession connect() throws IOException { return connect(System.getProperty("user.dir"), false); } @@ -40,10 +40,10 @@ public static AutoCloseableClient connect() throws IOException { * Opens connection with a Dagger engine. * * @param loadWorkspaceModules whether to opt into loading workspace modules - * @return The Dagger API entrypoint + * @return The Dagger session * @throws IOException */ - public static AutoCloseableClient connect(boolean loadWorkspaceModules) throws IOException { + public static AutoCloseableSession connect(boolean loadWorkspaceModules) throws IOException { return connect(System.getProperty("user.dir"), loadWorkspaceModules); } @@ -51,10 +51,10 @@ public static AutoCloseableClient connect(boolean loadWorkspaceModules) throws I * Opens connection with a Dagger engine. * * @param workingDir the host working directory - * @return The Dagger API entrypoint + * @return The Dagger session * @throws IOException */ - public static AutoCloseableClient connect(String workingDir) throws IOException { + public static AutoCloseableSession connect(String workingDir) throws IOException { return connect(workingDir, false); } @@ -63,11 +63,11 @@ public static AutoCloseableClient connect(String workingDir) throws IOException * * @param workingDir the host working directory * @param loadWorkspaceModules whether to opt into loading workspace modules - * @return The Dagger API entrypoint + * @return The Dagger session * @throws IOException */ - public static AutoCloseableClient connect(String workingDir, boolean loadWorkspaceModules) + public static AutoCloseableSession connect(String workingDir, boolean loadWorkspaceModules) throws IOException { - return new AutoCloseableClient(Connection.get(workingDir, loadWorkspaceModules)); + return new AutoCloseableSession(Connection.get(workingDir, loadWorkspaceModules)); } } diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Session.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Session.java new file mode 100644 index 0000000..b17bd29 --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Session.java @@ -0,0 +1,30 @@ +package io.dagger.client; + +import io.dagger.client.engineconn.Connection; + +/** + * A connection to an engine, and the root every generated package chains its first selection from. + * + *

Hand-written, because nothing generated is privileged. Core is a client package like any + * other, reached as {@code core(dag())}, so what {@link Dagger#dag()} hands back cannot be a + * generated class: it is the one thing every package takes, core and module alike. + */ +public class Session { + + private final Connection connection; + private final QueryBuilder queryBuilder; + + public Session(Connection connection) { + this.connection = connection; + this.queryBuilder = new QueryBuilder(connection.getGraphQLClient()); + } + + /** The builder a generated entry point chains from, and the identity of this session. */ + public QueryBuilder queryBuilder() { + return this.queryBuilder; + } + + public void close() throws Exception { + this.connection.close(); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java index b3a265d..67a605e 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/CLISession.java @@ -1,6 +1,6 @@ package io.dagger.client.engineconn; -import io.dagger.client.Version; +import io.dagger.client.modules.core.Version; import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonReader; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/Telemetry.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/Telemetry.java index 7d2ac7c..9fdab0d 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/Telemetry.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/Telemetry.java @@ -1,8 +1,8 @@ package io.dagger.client.telemetry; -import io.dagger.client.FunctionCall; -import io.dagger.client.FunctionCallArgValue; -import io.dagger.client.JsonConverter; +import io.dagger.client.modules.core.FunctionCall; +import io.dagger.client.modules.core.FunctionCallArgValue; +import io.dagger.client.modules.core.JsonConverter; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/Generate.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/Generate.java index efee912..385df82 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/Generate.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/Generate.java @@ -20,9 +20,9 @@ * @Function * @Generate * public Changeset generateCode() { - * return dag().directory() + * return core().directory() * .withNewFile("generated.txt", "content") - * .changes(dag().directory()); + * .changes(core().directory()); * } * } * } From ec08a6e8567cc97589d213b3c6292d5e92f85549 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:22:38 +0200 Subject: [PATCH 22/28] codegen: refuse a module named core Core is generated into io.dagger.client.modules.core, so a module whose name normalizes to that segment would overwrite it, or be overwritten by it, depending on emission order. Refuse it where the segment is decided, and say which package is taken and why, rather than leaving the loser to be found by whatever fails to compile. Signed-off-by: Yves Brissaud --- .../main/java/io/dagger/codegen/ModulePackage.java | 11 +++++++++++ .../java/io/dagger/codegen/ModulePackageTest.java | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java index fea77ee..f25303d 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/ModulePackage.java @@ -14,6 +14,9 @@ * {@code sdk-helpers} becomes {@code sdkhelpers}. Distinct module names can normalize to the same * segment, which would make one module's bindings overwrite another's, so the mapping is computed * for a whole target set at once and refuses a set it cannot separate. + * + *

One segment is spoken for before any target asks: core is generated under this root too, so + * {@value #CORE_SEGMENT} is refused whatever the target set. */ public final class ModulePackage { @@ -69,6 +72,14 @@ public static String segmentFor(String moduleName) { throw new IllegalArgumentException( String.format("module %s normalizes to %s, which Java reserves", moduleName, candidate)); } + if (CORE_SEGMENT.equals(candidate)) { + throw new IllegalArgumentException( + String.format( + "module %s normalizes to %s, where the generated core API is emitted; core is a" + + " client package like any other and %s.%s is taken. Rename or alias the" + + " module.", + moduleName, candidate, ROOT, CORE_SEGMENT)); + } return candidate; } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java index 885e036..7666de2 100644 --- a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/ModulePackageTest.java @@ -55,6 +55,18 @@ void aNameWithNoUsableCharactersIsRefused() { .hasMessageContaining("leading ASCII letter"); } + /** Core is generated under this root too, so its segment is taken before any target asks. */ + @Test + void aModuleNamedCoreIsRefused() { + assertThatThrownBy(() -> ModulePackage.packagesFor(List.of("core"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("io.dagger.client.modules.core") + .hasMessageContaining("generated core API"); + assertThatThrownBy(() -> ModulePackage.segmentFor("Co-re")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("io.dagger.client.modules.core"); + } + @Test void aNameJavaReservesIsRefused() { assertThatThrownBy(() -> ModulePackage.segmentFor("package")) From 4b1f6e0c8a81f71c6f6a07c33df1f1a43d7ae360 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:22:51 +0200 Subject: [PATCH 23/28] templates: reach core through its own package A scaffolded module is the first Java anyone writing against this SDK reads, so it has to show the shape the SDK now has: core imported from its own package and entered by name, not reached through the session. Signed-off-by: Yves Brissaud --- .../modules/daggermoduleplaceholder/DaggerModule.java | 10 +++++----- .../modules/daggermoduleplaceholder/DaggerModule.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/default/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java b/templates/default/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java index d18b7ad..38f8b3b 100644 --- a/templates/default/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java +++ b/templates/default/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java @@ -1,11 +1,11 @@ package io.dagger.modules.daggermoduleplaceholder; -import static io.dagger.client.Dagger.dag; +import static io.dagger.client.modules.core.Core.core; -import io.dagger.client.Container; import io.dagger.client.exception.DaggerQueryException; -import io.dagger.client.Directory; -import io.dagger.client.Workspace; +import io.dagger.client.modules.core.Container; +import io.dagger.client.modules.core.Directory; +import io.dagger.client.modules.core.Workspace; import io.dagger.module.annotation.Default; import io.dagger.module.annotation.Function; import io.dagger.module.annotation.Object; @@ -27,7 +27,7 @@ public DaggerModule(Workspace ws, @Default("alpine:3.24") String baseImageAddres /** A container with the workspace source, ready to build. */ @Function public Container container() { - return dag() + return core() .container() .from(this.baseImageAddress) .withDirectory("/src", this.source) diff --git a/templates/legacy/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java b/templates/legacy/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java index 7853b96..aecd5bd 100644 --- a/templates/legacy/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java +++ b/templates/legacy/src/main/java/io/dagger/modules/daggermoduleplaceholder/DaggerModule.java @@ -1,10 +1,10 @@ package io.dagger.modules.daggermoduleplaceholder; -import static io.dagger.client.Dagger.dag; +import static io.dagger.client.modules.core.Core.core; -import io.dagger.client.Container; import io.dagger.client.exception.DaggerQueryException; -import io.dagger.client.Directory; +import io.dagger.client.modules.core.Container; +import io.dagger.client.modules.core.Directory; import io.dagger.module.annotation.Function; import io.dagger.module.annotation.Object; import java.util.List; @@ -16,14 +16,14 @@ public class DaggerModule { /** Returns a container that echoes whatever string argument is provided */ @Function public Container containerEcho(String stringArg) { - return dag().container().from("alpine:latest").withExec(List.of("echo", stringArg)); + return core().container().from("alpine:latest").withExec(List.of("echo", stringArg)); } /** Returns lines that match a pattern in the files of the provided Directory */ @Function public String grepDir(Directory directoryArg, String pattern) throws InterruptedException, ExecutionException, DaggerQueryException { - return dag() + return core() .container() .from("alpine:latest") .withMountedDirectory("/mnt", directoryArg) From f9dbe1adf98babf768c40e4ac46c0cd4a7a62a0d Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:23:33 +0200 Subject: [PATCH 24/28] e2e: assert core where it now lives Core is generated under modules/core and entered as core(), so the fixtures that compile against it and the assertions that read it move with it. Two new assertions: nothing is emitted flat beside the hand-written runtime any more, and the entrypoint the annotation processor writes enters core by name. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 70 +++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 89d0abc..26d62d2 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -63,6 +63,9 @@ type E2e { let vendoredBindings: String! = "sdk/src/main/java/io/dagger/client/Dagger.java" let entrypointPath: String! = "src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java" + # Core is a client package: it lands under the same modules root as every + # other one, and its root type is reached the same way. + let corePackage: String! = "modules/core" """ Fail the current check when a condition is false. @@ -172,6 +175,18 @@ type E2e { assertAdded(changes, newModulePath + "/src/main/java/io/dagger/modules/newapp/NewApp.java") assertAdded(changes, newModulePath + "/" + vendoredBindings) assertAdded(changes, newModulePath + "/" + entrypointPath) + # Core is generated as a client package, so nothing lands flat beside the + # hand-written runtime and there is no Client to reach through the session. + let generatedRoot = newModulePath + "/sdk/src/generated/java/io/dagger/client" + assertAdded(changes, generatedRoot + "/" + corePackage + "/Core.java") + # Asserted as an equality rather than as the absence of Client.java: a + # negative on a path that moved would pass for the wrong reason. + let generatedEntries = generated.directory("/" + generatedRoot).entries + assert( + generatedEntries.join(", ") == "modules/", + "the generated tree should hold client packages alone, not: " + + generatedEntries.join(", "), + ) assert( changes.modifiedPaths.length == 0, "initializing a scope modified existing files: " + changes.modifiedPaths.join(", "), @@ -294,19 +309,22 @@ type E2e { "no ClientDep.java under " + clientPackage + ", only: " + bindings.join(", "), ) assert( - contains(withClient.directory("/" + bindingsDir).entries, "ClientDep.java") == false, + contains( + withClient.directory("/" + bindingsDir + "/" + corePackage).entries, + "ClientDep.java", + ) == false, "a client's types belong to its own package, not to core", ) # The entry point is a static method on the client's own root type, so a # caller writes clientDep(...) after one import rather than reaching through - # a global dag(). Core is left untouched by the client. + # core. Core is left untouched by the client, and is entered the same way. assertContains( withClient.file("/" + clientPackage + "/ClientDep.java").contents, "public static ClientDep clientDep(", "the client package should carry its own entry point", ) assertNotContains( - withClient.file("/" + bindingsDir + "/Client.java").contents, + withClient.file("/" + bindingsDir + "/" + corePackage + "/Core.java").contents, "clientdep", "core should not name a client package", ) @@ -329,9 +347,17 @@ type E2e { withoutClient.directory("/" + bindingsDir).exists("modules/clientdep") == false, "removing the client should drop its package", ) + # Core stays: the modules tree is where every client package lives, core + # included, so removing the last target empties it down to core alone. + # A directory entry carries a trailing slash, a file does not. + let remaining = withoutClient.directory("/" + bindingsDir + "/modules").entries assert( - withoutClient.directory("/" + bindingsDir).exists("modules") == false, - "removing the last client should drop the modules tree", + contains(remaining, "core/"), + "removing the last client should leave core: " + remaining.join(", "), + ) + assert( + remaining.length == 1, + "removing the last client should leave core alone: " + remaining.join(", "), ) null @@ -379,8 +405,10 @@ type E2e { # Core names no client package: a target is reached through its own package # and its own entry point, never by extending the global client. assertNotContains( - generated.file("/" + standaloneSourceRoot + "/io/dagger/client/Client.java").contents, - "io.dagger.client.modules", + generated + .file("/" + standaloneSourceRoot + "/io/dagger/client/" + corePackage + "/Core.java") + .contents, + "io.dagger.client.modules.clientdep", "core should not name a client package", ) @@ -466,7 +494,11 @@ type E2e { "-f", "target/classes/io/dagger/client/modules/sdkhelpers/SdkHelpers.class", ]) - .withExec(["test", "-f", "target/classes/io/dagger/client/Client.class"]) + .withExec([ + "test", + "-f", + "target/classes/io/dagger/client/" + corePackage + "/Core.class", + ]) .sync null @@ -561,20 +593,23 @@ type E2e { # arguments holder is nested in the client's own root type, so it travels # with the method rather than staying behind on core. assertContainsAll(bindings, [ - "public static ClientDefaults clientDefaults(Client dag) {", + "public static ClientDefaults clientDefaults(Session dag) {", "public static ClientDefaults clientDefaults() {", - "public static ClientDefaults clientDefaults(Client dag, ClientDefaultsArguments optArgs) {", + "public static ClientDefaults clientDefaults(Session dag, ClientDefaultsArguments optArgs) {", "public static ClientDefaults clientDefaults(ClientDefaultsArguments optArgs) {", "public static class ClientDefaultsArguments", ]) assertNotContains( bindings, - "clientDefaults(Client dag, String name", + "clientDefaults(Session dag, String name", "an argument with a default should not be a required parameter", ) assertNotContains( generated - .file("/" + defaultsAppPath + "/sdk/src/generated/java/io/dagger/client/Client.java") + .file( + "/" + defaultsAppPath + "/sdk/src/generated/java/io/dagger/client/" + corePackage + + "/Core.java", + ) .contents, "clientDefaults", "core should not gain an accessor for a client", @@ -653,6 +688,11 @@ type E2e { "res.orElse(null)", "the Optional return should be unwrapped before serialization", ) + assertContains( + generated, + "import static io.dagger.client.modules.core.Core.core;", + "the entrypoint should enter core through its own package", + ) null } @@ -661,9 +701,9 @@ type E2e { let nullableReturnSource: String! { "package io.dagger.modules.newapp;\n" + "\n" - + "import static io.dagger.client.Dagger.dag;\n" + + "import static io.dagger.client.modules.core.Core.core;\n" + "\n" - + "import io.dagger.client.Directory;\n" + + "import io.dagger.client.modules.core.Directory;\n" + "import io.dagger.module.annotation.Function;\n" + "import io.dagger.module.annotation.Object;\n" + "import java.util.Optional;\n" @@ -675,7 +715,7 @@ type E2e { + " if (!found) {\n" + " return Optional.empty();\n" + " }\n" - + " return Optional.of(dag().directory().withNewFile(\"found\", \"\"));\n" + + " return Optional.of(core().directory().withNewFile(\"found\", \"\"));\n" + " }\n" + "}\n" } From 8cdc7223f5392b119c6fb0fbf692da4c639d1a01 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:24:25 +0200 Subject: [PATCH 25/28] engine-e2e: pin core's package in the initialized module The dev-SDK check is the only one that initializes a module on a real engine and then calls it, so it is where the layout can be pinned against the engine rather than against a fixture: core is generated under modules/core, and the module the template scaffolds runs. Signed-off-by: Yves Brissaud --- .dagger/modules/engine-e2e/main.dang | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.dagger/modules/engine-e2e/main.dang b/.dagger/modules/engine-e2e/main.dang index 6fefcb3..73750b9 100644 --- a/.dagger/modules/engine-e2e/main.dang +++ b/.dagger/modules/engine-e2e/main.dang @@ -49,9 +49,16 @@ type EngineE2e { .withExec(["test", "-f", modulePath + "/pom.xml"]) .withExec(["test", "-f", modulePath + "/src/main/java/io/dagger/modules/sdksmoke/SdkSmoke.java"]) .withExec(["test", "-f", modulePath + "/sdk/src/main/java/io/dagger/client/Dagger.java"]) + # Core is a client package: generated under the modules root like any + # other, and nothing is emitted flat beside the hand-written runtime. + .withExec(["test", "-f", modulePath + "/sdk/src/generated/java/io/dagger/client/modules/core/Core.java"]) + .withExec(["test", "!", "-e", modulePath + "/sdk/src/generated/java/io/dagger/client/Client.java"]) .withExec(["test", "-f", modulePath + "/src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java"]) .withExec(["grep", "-q", "github.com/dagger/java-sdk/runtime", modulePath + "/dagger-module.toml"]) + # The scaffolded module's one function is core().container(), so running it + # proves core is reachable through its own package at run time, not just + # that the sources compile. let release = initialized .withExec(["dagger", "-m", modulePath, "call", "container", "file", "--path", "/etc/alpine-release", "contents"]) .stdout From b15f994cb2ceacedf780182f73f2d2a9b9f19817 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:24:59 +0200 Subject: [PATCH 26/28] README: document core as a client package Signed-off-by: Yves Brissaud --- README.md | 39 +++++++++++++++++++++++++++++++++++---- sdk/README.md | 10 ++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8fe12e1..51320bc 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,8 @@ generates the SDK bindings in one step: src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java # generated entrypoint sdk/src/main/java/... # vendored SDK library sdk/src/processor/java/... # vendored annotation processor - sdk/src/generated/java/... # client bindings (from the engine schema) + sdk/src/generated/java/io/dagger/client/modules/core/... # the core API (from the engine schema) + sdk/src/generated/java/io/dagger/client/modules//... # one package per client ``` The SDK settings become typed flags on `dagger module init java` and are @@ -106,6 +107,31 @@ nearest one at or above your current directory, which is how project built with anything but Maven has no `pom.xml`, so this SDK reports no scope for it. +## Calling the core API + +Core is a client package like any other. Its types are generated into +`io.dagger.client.modules.core`, and the way in is a static method on its root +type: + +```java +import static io.dagger.client.modules.core.Core.core; +import io.dagger.client.modules.core.Container; + +Container base = core().container().from("alpine:3.24"); +``` + +`core()` uses the ambient session; `core(dag)` takes one you already hold. +`io.dagger.client` itself holds only hand-written code — `Dagger`, `Session`, +`QueryBuilder` and the rest of the runtime — so nothing generated is privileged. + +> [!WARNING] +> `dag().container()` no longer exists. `Dagger.dag()` returns a `Session`, not +> a generated client, and core is reached as `core()` after a static import of +> `io.dagger.client.modules.core.Core.core`. Every core type moves with it: +> `io.dagger.client.Container` becomes +> `io.dagger.client.modules.core.Container`. `Dagger.connect()` returns an +> `AutoCloseableSession` in place of `AutoCloseableClient`. + ## Module clients Module dependencies are replaced by generated module clients: @@ -125,10 +151,14 @@ import static io.dagger.client.modules.sdkhelpers.SdkHelpers.sdkHelpers; sdkHelpers().moduleManifest().generate(); ``` -The core client is not extended with an accessor for it. A client package is +Core is not extended with an accessor for it. A client package is self-contained: it reaches core types where they live, and nothing in core names it. Pass a session explicitly when you have one — `sdkHelpers(dag)` — or let the -no-argument form use the ambient one. +no-argument form use the ambient one. Core is entered the same way, which is the +whole of the difference between a client and core: none. + +A module named `core` is refused, because the generated core API has that +package. Alias the target to something else. In a module scope the client set becomes the module's dependency set. Each client is recorded in the manifest the module has — `dagger-module.toml`, or the @@ -160,7 +190,8 @@ and regenerated whole: ``` my-java-app/ pom.xml # gains one profile, see below - dagger/src/main/java/io/dagger/client/** # the SDK runtime and the core API + dagger/src/main/java/io/dagger/client/** # the hand-written SDK runtime + dagger/src/main/java/io/dagger/client/modules/core/** # the core API dagger/src/main/java/io/dagger/client/modules//** # one package per client ``` diff --git a/sdk/README.md b/sdk/README.md index 92a66d3..845e363 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -149,7 +149,9 @@ Here is a code snippet using the Dagger client ```java package io.dagger.sample; -import io.dagger.client.Client; +import static io.dagger.client.modules.core.Core.core; + +import io.dagger.client.AutoCloseableSession; import io.dagger.client.Dagger; import java.util.List; @@ -157,8 +159,8 @@ import java.util.List; public class GetDaggerWebsite { public static void main(String... args) throws Exception { - try (Client client = Dagger.connect()) { - String output = client + try (AutoCloseableSession session = Dagger.connect()) { + String output = core(session) .container() .from("alpine") .withExec(List.of("apk", "add", "curl")) @@ -195,7 +197,7 @@ A module function can return a nullable object the same way, by declaring ```java @Function public Optional maybeDirectory(boolean found) { - return found ? Optional.of(dag().directory()) : Optional.empty(); + return found ? Optional.of(core().directory()) : Optional.empty(); } ``` From 1bc5e071a0a924e37b02cc4be40f1aa2851fddce Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:25:41 +0200 Subject: [PATCH 27/28] hack/designs: record core as a client package The design rejected making core a target like any other, on the grounds that the symmetry was not worth rewriting every call site. That call is reversed: the owner asked for it, and the SDK has already broken compatibility in this series, so the rewrite is paid for once rather than twice. The rejected alternative is kept and rewritten rather than deleted. A design record that quietly loses its own reversals is not a record. Signed-off-by: Yves Brissaud --- .../2026-09-15-client-module-resolution.md | 231 ++++++++++++++++++ .../2026-09-13-unified-client-generation.md | 153 +++++++++--- 2 files changed, 352 insertions(+), 32 deletions(-) create mode 100644 hack/designs/2026-09-15-client-module-resolution.md diff --git a/hack/designs/2026-09-15-client-module-resolution.md b/hack/designs/2026-09-15-client-module-resolution.md new file mode 100644 index 0000000..6fc808e --- /dev/null +++ b/hack/designs/2026-09-15-client-module-resolution.md @@ -0,0 +1,231 @@ +# Asking the engine for a client's module + +Status: proposed +Date: 2026-09-15 +Scope: an engine change, proposed from the consumer side. Not part of +`dagger/java-sdk#23`. + +## Terms + +**Client** — a module that generated code talks to, declared in `dagger.toml` +under an SDK scope's `clients`. The declaration is what causes the bindings to +be generated. It is a *generation-time* input. + +**Serve** — make a module's fields resolvable in a session, so `dag.` +exists. Until something serves, the field is absent and a query naming it fails. + +**Context** — a `ModuleSource`'s `contextDirectory`: the full directory loaded +for that module, with the module itself at `sourceRootSubpath` inside it. For a +module in a workspace this is the git root; for a git module it is the clone. + +## Requirements + +1. **Generated code is self-sufficient.** Once bindings exist, they load their + module on their own terms. Deleting the SDK registration, or the scope, or + the `clients` list must not stop committed code from running. Generated code + is code; it does not consult build configuration at run time any more than a + compiled binary re-reads its dependency manifest. +2. **A module cannot reach the caller's workspace.** `dagger/dagger#14148`: a + transitively-loaded dependency can currently obtain `currentWorkspace` and + enumerate the user's workspace root. +3. **No `[[dependencies]]`.** Manifest v2 removes it. +4. **One code path.** The same generated call inside a module and outside one. + +Requirements 1 and 2 are the hard pair. A self-sufficient client carries a +descriptor, and a descriptor is not a capability — anything that can hold one +can forge one. Resolving a forged path is precisely the hole. + +## The resolution + +Do not ask *who is allowed to name this path*. Ask *whose filesystem the path is +interpreted against*. + +A module already owns a context directory, and it is wider than the module: it +is the tree the module was loaded from, with siblings in it. The engine holds +that context for every loaded module and never hands it over wholesale. + +So a module may name a path, and the engine resolves it **in the calling +module's own context** — never in the caller-of-the-caller's workspace. + +| caller | its context | so a path names | +| --- | --- | --- | +| the user's own module | the user's workspace | the user's own modules | +| a git dependency | its own clone, at its pin | modules in its own repository | +| a standalone program | its workspace | its own workspace | + +A third-party dependency is confined to itself, structurally: it holds no handle +to the user's workspace, so there is no path it can write that reaches one. The +refusal is not a check standing in front of a capability — the capability is not +there to check. + +And the field grants nothing new. A module's own context is already its own; the +engine simply interprets a path against it and loads a module. No `directory`, +no `file`, no `export`, no way to walk sideways into them. + +## Proposal + +```graphql +extend type Query { + """ + A module at a path in the caller's own source context. + """ + contextModuleSource(path: String!): ModuleSource! +} +``` + +Present in **both** the module-facing and client-facing schemas. Resolution root: + +- **Module session** — the calling module's `ModuleSource.contextDirectory`. The + engine created the session and holds the source; the caller asserts nothing + about which context is used. +- **Client session** — the caller's workspace, which it already reaches through + `currentWorkspace`. No new capability for clients at all. + +Paths are clamped to the context, as workspace paths already are. + +```mermaid +graph TD + subgraph user["the user's workspace"] + APP["module: app
context = the workspace"] + GREET["module: greeter"] + end + subgraph repo["github.com/acme/tool @ pin"] + DEP["module: tool
context = the clone"] + HELPER["module: helper"] + end + APP -->|"contextModuleSource('/.dagger/modules/greeter')"| GREET + DEP -->|"contextModuleSource('/helper')"| HELPER + DEP -.->|"no handle to it"| user +``` + +A git client needs none of this: `moduleSource(refString:, refPin:)` is already +self-sufficient, already works in both session kinds, and is unchanged. + +### What the SDKs do + +The client package keeps the descriptor it already carries, and serves once per +session on first use: + +``` +contextModuleSource(path: "/.dagger/modules/greeter").asModule().serve() +``` + +For the Java SDK this is a one-line change in `ModuleTargets`: the local branch +swaps `currentWorkspace.moduleSource(path)` for `contextModuleSource(path)`, and +`servesWorkspacePaths` goes away because the answer is now yes everywhere. The +descriptor, the pin and the git/local split all stay exactly as they are, which +is the point — they are what makes the bindings stand on their own. + +### What it does for #14148 + +It supplies the sanctioned route the issue says must exist before +`currentWorkspace` can be gated for module sessions, and it answers the question +the issue leaves open — whether the top-level module differs from a dependency — +with **no**. They run the same rule against different contexts. The top-level +module's context is the user's workspace because it *is* the user's code; a +dependency's context is its own source because that is what it is. + +## Alternatives considered + +**Look the client up by name in `dagger.toml`.** Proposed first, and wrong. It +makes generated code depend on build configuration at run time: delete the SDK +registration or the `clients` list and committed, compiled code stops working. +It also breaks any client that travels. Self-sufficiency is not a nice-to-have +here — the bindings are an artifact, and an artifact that silently depends on +the config that produced it is not one. + +**Serve a scope's clients automatically when the engine creates the session.** +The closest thing to what `[[dependencies]]` did, and needs no new schema at +all. Rejected for being eager — one unresolvable client breaks every other one +and the module with them — and for the same self-sufficiency reason: it is the +workspace config doing the work, not the code. + +**A capability token baked in at generation time.** Unforgeable, self-sufficient, +and the theoretically right answer. Rejected because making it unforgeable needs +signing and key management in the engine; a content digest avoids the secret but +changes on every source edit, so local development would invalidate bindings +continuously. Resolving in the caller's own context gets the same confinement +with no new machinery. + +**Gate `currentWorkspace` for modules and stop there.** Fixes the hole, breaks +the legitimate case, leaves the SDKs with nothing. The option `#14148` +explicitly warns against. + +## Security model + +**The asset is the user's filesystem.** A module can already run containers, +reach the network and execute arbitrary code; loading another module is not a +privilege escalation for something that can already do all three. What a module +must not have is the user's files, which is what `currentWorkspace` hands over +and what step 3 below closes. + +**Chains do not amplify.** `Module.serve` is a per-session schema mutation, so a +dependency that serves something mutates its own schema and not its caller's — +the reason the TypeScript dispatcher has to serve into its own session rather +than the spawner's. Combined with per-context resolution, a dependency three +hops down reaches its own context and nothing else. Depth buys nothing. + +**This opens no new door.** A module can already serve an arbitrary module from +code today: + +```graphql +{ moduleSource(refString: "github.com/somewhere/thing@v1") { asModule { serve } } } +``` + +No workspace, no configuration, no declaration. Loading initiated from code and +invisible in config is the status quo. `contextModuleSource` adds a strictly +narrower form of it. + +### What does change, and it is worth naming + +A declaration in `[[dependencies]]` was **authoritative**: the engine enforced +it, so the set in the manifest was the set. A descriptor in generated code is +**descriptive**: it records what the generator emitted, and nothing stops +hand-written code from loading more. The set remains statically visible — the +client packages are committed, and listing them gives it — but "this module may +not load anything new" stops being a statement that can be enforced. + +That is manifest v2's doing rather than this design's, and this design does not +restore it. Two separable answers, neither of which reintroduces a run-time +configuration lookup: + +1. **For auditability**, have the SDK emit a generated, committed record of the + client set: diffable, greppable, and readable by supply-chain tooling. + Descriptive metadata, explicitly not an authorization input. This recovers + the property a manifest gave without making generated code depend on + configuration to run. +2. **For enforcement**, if it is ever wanted, policy belongs at the workspace or + session level — modules loaded here may only load from these registries — + rather than in a per-module manifest. Orthogonal to this design and composes + with it. + +### Vendored third-party code + +A third-party module vendored into the user's workspace shares the user's +context, so it can name modules there. It is still confined to loading them and +gets no file access. This is not treated as a gap: vendoring third-party code +into your own tree is a decision to run it beside your files, and the +responsibility sits with whoever vendored it. A module reached by git ref, which +is the ordinary case, is confined to its own clone. + +## Migration + +1. Add `contextModuleSource(path:)` to both schemas, version-gated. Purely + additive; nothing changes behaviour. +2. Move each SDK's serve onto it — the TypeScript dispatcher's + `currentWorkspace { moduleSource(path:) }`, and the Java SDK's + `ModuleTargets`. +3. Gate `currentWorkspace` in the schema served to module sessions, and land it + as the fix for `#14148`. + +Steps 1 and 2 are additive and stand on their own merit. Step 3 is the security +fix and depends on them. + +## Open questions + +- **The name.** `contextModuleSource` echoes `contextDirectory`, which is what + it resolves against. `ownModuleSource` and `localModuleSource` were the other + candidates; the second is misleading, since a git module has a context too. +- **Does a module ever need a *wider* context?** A module whose client sits + outside its own git root has no path to it. Believed not to arise — a client + is either in the same tree or reached by git ref — but not proven. diff --git a/hack/designs/done/2026-09-13-unified-client-generation.md b/hack/designs/done/2026-09-13-unified-client-generation.md index 86dea50..d6a55cb 100644 --- a/hack/designs/done/2026-09-13-unified-client-generation.md +++ b/hack/designs/done/2026-09-13-unified-client-generation.md @@ -12,7 +12,8 @@ This document uses one word per concept. | **target** | A Dagger module that Java code calls. | | **scope** | A directory the SDK generates into. A *module scope* holds a Dagger module. A *standalone scope* is an ordinary Maven project that only calls targets. | | **bindings** | The generated Java code. | -| **client package** | `io.dagger.client.modules.`, holding one target's bindings. | +| **client package** | `io.dagger.client.modules.`, holding one client's bindings. Core has one, `io.dagger.client.modules.core`; so does each target. | +| **session** | `io.dagger.client.Session`: hand-written, owns the engine connection, and is what every entry point takes. | | **target descriptor** | The generated record of one target's name, reference and pin, used to serve it. | The engine and its configuration use "client" for what this document calls a @@ -175,23 +176,56 @@ built from `dagger/dagger#13992` is out of date. ### The generated layout ``` -io.dagger.client core API and the hand-written runtime +io.dagger.client the hand-written runtime +io.dagger.client.modules.core the core API io.dagger.client.modules. one package per target ``` -`io.dagger.client` keeps its present meaning and contents: the hand-written -runtime (`Dagger`, `QueryBuilder`, `engineconn`, `exception`, `graphql`, -`telemetry`) and the generated core types, including the generated `Client` -class that binds the GraphQL `Query` root. `Dagger.dag()` still returns that -`Client`, and core is still reached as `dag().container()`. No module written -against this SDK changes the way it calls core. +`io.dagger.client` holds hand-written code and nothing else: `Dagger`, +`Session`, `QueryBuilder`, `Arguments`, `InputValue`, `Scalar`, `IDAble`, the +serializers, `ModuleTarget`, `ModuleTargets`, `engineconn`, `exception`, +`graphql`, `telemetry`. No generation writes into it. + +Core is generated into `io.dagger.client.modules.core`, under the same root as +every target, and is reached the way a target is: + +```java +import static io.dagger.client.modules.core.Core.core; + +core().container() +``` + +`core(dag)` names a session, `core()` takes the ambient one. `Core` is the +generated class that binds the GraphQL `Query` root; `Client` no longer exists, +and neither does `dag().container()`. + +What makes that possible is that `Dagger.dag()` stops returning a generated +type. `Session` is hand-written, owns the connection and exposes the query +builder every generated package chains from, and is what every entry point +takes. `AutoCloseableSession` replaces `AutoCloseableClient`. Core is then a +client package like any other: nothing generated is privileged, and +`core(dag())` and `sdkHelpers(dag())` are the same shape. + +One asymmetry survives, and it is a link-time one rather than an API one. The +hand-written runtime still imports generated core: `Telemetry` needs +`FunctionCall` and `FunctionCallArgValue`, which are core schema types, and the +generated `Version` constant lands in core because that is where the version +visitor emits. So core is *generated* like any other client and *reached* like +any other client, but it is not unlinked from the runtime the way another client +is. `Telemetry`'s dependency is inherent — tracing a module function call needs +the type that describes one. `Version` is merely misfiled: it is a codegen +artifact rather than a schema type, and emitting it into the runtime package +would remove it. Neither is worth a second break on its own; both are recorded +here so the claim above is read as "core has no privileged API" rather than +"core is entirely unexceptional". Each target gets one package under `io.dagger.client.modules`, named from the target's final name. The target's own types live there and nowhere else. Nesting the targets one level down is what makes that safe: a target named `graphql` or `exception` becomes `io.dagger.client.modules.graphql`, which cannot collide -with the runtime's own `io.dagger.client.graphql`. Only names Java itself -reserves are refused, and two names that normalize to one package segment. +with the runtime's own `io.dagger.client.graphql`. Names Java itself reserves +are refused, two names that normalize to one package segment are refused, and so +is `core`, which the generated core API has. The way into a target moves there too, as a static method on the target's own root type: @@ -204,7 +238,8 @@ sdkHelpers().moduleManifest() Core is not extended with an accessor. One import is the whole of the integration, and a caller that never names a session gets the ambient one; a -caller that has one passes it, as `sdkHelpers(dag)`. +caller that has one passes it, as `sdkHelpers(dag)`. Core is entered by the same +two forms, which is the whole of the difference between core and a target: none. ### What belongs to core and what belongs to a client package @@ -471,16 +506,38 @@ every module generation. It is not needed here: a module's own types are its own hand-written Java, and the current SDK does not generate them either, because the module-facing schema holds core and dependencies only. -**Split the session from the core client.** The abandoned attempt moved the -hand-written runtime to `io.dagger.sdk`, the generated core to `io.dagger.core`, -made `Dagger.dag()` return a new `Session` handle, and made core reachable as -`core(dag())` so that core would be "a target like any other". The symmetry is -real. The cost is that `dag().container()`, the most common expression in every -Java module, becomes `core(dag()).container()`, every existing module must be -rewritten, and the annotation processor's many references to core types move -with it. The design here gets package separation without that. A `Session` type -that owns the connection instead of a generated class remains a reasonable -tidy-up on its own; it is not part of this change. +**Make core a target like any other — rejected, then adopted.** The abandoned +attempt moved the hand-written runtime to `io.dagger.sdk`, the generated core to +`io.dagger.core`, made `Dagger.dag()` return a new `Session` handle, and made +core reachable as `core(dag())`. This document first rejected it: the symmetry +was real, but `dag().container()` is the most common expression in every Java +module, and package separation could be had without rewriting it. + +That is reversed. Core is generated into `io.dagger.client.modules.core`, +reached as `core(dag())` or `core()`, and `dag().container()` is gone. + +Three things changed the arithmetic. + +- The owner asked for it. The symmetry is the point of the feature, not a + side-effect of it, and one shape for "reach a client" is worth more than the + call sites it costs. +- This series already breaks every module that uses a target: its types move + package and its imports change. Moving core in the same release is one + migration rather than two, and a second break later would be the expensive + one. +- The rejection undercharged for what it was keeping. Leaving core flat in + `io.dagger.client` leaves the generated `Client` class there, so the + hand-written `Dagger.dag()` returns a generated type and every entry point in + every client package takes one. The `Session` this document called "a + reasonable tidy-up on its own" is not separable: it is what lets core stop + being special. + +What it kept from the rejection is the package root. The abandoned attempt split +the tree three ways, into `io.dagger.sdk`, `io.dagger.core` and one package per +target; here the runtime stays at `io.dagger.client` and core joins the targets +under `io.dagger.client.modules`. Only generated code moves, so a module's +imports of the runtime — `io.dagger.client.exception`, the annotations — are +untouched, and a target's package is spelled the same as before. **Serve every target eagerly when the session opens.** Simpler than serving from the entry point: one bootstrap, run once. Rejected because it makes an unusable @@ -511,7 +568,7 @@ parameter already exists, but generated clients do not use it. | Component | Change | | --- | --- | | `sdk/dagger-codegen-maven-plugin` | Read `@sourceMap` attribution; partition a schema into core and one target; validate names; resolve type references through a registry so more than one output package is possible; take a generation plan instead of a single schema; emit a client's entry points and the descriptor they serve; a goal that inserts the Maven profile. | -| `sdk/dagger-java-sdk` | Public query transport so generated code outside `io.dagger.client` can build queries; `ModuleTarget` and `ModuleTargets`; `CLISession`; the `Connection` fallback and the `--load-workspace-modules` flag; a synchronized `Dagger.dag()`. | +| `sdk/dagger-java-sdk` | Public query transport so generated code outside `io.dagger.client` can build queries; `ModuleTarget` and `ModuleTargets`; `CLISession`; the `Connection` fallback and the `--load-workspace-modules` flag; a synchronized `Dagger.dag()`; `Session` and `AutoCloseableSession`, which is what `dag()` and `connect()` return once no generated type is left in `io.dagger.client`. | | `codegen.dang` (new) | Build a plan, run the plugin, vendor the result. Shared by both scope kinds. | | `mod.dang` | Build a module scope's plan: core from the module-facing schema, one entry per recorded target. | | `client.dang` (new) | Build a standalone scope's plan, merge core, emit the descriptors, insert the Maven profile. | @@ -529,8 +586,11 @@ compiles generated output; these extend it. target's contributed fields on `Query` and on `Binding` stay on the core class; a core-only schema partitions to itself. - The type registry resolves a core type referenced from a client package to + `io.dagger.client.modules.core`, and a hand-written runtime class to `io.dagger.client`. -- Plan execution: a two-target plan emits three packages and one `Client`. +- Plan execution: a two-target plan emits three packages, one of them core. +- Core's entry point: `Core.core(Session)` and `Core.core()`, in + `io.dagger.client.modules.core`, with no descriptor and nothing served. - Core merge: two targets contributing to `Binding` merge; two targets whose bare cores differ are refused, and the message names both targets and both engine versions; reversing the target order changes nothing. @@ -632,13 +692,17 @@ to a different version, will conflict. The profile is one marked element and is removable, generation refuses to overwrite an unmarked profile of the same id, and the generated tree is inert without the profile. -**Breaking change for existing modules that use targets.** A target's types move -from `io.dagger.client.` to -`io.dagger.client.modules..`. Call sites are unchanged, imports -are not, and the nested arguments class stays where it is. Targets are recent -and the change is mechanical, so no compatibility shim is proposed. An -end-to-end check compiles a module written against the old layout after -migration, and the README documents the move. +**Breaking change for every existing module.** A target's types move from +`io.dagger.client.` to `io.dagger.client.modules..`, and +core's move to `io.dagger.client.modules.core.`. For a target the call +sites are unchanged and only the imports are; for core both change, because +`dag().container()` becomes `core().container()`. `Dagger.dag()` returns a +`Session`, and `AutoCloseableClient` is `AutoCloseableSession`. The change is +mechanical but it is not small, and no compatibility shim is proposed: a +deprecated `Client` would have to be generated from the same schema into the +package the runtime occupies, which is the arrangement being removed. The +README documents both moves, and `engine-e-2-e:dev-sdk-check` runs a scaffolded +module on a real engine, so the new call shape is proven rather than asserted. **Size.** This changes the code generator, the runtime library, the generation driver and the test suite together. See **On shipping this as one change**. @@ -679,7 +743,7 @@ graph TD CE["one entry per target:
target.clientSchemaIntrospectionJSON,
owned types only"] --> P["codegen.dang: the plan"] P --> G["dagger-codegen-maven-plugin
one Maven invocation, every package"] - G --> CORE["io.dagger.client
core types, all their fields"] + G --> CORE["io.dagger.client.modules.core
core types, all their fields"] G --> CLI["io.dagger.client.modules.<target>
one package per target"] CORE --> OUT @@ -706,7 +770,7 @@ sequenceDiagram CLI-->>SDK: {"port", "session_token"} SDK->>Eng: attach end - SDK-->>App: Client + SDK-->>App: Session App->>SDK: TheTarget.theTarget(dag) alt the package carries a descriptor SDK->>Eng: moduleSource(ref).withName(name).asModule().serve() @@ -801,6 +865,31 @@ and the documentation. execs let a concurrent check overwrite the jar in between. One exec closes it. +Nine more land the reversal recorded under **Alternatives considered**. + +20. **`codegen: name the runtime package apart from the generated core`** — the + registry resolved a schema type and a hand-written runtime class through one + package name. Held apart first, so the move is a change of one constant. No + output changes. +21. **`java-sdk: generate core as a client package`** — the atomic one. + `io.dagger.client.Session`, `AutoCloseableSession`, a `Dagger.dag()` that + returns a session, core emitted into `io.dagger.client.modules.core` with + `Core` as its root type and `ClientEntryPoint` split into a core kind and a + module kind, and the annotation processor rewritten to enter core by name. + It does not divide further: the processor cannot compile against a `Core` + that does not exist, and `dag()` cannot return a `Session` while the + processor calls `dag().module()`. +22. **`codegen: refuse a module named core`** — the segment is taken. +23. **`templates: reach core through its own package`**. +24. **`e2e: assert core where it now lives`**. +25. **`engine-e2e: pin core's package in the initialized module`**. +26. **`README: document core as a client package`**. +27. **`hack/designs: record core as a client package`** — this entry and the + rewritten alternative. +28. **`prebuilt: rebuild the codegen plugin`** — again, for the same reason as + patch 14: generation seeds from `prebuilt/m2` and never compiles the plugin + sources when it exists, so a stale jar silently runs the old generator. + Four things differ from what this document first planned, and the reasons are worth keeping. The formatter patch was not planned; it was added because the drift made every other patch noisy. `codegen.dang` and the per-target layout From bf1b2d05fe55bc949cb52f9d7b57c6e5fa9dd399 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Sep 2026 04:27:17 +0200 Subject: [PATCH 28/28] prebuilt: rebuild the codegen plugin for the core move The second rebuild in this series, and not a duplicate of the first. The committed jar has to match the generator at the point in the stack where it is used: the earlier rebuild matches the plan-driven generator, and every patch between the two generates core flat, correctly, with that jar. This one matches the generator that emits core into its own package. Folding the two into one would put a jar that emits modules/core underneath the patches that still expect core flat, so the intermediate patches would generate one layout and assert the other. Generation seeds the local Maven repository from prebuilt/m2 whenever it exists and never compiles the plugin sources in that case, so without this every scope would keep generating with the old core layout while the sources say otherwise. Signed-off-by: Yves Brissaud --- .../dagger-codegen-maven-plugin-0.21.4.jar | Bin 113746 -> 115968 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/prebuilt/m2/io/dagger/dagger-codegen-maven-plugin/0.21.4/dagger-codegen-maven-plugin-0.21.4.jar b/prebuilt/m2/io/dagger/dagger-codegen-maven-plugin/0.21.4/dagger-codegen-maven-plugin-0.21.4.jar index ef17e4752afa63f7f69b814510f1c9d943edbe21..90e7c335d99605dac8dee88464267f9f3df2410d 100644 GIT binary patch delta 37590 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 Wvj&-<2(J-f1hgXW)`&b1arqyxo8FWF