From ebf7fa4359ba12041f2d9a4d40ae83b39d0d000a Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Wed, 26 Aug 2026 21:16:35 +0200 Subject: [PATCH 01/17] hack/designs: plan modules-have-clients A module's dependency and a standalone generated client are the same artifact built twice. Record the design that collapses them: the hand-written runtime moves to io.dagger.sdk, core types are generated into io.dagger.core, each bound module gets io.dagger.client. generated from the engine's client-facing schema, and an idempotent serve preamble makes the emitted bytes identical whether the client is a module's dependency, the module's own self client, or a standalone artifact. Decisions D1-D5 are recorded in the document, each checked against the engine source or the Go SDK rather than assumed: the client schema really does exclude the bound module's dependencies, and it installs that module namespaced rather than promoting it to the Query root, which is what makes the self client fall out of the same code path. Signed-off-by: Yves Brissaud --- .../2026-08-26-modules-have-clients.md | 1104 +++++++++++++++++ 1 file changed, 1104 insertions(+) create mode 100644 hack/designs/2026-08-26-modules-have-clients.md diff --git a/hack/designs/2026-08-26-modules-have-clients.md b/hack/designs/2026-08-26-modules-have-clients.md new file mode 100644 index 0000000..d69518d --- /dev/null +++ b/hack/designs/2026-08-26-modules-have-clients.md @@ -0,0 +1,1104 @@ +# Modules have clients, not dependencies + +Status: proposed +Date: 2026-08-26 + +## Problem + +A module's dependency and a standalone generated client are already the same +thing, built twice. + +When a Java module calls a dependency today, it calls generated typed bindings +against a served module. That is the definition of a client. The only thing that +differs from a standalone client — a test, an app, the kind of artifact the Go +and TypeScript SDKs already generate — is *how the session and the target module +are obtained*: inside a module the engine has already served the dependency into +the session schema, while outside the process must open its own connection and +serve the target itself. + +This SDK does not model that. It has one generated package, `io.dagger.client`, +produced from `moduleSource.introspectionSchemaJSON` — the *module-facing* +schema, which loads the module's dependencies and merges them into one flat +schema (`Mod.generateModule`, `mod.dang:231`). Every type from core and from +every dependency lands in that single package, next to the hand-written runtime: + +``` +/sdk/src/main/java/io/dagger/client/** hand-written runtime +/sdk/src/generated/java/io/dagger/client/** core + every dependency, flat +``` + +Three consequences: + +1. **There is no client artifact.** Nothing this SDK produces can be handed to an + application that is not itself a Dagger module. `io.dagger.client.Client` is + reachable only through `Dagger.dag()`, a process-wide singleton + (`Dagger.java:6`), and the bindings it exposes are whatever that one module's + merged schema happened to contain. +2. **Dependency bindings are unattributed.** A dependency's types are + indistinguishable from core's once generated; nothing in the output records + which module contributed `Report` or `Binding.asHello`. Regenerating for a + different dependency set silently changes the meaning of the same package. +3. **The generator only has one mode.** `CodeWriter` hardcodes the target package + (`CodeWriter.java:20`) and every visitor resolves type references with + `ClassName.bestGuess(simpleName)`, which is only correct because everything is + in one package. There is no seam at which a second package could be emitted. + +Meanwhile the engine has already moved. `ModuleSource.clientSchemaIntrospectionJSON` +is the *client-facing* schema. Its implementation +(`core/schema/modulesource.go`, `clientSchemaIntrospectionJSONFile`) starts from +the core-only schema builder and installs exactly one module, namespaced, not as +an entrypoint. Its own doc comment is the specification this design builds on: + +> only the bound module is installed, as a normal namespaced module, so a +> generated client reaches its functions via `dag.` and never through +> a promoted Query root. The module's own dependencies are deliberately excluded +> — a client is generated for a single module plus core, not for its whole +> dependency graph. Unlike the module-facing schema, it hides no core types. + +The Go SDK consumes it already (`go-sdk.dang:360`, `generateClient`). Java does +not consume it at all. + +## Decisions + +These were the fork-in-the-road questions; they are settled and recorded here so +the rest of the document reads as consequences rather than options. + +**D1 — a client is deps-excluded (leaf-shaped).** Confirmed against the engine +source above. A *dependency-authored* type does not cross between two clients. +Core types do, because they are literally the same Java type. + +**This costs nothing, because the engine already forbids it.** An earlier draft +of this document claimed cross-dependency composition works today and that D1 +gives it up. That was wrong. `Module.validateTypeDef` rejects a module whose API +exposes a dependency-authored type, in all three positions — object fields +(`core/module.go:1220`), function return types (`core/module.go:1244`) and +function arguments (`core/module.go:1262`), each with +`cannot reference external type from dependency module %q`. A module can +therefore never hand a dependency's type to another module in the first place, +so a deps-excluded client cannot lose a capability that does not exist. + +Two consequences worth stating, because they fall out of the same fact: + +- The merged, deps-included schema this SDK generates today is *wider than + anything a module is allowed to use*. Flattening it into one package was + always over-generation. +- A client's schema can only reference core types and its own module's types. + There is no "type owned by a third module" case for the partition to handle — + the engine has already made it unrepresentable. + +**D2 — the hand-written runtime moves to `io.dagger.sdk`.** `io.dagger.client` +becomes *exclusively* generated: one package segment per bound module, nothing +else, ever. + +The reason is naming honesty, not collision avoidance. An earlier draft justified +the move by the risk of a module named `telemetry` or `graphql` colliding with an +SDK subpackage; that argument does not hold up, because the collision set is +exactly four names (`engineconn`, `exception`, `graphql`, `telemetry`) and +`io.dagger.clients.` would avoid it while moving zero files. The real reason +is that `io.dagger.client.QueryBuilder` **is not a client**. Once +`io.dagger.client.` means "the generated client for module m", leaving the +transport at that prefix makes the package name a lie. The collision going away +is a welcome side effect, not the justification. + +`io.dagger.runtime` was rejected: this repository already uses "runtime" for the +module runtime (the build/package contract under `runtime/`), so the name would +actively mislead. `io.dagger.sdk` matches both the vendored directory +(`sdk/src/main/java`) and the Maven artifact (`dagger-java-sdk`). + +**D3 — the entry point is a static factory, with a static-import alias.** +`Hello.from(dag())` is primary. The same class also carries a static +`hello(Client)` so a caller who prefers it can +`import static io.dagger.client.hello.Hello.hello;` and write `hello(dag())`. +Both are two lines of generated code delegating to one constructor, so offering +both costs nothing and lets the call site choose. A terse `f()` was considered +and dropped: cryptic abbreviations do not belong in a generated public API. + +**D4 — both entry points, mirroring the Go SDK.** `generateClient`, +`generateAllClient` (`@generate`, driven by workspace config), and `initClient`, +matching `go-sdk.dang:360/384` field-for-field. `currentModule.asSDK.clients` +exists on the pinned engine (verified by introspection: +`CurrentModuleAsSDKClient { id, module, moduleSource, path, pin }`). + +**D5 — a module generates a client for itself, and that is how it calls +itself.** Self calls go through `io.dagger.client.` exactly like calls to +any dependency; there is no separate self-call mechanism. This is a must-have, +not a convenience: a module that cannot reach itself through the engine cannot +benefit from function-level caching on its own calls. + +Reading a module's *own* `clientSchemaIntrospectionJSON` installs the module, +which for Java means building it — and the build needs the very sources this +generation is producing. On a first `init` + `generate` there is no `sdk/` at +all. The circularity is real, and it is broken by **bootstrapping through a +staged workspace**, the same device `generateLocalDependencies` already uses for +local dependencies: + +1. generate `io.dagger.core` and one client per declared dependency (neither + needs the module built — see D7); +2. vendor the runtime plus those packages, **carrying over the previously + committed self client if one exists**, so module code that already references + it still compiles; +3. run the annotation processor to produce the entrypoint, as today; +4. stage all of that onto the workspace (`ws.withNewDirectory(...)`) — the + module is now buildable — and read + `stagedWs.moduleSource(ref).clientSchemaIntrospectionJSON`, which makes the + engine build and introspect the module; +5. generate the self client from that schema with the same `client` mode, and + replace the carried-over one. + +The carried-over self client is only ever a compile-time crutch for step 4; the +committed output always comes from step 5. Adding a function and calling it +through the self client in the same edit fails the bootstrap build, exactly as +it would in any generated-client workflow: generate first, then call. + +The engine already serves a module to itself at call time +(`CallOpts{SkipSelfSchema: false}`, `core/object.go:1436`), so inside the module +the self client's serve is deduplicated by the engine like any other repeat +serve. + +Simple-name overlap is the one ergonomic cost: the authored +`io.dagger.modules.hello.Hello` and the generated `io.dagger.client.hello.Hello` +share a simple name, and inside the module the authored one is in scope. The D3 +static-import alias is the answer, and it is the documented idiom for self calls: + +```java +import static io.dagger.client.hello.Hello.hello; +… +hello(dag()).build(source) // a self call, through the engine +``` + +The static import brings in the *method*, not the type, so nothing clashes. A +module named after a core type (`workspace`, `env`, `secret`, `service`, `cache`) +has the same overlap against `io.dagger.core` and the same answer. + +**D6 — the query transport becomes public SDK API.** Generated code moves out of +`io.dagger.client`, so every runtime symbol it touches has to be reachable across +a package boundary. Today they are package-private: + +| Symbol | Today | Why generated code needs it | +|---|---|---| +| `QueryBuilder` (class, ctor, `chain`, `chainNode`, `execute*`) | package-private | field, ctor param and every field method on every generated type | +| `InputValue` | package-private **interface** | every generated input object has it in `implements` | +| `Arguments.merge` | package-private | optional-argument merging | +| `Scalar.convert()` | package-private | scalar serialization | +| `QueryPart` | package-private | transitively, via `QueryBuilder`'s signature | +| generated `Client` constructors | package-private | `AutoCloseableClient extends Client` becomes cross-package | + +`InputValue` is the one that makes this non-negotiable rather than a preference: +a class cannot implement a non-public interface from another package. Without +D6 the cutover simply does not compile. + +This **reverses a decision already recorded in this repo's design corpus**: +`hack/designs/2026-08-17-nullable-object-returns.md` deliberately kept +`QueryBuilder` package-private and rejected a public transport seam as permanent +API surface. That reasoning was right for that change and does not survive this +one — generated code in another package cannot be served by a package-private +transport. The reversal is deliberate and is called out here rather than made +silently. A public `Client.queryBuilder()` accessor is added too; it does not +exist today. + +**D7 — `io.dagger.core` depends on the engine and the consumer, never on a +bound module.** Core is generated from the schema the *consumer* is entitled to +see, partitioned to strip every module-owned symbol: + +- a **module** gets its core from its own module-facing + `introspectionSchemaJSON`. That schema loads the module's dependencies but + never the module itself (`moduleSourceIntrospectionSchemaJSON` → + `loadDependencyModules` → `SchemaIntrospectionJSONFileForModule`), so there is + no circularity, and it hides `TypesHiddenFromModuleSDKs` — which means + `dag().host()` in module code **stays a compile error**, closing the guard + regression an earlier draft had accepted; +- a **standalone client** gets its core from the bound module's client-facing + `clientSchemaIntrospectionJSON`, which hides nothing, because a client is + allowed everything the CLI is. + +Both are "the engine's core, as this consumer is allowed to see it". The +dependency-owned symbols in the module-facing schema are exactly what the +partition strips, so the result is core-only either way. Because every +`io.dagger.client.` package refers to core types only by name, the +per-module client bytes are identical across both contexts even though the +*core* package legitimately differs (hidden types, compatibility view). That is +the property the byte-identity claim is about. + +The compatibility view is the residual risk: the engine renders core through the +target module's declared `engineVersion` (`core/schema/modulesource.go:3537`), +and a dependency declared at a pre-`v1.0.0` version gets legacy per-type ID +scalars (`Sub1ID`, `loadSub1FromID`) that a `v1.0.0` core does not have. +Generation therefore fails early and clearly when a dependency declares an +`engineVersion` below `v1.0.0-0`, the floor this SDK already requires. Anything +subtler surfaces as a compile error in the vendored SDK build, which is loud if +not pretty. + +**D8 — the SDK can open its own session again.** `Connection.get` regains the +path that `89b80fe` removed as dead code: honour `DAGGER_SESSION_PORT` / +`DAGGER_SESSION_TOKEN` when set (a module runtime, or `dagger run`), otherwise +spawn `dagger session --label dagger.io/sdk.name:java …`, read the +`{port, session_token}` line it prints, and connect. The binary comes from +`_EXPERIMENTAL_DAGGER_CLI_BIN` or `dagger` on `PATH`. This is precisely what the +Go SDK (`dagger.Connect`) and the TypeScript SDK +(`sdk/typescript/src/provisioning/bin.ts:176`) do, minus one thing: both also +auto-download a CLI matching their version when none is found. The Java SDK does +not, in this series — like Testcontainers using whatever Docker the host has, it +uses the `dagger` the host has, and says so clearly when there is none. Download +is a follow-up, not a blocker, and it needs the checksum-verifying downloader +and archive dependencies that were dropped for weight. + +`ProcessBuilder` is enough for the session process; the `fluent-process` +dependency that the old `CLIRunner` used is not reintroduced. `AutoCloseableClient` +closes the session process it started; a connection taken from the environment +owns nothing. + +**D9 — the SDK stages its own local dependencies.** `mod.dang` no longer calls +the engine's `ModuleSource.generateLocalDependencies`. That routes through +`Workspace.generators(include: [])`, and two things make it +unusable here, both verified against engine source and by probing: + +- the engine returns an **empty generator group for any value workspace** + (`isSyntheticWorkspace` → `IsValueWorkspace`: a workspace built from a + `Directory`, which is what every in-memory e2e check runs in), so a Java + module with a local Java dependency can never be generated in a check; +- in this repository the rollup carries exactly four generator nodes + (`dagger-dang-sdk`, `packager`, `sdk-sdk`, `templates`) and none for + `java-sdk`, on `upstream/main` as much as here, whatever the root config + says — so the engine path had never actually worked for this SDK. + +Instead, `generatedOverlay` recurses: for each dependency that is not git and +sits in the modules registered to this SDK in the workspace (`ws.sdk(name: +currentModule.name).modules` — the registry, not the cwd-scoped +`modules(ws)`, since a dependency is usually a sibling), it generates that +module and overlays its `sdk/` and `src/generated/java` onto the workspace with +`withNewDirectory`. Overlays rather than changesets, because a changeset is +measured from the caller's cwd and may not reach a sibling. Dependencies of +other SDKs, remote ones, and skip-marked ones are assumed committed — the same +rule the engine applies. + +Two facts about module sources inside a value workspace follow from the same +probing and are handled explicitly: a local dependency reports +`kind = DIR_SOURCE` (not `LOCAL_SOURCE`) with an empty `asString`, so ownership +is decided on "not git", and the binding baked into a client is normalized to +`LOCAL_SOURCE` by workspace path for both — which also keeps the bytes +identical between a client generated in a value workspace and on the host. + +### Where the Go and TypeScript SDKs actually are + +Worth stating plainly, because it sets expectations for review: + +- **Go SDK**: has `generateClient` / `generateAllClient` / `initClient` exactly as + D4 describes, and this design copies that shape. But Go *module* generation + still delegates to the engine (`generatedContextDirectory`, `mod.dang:72`), + which uses the module-facing merged schema. **Go has not unified + dependencies-as-clients.** +- **TypeScript SDK**: `design/client-gen.md` describes the client schema as + having "deps loaded" and the target module's "own types promoted to `Query` + for self-bindings". **Both statements are stale** against the engine source + quoted above. Do not use that document as the spec for this one. + +So the `generate-a-client` half of this work has a proven reference to copy, and +the `dependencies-become-clients` half does not. Java is first there. That is +where the design risk is concentrated. + +## Goals + +- One generator, one output shape. The package generated for a module is + byte-identical whether it was produced because another module declared that + module as a dependency or because someone asked for a standalone client. +- Core types live in their own package, shared by every generated client. +- A module declares a dependency in `dagger-module.toml` exactly as it does + today; what changes is that the SDK generates a *client* for it. +- Produce a standalone client artifact that an ordinary Maven project can build + and run — structurally the analogue of what the Go and TypeScript SDKs emit, + including opening its own engine session (D8). +- A single engine session shared by every client in a process. +- One idempotent serve preamble: a no-op where the target module is already + served, a real bootstrap where it is not, with no context-dependent branch in + the generated code. + +## Non-goals + +- **No compatibility shim.** `io.dagger.client.Container` and friends move. There + is no alias package, no deprecation window, no dual-mode generator. Breaking + compatibility is in scope and intended. +- **No cross-module type composition between two dependency clients.** See D1. +- **No engine changes.** Everything this needs already exists on + `v1.0.0-beta.10`. If a gap appears, it is a separate `dagger/dagger` proposal, + not a patch in this series. +- **No published Maven artifacts.** The SDK stays self-contained and vendored, as + the README describes. A standalone client vendors what it needs. +- **No CLI auto-download.** The SDK opens a session with the `dagger` binary + the host provides; fetching one is follow-up work (D8). +- No change to module authoring: `@Object`, `@Function`, the entrypoint, and the + two-pass pom stay as they are, and `dag().host()` in module code stays a + compile error (D7). + +## Approach + +### The unification, precisely + +A generated client is **generated bindings plus a serve preamble**, where the +preamble is idempotent. It probes whether its bound module is already present in +the session schema; if it is, the preamble does nothing, and if it is not, it +serves it. Inside a module the dependency has already been served by the engine, +so the probe short-circuits. Outside, the probe misses and the preamble serves. +The generated bytes are the same either way, because the branch is taken at +runtime against session state, not at generation time against context. + +That is the whole feature. Everything below is the mechanics of making the +generator emit one artifact instead of one flat package. + +### Package layout + +```mermaid +graph TD + subgraph handwritten["io.dagger.sdk — hand-written runtime (moved)"] + QB["QueryBuilder, Arguments, IDAble,
Scalar, Dagger, ModuleBinding"] + SUB["…engineconn · …graphql
…exception · …telemetry"] + end + subgraph core["io.dagger.core — generated, one per engine schema"] + CORE["Client (Query root)
Container, Directory, File, Service,
Workspace, TypeDef, …"] + end + subgraph clients["io.dagger.client.<module> — generated, one per bound module"] + C1["io.dagger.client.hello
Hello, HelloReport, …"] + C2["io.dagger.client.builder
Builder, BuilderOptions, …"] + end + CORE --> QB + C1 --> CORE + C2 --> CORE + C1 --> QB + C2 --> QB +``` + +- `io.dagger.sdk` is the hand-written SDK runtime, moved wholesale from + `io.dagger.client`. Its subpackages keep their relative names + (`io.dagger.sdk.engineconn`, `.exception`, `.graphql`, `.telemetry`). One class + is added: `ModuleBinding`, the serve preamble. +- `io.dagger.core` is new and holds the generated core API, including `Client` + (the `Query` root). This is the only package whose contents depend on the + engine version alone. +- `io.dagger.client.` is new, one package per bound module, holding only + the types that module contributes plus its entry point. A module named `hello` + produces `io.dagger.client.hello`. Nothing hand-written lives under + `io.dagger.client` any more, so a module name can never collide (D2). + +`io.dagger.core` referring to `io.dagger.sdk.QueryBuilder` while +`io.dagger.sdk.Dagger` refers to `io.dagger.core.Client` is a package cycle. Java +permits it and both are compiled in the same pass; it is called out here so it is +a decision rather than an accident. Removing it would mean moving `Dagger` into +the generated package, mixing hand-written code into generated output, which is +worse. + +Module names are still normalized to a legal Java package segment (lowercased, +`-` stripped), and a name that cannot be normalized fails at generation time with +a clear error. + +### Type attribution: `@sourceMap` is the partition + +The introspection JSON already says which module contributed each type and each +field: the engine emits `@sourceMap(module: "", …)` on both. Core types and +core fields carry no `module`. `dagger/dagger`'s own codegen partitions on +exactly this (`cmd/codegen/introspection/filters.go`, `isOwnedByModules`), and +this SDK already parses directives on `Type` and `Field` +(`Type.java:86`, `Field.java:84`) — only the accessor is missing. + +So the partition is exact, needs no second schema, and needs no name-prefix +heuristics: + +- a type whose `@sourceMap.module` is empty belongs to `io.dagger.core`; +- a type whose `@sourceMap.module` is `M` belongs to `io.dagger.client.`; +- a **field** whose `@sourceMap.module` is `M`, on a type that belongs to core, + is a module extension of a core type (`Query.hello`, `Binding.asHello`) and + belongs to `M`, not to core. + +This is verified against a real schema, not assumed. Dumping +`clientSchemaIntrospectionJSON` for `.dagger/modules/e2e` (which declares a +`java-sdk` dependency) on the pinned engine gives 124 types, of which: + +- exactly one type carries `@sourceMap(module: "e2e")` — `E2E`; +- eight *fields* carry it — the seven `@check` functions on `E2E`, plus + **`Query.e2E`**, which is precisely the "module extension on a core type" case + the partition has to handle; +- `JavaSdk` and `Mod` — the dependency's types — are **absent**, confirming D1 + empirically as well as from the source; +- `Container`, `Directory`, `File`, `Service`, `Workspace` and `Host` are all + present and unhidden. + +Two implementation details fall out of that dump and are easy to get wrong: + +- the directive argument is a **JSON-quoted** string (`"\"e2e\""`), so + `getSourceMapModule` must strip the surrounding quotes exactly as the existing + `Directive.getExpectedType` already does; +- **the module's root type name cannot be derived by capitalizing the module + name.** Module `e2e` has root type `E2E`, not `E2e`. The root type must be read + off the schema — it is the return type of the `Query` field owned by that + module (`Query.e2E` → `E2E`). Generating the name by string manipulation + produces a type that does not exist. + +**Module-owned fields on core types need somewhere to go.** `Query.e2E` is the +entry point and is handled by the factory, but the engine also lets a module +extend `Binding` and `Env` — `cmd/codegen/introspection/filters.go:5` lists the +extendable types as `Query`, `Binding` **and** `Env` (the earlier draft of this +document said "just Query", which was wrong). Java has no extension methods, so +`Binding.asHello()` has no home in `io.dagger.client.hello` and no business in +`io.dagger.core`. + +These are emitted as **static shims on the module's entry-point class**: + +```java +public static Hello asHello(io.dagger.core.Binding binding) { + return new Hello(binding.queryBuilder().chain("asHello")); +} +``` + +Without this rule the partition silently deletes the whole LLM/agent surface for +module types (`Binding.asHello`, `Env.withHelloInput`, `LLM.hello`). Dropping +them would be a capability loss disguised as a partition detail, so it is made +an explicit emission rule with its own test. + +Field-level attribution is applied to *every* type, not only to `Query`. +`dagger/dagger`'s Go filter restricts field filtering to an `ExtendableTypes` +list containing just `Query`, which leaves a module-contributed `Binding.asHello` +in the core partition while its return type is filtered out of it. Java cannot +tolerate that — it is a compile error, not a soft inconsistency — so the stricter +rule is used here. + +### Generation modes + +`DaggerCodegenMojo` gains a mode and a target package. Both modes read one +`clientSchemaIntrospectionJSON` — core plus exactly one module: + +| mode | emits | into | +|---|---|---| +| `core` | every type and field with no owning module, plus the non-schema emissions `Version` and `JsonConverter` | `io.dagger.core` | +| `client` | every type and field owned by module `M`, plus `M`'s entry point and its core-type shims | `io.dagger.client.` | + +`Version` (`VersionVisitor`) and `JsonConverter` (`IDAbleVisitor`) are not schema +types, so they do not fall out of the partition and would otherwise be emitted +into *every* package. `JsonConverter` in particular is imported by name by the +annotation processor, so a duplicate in a client package is an ambiguous import. +Both are core-mode only. + +In `client` mode, references to non-owned types resolve to `io.dagger.core` +rather than to the local package. A `TypeRegistry`, built once from the schema +partition, replaces every `ClassName.bestGuess(simpleName)` in the visitors and — +critically — in `TypeRef`, which is the actual type-reference resolver. That +substitution is the bulk of the codegen change and is mechanical. + +Because `core` mode drops everything with an owning module, the core package it +emits is identical no matter which module's client schema it was derived from. +That is what makes `io.dagger.core` shareable, and it is asserted by a test +rather than assumed. + +### The serve preamble + +The entry point generated into `io.dagger.client.` is a static factory on the +module's root type, plus the D3 alias: + +```java +package io.dagger.client.hello; + +public class Hello { + public static Hello from(io.dagger.core.Client dag) { + QueryBuilder qb = dag.queryBuilder(); + ModuleBinding.ensureServed(qb, "hello", "Hello", "LOCAL_SOURCE", "dagger/modules/hello", ""); + return new Hello(qb.chain("hello")); + } + + /** Alias for {@link #from}, for use with a static import. */ + public static Hello hello(io.dagger.core.Client dag) { + return from(dag); + } + … +} +``` + +Java has no extension methods, so `dag().hello()` would require regenerating the +core `Client` per bound module — which would make core non-shareable and the +client non-identical. The static factory is the cost of the language. + +The five baked values are the bound module's identity, and they depend only on +that module — never on the consumer. That is why the emitted bytes are identical +in every context. They come off the module source exactly as the Go SDK reads +them (`moduleOriginalName`, `kind`, the ref, `asString`, `pin`). + +`ModuleBinding.ensureServed` is hand-written runtime, so the generated code +carries data and no logic. It **serves on the first call and remembers the exact +tuple it served, per session** — there is no probe: + +```mermaid +sequenceDiagram + autonumber + participant App as caller + participant MB as ModuleBinding + participant E as engine session + App->>MB: ensureServed(name, kind, ref, pin) + alt kind = GIT_SOURCE + MB->>E: moduleSource(ref, refPin: pin).withName(name).asModule().serve() + else local + MB->>E: currentWorkspace().moduleSource(ref).withName(name).asModule().serve() + end + E-->>MB: ok (same identity already served -> dedup) + E-->>MB: error (same name, different source) +``` + +An earlier draft probed `{ __type(name: rootType) { name } }` first and skipped +the serve when the type was present. That is removed, because the engine already +does the right thing and does it atomically. `Server.serveModule` +(`engine/server/session.go:1960`) looks the module up by name and: + +- if it is **not** served, serves it; +- if it **is** served from the same source and pin, `isSameModuleReference` + matches and the call succeeds — `With` "handles deduplication and promotion + internally"; +- if it is served from a *different* source, it returns + `module %s ... already exists with different source %s`. + +So unconditional serving is idempotent for free, and the probe was strictly +worse than useless: `__type` only proves a type *name* exists, so it would skip +serving when a **different** module of the same name was already present — +silently binding the caller to the wrong module and suppressing exactly the +conflict the engine is there to report. The `Module.serve` doc comment saying +"once per session" is stale relative to this implementation. + +Dropping the probe also removes the need for `QueryBuilder` to express a raw +`__type` query (it cannot — it only builds `{field{field}}` chains). The +preamble is now pure data plus one engine call. + +**The guard that stays is a cache of successfully served tuples, keyed on the +session.** `ModuleBinding` holds a weak map from `GraphQLClient` to the set of +`(name, kind, ref, pin)` tuples that have been served on it, and skips a repeat +of an exact tuple. That is not the probe wearing another hat, and the difference +is the reason it is safe: the probe would have skipped a serve *before* the +engine had ever been asked about that name, so a different module already served +under it went unreported. The cache only ever skips a serve the engine has +already accepted for that exact source and pin — a conflict would have errored +on the first call — so nothing it suppresses could have failed. Without it every +entry-point call in a module pays a round trip on a serve the engine has already +deduplicated, which is the common case, not the rare one. + +The bound module's **final** name — after any `withName` alias — is what gets +baked and what the serve applies. The engine applies dependency aliases with +`withName` (`core/modulesource.go:1978`) and namespaces the schema by the final +name (`core/gqlformat.go:36`), so a client generated for a dependency aliased to +`alias` chains `alias` and must serve under `alias` too. Using +`moduleOriginalName` here would generate a client that chains one name while +serving another — a runtime wrong answer, not merely different bytes. + +Local bindings bake the module's **workspace-relative** path, resolved through +`currentWorkspace().moduleSource(path)` — never a cwd-relative or absolute host +path. A local binding does not survive being shipped away from the workspace; a +git binding does. That limitation is the engine's and is repeated in the +generated javadoc rather than papered over. + +### Where the schemas and identities come from + +All of it is reachable from dang today, with no engine change: + +```mermaid +graph LR + MS["ws.moduleSource(modPath)"] -->|introspectionSchemaJSON| CORESCHEMA["module-facing:
core + deps, self absent"] + MS -->|dependencies| DEPS["[ModuleSource!]!"] + DEPS -->|clientSchemaIntrospectionJSON| DEPSCHEMA["core + dep"] + DEPS -->|moduleName, kind, sourceRootSubpath, asString, pin| IDENT["baked binding identity"] + CORESCHEMA -->|mode=core| P1["io.dagger.core"] + DEPSCHEMA -->|mode=client| P2["io.dagger.client.<dep>"] + IDENT --> P2 + P1 --> STAGE["staged workspace:
runtime + core + deps + entrypoint"] + P2 --> STAGE + STAGE -->|"moduleSource(modPath).clientSchemaIntrospectionJSON"| SELFSCHEMA["core + self"] + SELFSCHEMA -->|mode=client| P3["io.dagger.client.<self>"] +``` + +`ModuleSource.dependencies` returns `[ModuleSource!]!` with dependency aliases +already applied, so each dependency's client schema, its **final** name +(`moduleName`), and its identity (`kind`, `sourceRootSubpath` for a local +module, `asString` and `pin` for git) come straight off the graph. The self +client comes from the staged workspace per D5. + +One codegen invocation handles every package. `DaggerCodegenMojo` reads a +**plan directory** — `//schema.json` plus +`//meta.json` carrying `mode`, `module`, and the binding identity — +and emits all entries into one output tree in a single Maven run, so the number +of dependencies does not multiply Maven invocations. The self client is a second, +codegen-only run over a one-entry plan after the bootstrap build. The Mojo cleans +the SDK-owned generated package roots it is about to write before writing, so a +removed dependency or a renamed alias does not leave a stale package behind. + +`mod.dang:226` already stages `generateLocalDependencies(ws)` before resolving +the module source, so a local dependency's own generated output is up to date +before its client schema is read. That staging is kept. + +### What a module's tree looks like + +``` +/sdk/src/main/java/io/dagger/sdk/** runtime (vendored, moved) +/sdk/src/processor/java/** processor (vendored) +/sdk/src/generated/java/io/dagger/core/** core API +/sdk/src/generated/java/io/dagger/client//** the module's own client (D5) +/sdk/src/generated/java/io/dagger/client//** one package per declared dependency +/src/generated/java/io/dagger/gen/entrypoint/** entrypoint (unchanged) +``` + +Everything stays under the existing `sdk/src/generated/java` source root, so the +module pom needs no change. + +### What a standalone client looks like + +`generateClient(ws, module, path)` produces a plain Maven project: + +``` +/pom.xml seeded when absent, then the user's +/sdk/src/main/java/io/dagger/sdk/** runtime (vendored) +/sdk/src/generated/java/io/dagger/core/** core API +/sdk/src/generated/java/io/dagger/client//** the bound module's client +``` + +Everything generated sits under `sdk/`, exactly as in a module, so the user's +own `src/main/java` is never touched and the whole of `sdk/` can be dropped and +rewritten on every run. The pom is rendered from `client-template/` by the same +helper that renders module templates — it sits outside `templates/`, which is +the list of *module* init templates — so there is one source of truth for the +dependency list. + +`sdk/src/generated/java/io/dagger/client//**` is byte-identical to the +`sdk/src/generated/java/io/dagger/client//**` that a module depending on +`` receives. This is the feature's central claim, and it is checked +directly (see Testing). + +Stated precisely, because the unqualified version is false: the emitted bytes are +identical **for a fixed binding tuple** — final module name, source kind, +canonical ref or workspace-relative path, pin, compatibility view, schema bytes, +and generator revision. The same module resolved locally and from git is *not* +byte-identical, and should not be: a local binding bakes a workspace-relative +path and no pin, a git binding bakes a canonical ref and a pin +(`core/modulesource.go:978,991`). What the claim rules out is the *context* — who +is generating, and whether the consumer is a module or a standalone project — +mattering. That is the property worth having, and it is the one tested. + +The claim is about **content**, not about file modes, and the check normalizes +modes before comparing digests. Codegen emits one mode everywhere +(`Codegen.sdkBuilt` chmods its output, so the number of passes cannot change +it), but a `Changeset.layer` does not carry that mode through: the engine writes +a module's generated tree into the workspace at 0666/0777 while a standalone +client's lands at 0644/0755, from byte-identical 0644 input. Measured by +exporting both trees. The mode is the workspace's to decide, so the check +levels it and still compares every byte and the whole shape. + +`generateAllClient(ws)` is the `@generate` rollup over +`currentModule.asSDK(workspace: ws).clients`, the same API `generateAll` reads +the module list from (`CurrentModuleAsSDKClient{path, module, moduleSource, +pin}`). Cwd-scoped exactly as `generateAll` already is for modules: the engine +owns the list, the cwd policy, and the resolution of each bound module, so the +local-vs-git branch `go-sdk.dang:384` writes by hand is not needed here — the +`moduleSource` the engine hands back is already resolved. `initClient` seeds the +SDK-owned files for a newly registered client — for Java that is `pom.xml` (the +Go SDK needs none, so its `initClient` is empty). + +## Alternatives considered + +**Keep the merged, deps-included schema and just split packages.** Split +`io.dagger.client` into core plus one package per dependency, still generated +from `introspectionSchemaJSON`. This preserves cross-module type composition and +is less work. Rejected: the resulting packages are not clients — they cannot be +generated outside a module, because the module-facing schema exists only for a +module. It would produce a nicer version of today's problem, not the unification. + +**A per-module wider schema (core + all of that module's client-deps together).** +Keeps today's ergonomics — dependency-authored types interoperate across a +module's own clients — at the cost of a schema-sharing mechanism the engine does +not expose, and of clients whose bytes differ between the module and standalone +cases, which contradicts the central goal. Rejected per D1; it remains the escape +hatch if dependency-type crossing turns out to matter in practice, and it would +begin as a `dagger/dagger` proposal. + +What is actually lost is narrower than it looks. Core types cross freely: a +`Container` returned by client A and passed to client B is +`io.dagger.core.Container` on both sides — the same Java type, no conversion. +Only a *dependency-authored* type crossing between two different clients is +unsupported. + +**Keep the runtime in `io.dagger.client`.** Fewer files move. Rejected per D2: +it leaves transport code under a prefix that means "generated client". + +**`io.dagger.clients.` for the generated clients instead.** Reserves no names, +moves no files, and removes the collision just as completely — on the +collision criterion alone it dominates D2. Rejected anyway: `io.dagger.client` +and `io.dagger.clients` differing by one letter, with completely different +contents, is worse to read and to import than moving the transport once. + +**Move the runtime into `io.dagger.core` as well.** Would remove the package +cycle. Rejected: it conflates "the schema-derived API" with "the transport", +which are versioned by different things, and puts hand-written files inside a +generated package. + +**Serve unconditionally and ignore an "already served" error.** Fewer round +trips. Rejected: it depends on matching an engine error string, which is not a +contract. + +**Publish `io.dagger:dagger-java-core` to Maven and depend on it.** What the +TypeScript SDK does with `@dagger.io/dagger`. Rejected: it contradicts this +repository's self-contained, no-published-artifact design, and it would make +generation depend on release infrastructure that does not exist yet. + +## Affected components + +| Component | Change | +|---|---| +| `sdk/dagger-codegen-maven-plugin` | `CodeWriter` takes a package; new `TypeRegistry` and schema partition; all visitors **and `TypeRef`** resolve through the registry; `Directive.getSourceMapModule`; new entry-point emission; `DaggerCodegenMojo` gains `mode`, `package`, `module`, and binding parameters | +| `sdk/dagger-java-sdk` | package move `io.dagger.client` → `io.dagger.sdk`; new `io.dagger.sdk.ModuleBinding`; `Dagger` returns `io.dagger.core.Client`; generated-type constructors widened to public so cross-package construction works | +| `sdk/dagger-java-annotation-processor` | imports and hardcoded type names move to `io.dagger.sdk.*` / `io.dagger.core.*` | +| `mod.dang` | `generateModule` drives core generation off the module-facing schema, one client per declared dependency off each dependency's `clientSchemaIntrospectionJSON`, and the self client through the staged-workspace bootstrap; rejects dependencies declared below `v1.0.0-0` | +| `sdk/dagger-java-sdk` (`engineconn`) | `Connection.get` regains `dagger session` provisioning behind the environment path (D8) | +| `prebuilt/m2` | regenerated — `mod.dang` prefers the committed codegen plugin whenever `prebuilt/m2/io/dagger` exists, so codegen changes are inert until the plugin jar is rebuilt and committed | +| `client.dang` (new) | client generation: schema, identity, vendoring, pom | +| `main.dang` | `generateClient`, `generateAllClient` (`@generate`), `initClient` | +| `templates/{default,empty,legacy}` | imports move | +| `sdk/dagger-java-samples` | imports move | +| `.dagger/modules/e2e` | new fixtures and checks (below) | +| `README.md` | the layout section and the generation description | + +## Testing + +What exists, exactly. + +Unit, in `dagger-codegen-maven-plugin` (`mvn -Ptests --projects +dagger-codegen-maven-plugin test`): + +- `SchemaPartitionTest` — a fixture schema with `@sourceMap` on types and on + fields splits into the expected core and module sets, including a + module-contributed field on a core type (`Binding.asHello` goes to the module, + `Binding` stays in core); core is the same whichever module the schema was + bound to; narrowing does not mutate the schema it came from; `Version` and the + IDAble helpers are core-only. +- `SourceMapAttributionTest` — the directive accessor, including the + JSON-quoted value and the field-on-a-core-type case. +- `GeneratorTest` — a plan emits core and one package per client into one tree; + core's *bytes* do not depend on which module's schema they came from; a full + plan drops the client packages it does not mention except the kept one, and a + plan without core touches nothing else; two modules naming one package are + rejected; a plan holds at most one core. +- `ModuleClientCodegenTest` — the `from(Client)` factory with the module's + constructor arguments, the static-import alias, a git binding's ref and pin, a + shim on `Binding` (with its preamble starting at the session root), the root + type read off the schema (`e2e`/`E2E`), a module named after a core type + rejected, two shims of one field name getting helper classes of their own, + schema arguments escaped where they would shadow a generated local, package + segments, and that the emitted client compiles against stubs of core and the + runtime. +- `NullableObjectCodegenTest`, `SchemaTest`, `DaggerCLIUtilsTest` — the + pre-existing nullable-object surface, the version gate, and `dagger version` + parsing. + +Unit, in `dagger-java-sdk` (needs `-Ddaggerengine.schema`): + +- `ModuleBindingTest` — over a fake engine: a local module served by workspace + path under its final name, a git module by canonical ref and pin, an unpinned + git module, a binding served once per client and again for a different name or + a second client, and a source kind a client cannot serve rejected before any + request. +- `QueryBuilderTest` — `root()` drops the selection and keeps the session, plus + the nullable-object query shapes. +- `CLISessionTest` — the announcement is parsed, the process is stopped by + `close()` and by a failure to read the announcement, a CLI that exits without + announcing and a missing CLI are explained. + +e2e, as `@check` functions in `.dagger/modules/e2e` — three, each running real +generation in the engine: + +- `clients-generate-check` — generating fixture module `app` (which declares + `dep`, and `dep` again aliased to `greeter`) from nothing produces + `io.dagger.core`, both dependency clients and app's own client; `Host` is + absent, so it stays hidden from module code; the dependency client serves + `dep` by workspace path and the aliased one serves `greeter`; core types + returned by a dependency resolve to `io.dagger.core`; with the self client + vendored the module calls itself and a second generate picks the new function + up; a committed client package the plan no longer mentions is removed; and a + third generate with no edits changes nothing. +- `standalone-client-check` — a standalone client for `dep` is + **byte-identical** (`Directory.digest`, over trees levelled to one file mode — + see the byte-identity note above) to the client `app` vendors for it, + sees `Host`, is named after its directory, and builds with a plain `mvn + package` together with a `main` that uses it. +- `registered-client-check` — `initClient` seeds the pom and nothing else, the + `@generate` rollup materializes the registered client from workspace config, + and a second rollup on the applied result is an empty changeset. + +What stays untested, plainly: + +- **No generated client is invoked at runtime through the engine.** The e2e + checks compile and build; nothing calls `dep(dag()).greet("x")` and asserts + the answer. That needs committed generated fixtures or a git-bound module. + The unit tests cover the request shapes the preamble sends. +- **Git-bound dependencies and clients.** Every fixture is local. The git branch + of the binding is covered by unit tests on the emitted code and on + `ModuleBinding`, not end to end. +- **Session provisioning end to end.** `CLISessionTest` drives a fake CLI; no + check opens a real `dagger session` from a standalone client and calls + through it. + +Regression surface that must stay green: the existing e2e checks, the `sdk-sdk` +contract suite (`seeds-files`, `does-not-write-config`, `honors-custom-path`, the +`chain` generation checks), `packager:unit-tests`, and `templates:generate`. + +## Risks + +- **Blast radius.** Every generated import in every Java module changes, and D2 + moves every hand-written runtime file too. This is intended and unavoidable + given the no-shim decision, but it means a broken intermediate patch is very + visible. Mitigated by ordering the series so the tree builds at every patch and + by the two-pass pom being unchanged. +- **Java is first at deps-as-clients.** The Go SDK's module generation still uses + the engine's merged schema, so there is no reference implementation for the + half of this design that turns dependencies into clients — only for the + standalone-client half. Expect the dependency path to need more iteration. +- **`Host` and `Engine*` stay hidden from module code, and are visible to a + standalone client.** An earlier draft of this bullet had them leaking into + modules; D7 is what closes it. A module's core comes from its own + module-facing `introspectionSchemaJSON`, which hides + `TypesToIgnoreForModuleIntrospection` and `TypesHiddenFromModuleSDKs`, so + `dag().host()` in module code stays a compile error. Only a standalone + client's core comes from the client-facing schema, which hides nothing — which + is correct, because a client is allowed everything the CLI is. The e2e + generate check asserts `io/dagger/core/Host.java` is absent from a module and + present in a standalone client. +- **`serve` once-per-session.** Serving is idempotent in the engine for an exact + source and pin; the per-client cache of served tuples keeps the repeat off the + wire. `ModuleBindingTest` pins both the request shapes and the call counts. +- **Simple-name overlap.** The authored `io.dagger.modules..M` and the + generated `io.dagger.client..M`, and a module named after a core type + against `io.dagger.core`. Compiles — JavaPoet emits fully-qualified names — + and the static-import alias is the idiom that keeps call sites clean (D5). +- **Bootstrap cost.** The self client adds one engine-driven module build per + `generate`. It is the same class of cost `generateLocalDependencies` already + pays for each local dependency, and it is cached by the engine across + unchanged inputs. +- **Self serve identity.** Inside a module, the self client serves + `currentWorkspace().moduleSource()`; the engine deduplicates only + if that resolves to the same canonical reference it served the module under. + The self-client e2e check is what proves it; if it does not match, the fix is + in how the path is baked, not in the design. +- **Dependency-authored types cannot cross clients.** Accepted per D1. The + generator fails loudly at generation time when a client's schema references a + type it cannot resolve, rather than emitting code that does not compile. +- **Constructor visibility widening.** Generated types need public `QueryBuilder` + constructors for cross-package construction, which enlarges the public surface + of generated classes. Documented as internal in the generated javadoc; no + better option exists without sealing, which Java 17 does not offer across + packages. +- **A standalone client cannot deserialize IDs against its own session.** The + generated `Deserializer` nested classes resolve through `Dagger.dag()`, the + process-wide singleton, so a client opened with `Dagger.connect()` has + `JsonConverter` talking to a different session than the one it holds. This is + the pre-existing singleton model, not something this series introduces; + threading the session through deserialization is follow-up work. +- **No startup timeout on a spawned session.** `CLISession.start` reads the + CLI's stdout until it announces a port and token or exits, so a `dagger + session` that hangs before announcing hangs the caller. The CLI's own + behaviour is the bound; a deadline is follow-up work. + +# Implementation plan + +Stacked Git series on `unified-clients-lead-c699e437`, based on `upstream/main` +@ `d806484`. Every patch carries +`Signed-off-by: Yves Brissaud `. + +Ordering constraint discovered in review: the transport must become public +(D6) **before** anything is generated outside `io.dagger.client`, and the +package move must land before the generator starts emitting multiple packages. +Each patch below compiles on its own; the codegen learns the new shape while +still driven in a single-package configuration, and the switch-over is one +patch with every consumer. + +### Patch 1 — `hack/designs`: this document ✅ + +### Patch 2 — `codegen`: read `@sourceMap` module attribution ✅ + +`Directive.getSourceMapModule`, stripping the JSON quotes around the value as +`getExpectedType` does, plus `Type.getOwningModule()` and +`Field.getOwningModule()`. Tests cover the `Query.e2E` field-on-a-core-type case. + +### Patch 3 — `codegen`: partition a schema into core and one module + +`SchemaPartition`: given a schema and a module name, the core type set (types +with no owner, with owned fields removed) and the module type set (owned types, +plus owned fields on core types, which become the shims). Unit tests including +the `Binding.asHello` case and core stability across two modules. + +### Patch 4 — `codegen`: resolve type references through a registry + +`TypeRegistry` maps a GraphQL type name to a `ClassName`. `CodeWriter` takes a +target package. Every `ClassName.bestGuess(...)` goes through the registry — in +`ObjectVisitor`, `InterfaceVisitor`, `InputVisitor`, `ScalarVisitor`, +`IDAbleVisitor`, `Helpers`, and **`TypeRef`** (the actual resolver). Behaviour +unchanged: the registry maps everything to one package. + +### Patch 5 — `sdk`: widen the query transport to public API (D6) + +`QueryBuilder` (class, constructor, `chain`, `chainNode`, `execute*`), +`InputValue`, `Arguments.merge`, `Scalar.convert()`, `QueryPart`, and a new +public `Client.queryBuilder()` accessor. Generated `Client` constructors widen so +`AutoCloseableClient` can extend across packages. No package has moved yet, so +this patch is pure visibility plus one accessor, and the existing tests pin the +behaviour. + +### Patch 6 — `sdk`: move the runtime to `io.dagger.sdk` (D2) + +The package move, mechanical, with every in-repo consumer updated in the same +patch (SDK, processor, samples, templates). No behaviour change; the generated +package is still `io.dagger.client`. + +### Patch 7 — `sdk`: `ModuleBinding`, the unconditional serve preamble, and session provisioning (D8) + +Landed before the entry-point codegen (they are swapped relative to the first +draft) so that the generated code never references a runtime class that does +not exist yet. + +### Patch 8 — `codegen`: emit the entry point, the alias, and the core-type shims + +The static `from(Client)` factory, the D3 `(Client)` alias, the static +shims for module-owned fields on `Query`/`Binding`/`Env`, and module-name +normalization. The root type is resolved **from the schema** — the return type of +the `Query` field owned by the module — never by capitalizing the module name, +which gives `E2e` where the real type is `E2E`. Binding identity uses the +module's **final** name, so an aliased dependency chains and serves the same +name. Unit tests pin the local binding, the git binding, the `e2e`/`E2E` case, +and an aliased binding. + +Hand-written `io.dagger.sdk.ModuleBinding`: no probe — serve the exact binding, +let the engine deduplicate, and remember the tuple per session so a repeat costs +nothing. Applies `withName(finalName)`. Unit tests over a faked engine cover +local, git, alias and repeat calls, and assert the request count so the +once-per-client rule is observed rather than inferred. + +In the same patch, `Connection.get` regains `dagger session` provisioning +(`ProcessBuilder`, no new dependency) behind the environment path, and the +connection shuts the session process down. A unit test drives it with a fake +`dagger` script that prints the announcement line. + +### Patch 9 — the cutover + +One patch, because the tree cannot build between halves: + +- `DaggerCodegenMojo` reads a plan directory (one entry per package, each with + its schema and `meta.json`) and cleans the package roots it writes; a plan + that carries core also drops every client package it does not mention, + except the one named by `keep` — the module's own previous self client, + carried through the first pass so module code that already calls it still + compiles; the single-schema form it accepts today becomes a one-entry core + plan; +- `io.dagger.core` is generated from the consumer's schema (D7); +- the annotation processor's hardcoded type names move to `io.dagger.core`; +- templates and samples move. + +### Patch 10 — `prebuilt`: regenerate the committed codegen plugin + +`mod.dang` prefers `prebuilt/m2` whenever it exists, so every codegen change +above is inert in module generation until the plugin jar is rebuilt and +committed. `packager:generate` produces it; this patch commits the result. + +### Patch 11 — `mod.dang`: generate core, one client per dependency, and the self client + +`generateModule` builds a plan (core from the module-facing schema, one +`client` entry per `modSource.dependencies`), vendors the result with the +previous self client carried over, produces the entrypoint, stages everything +onto the workspace, reads the module's own client schema off the staged +workspace, and generates the self client from a one-entry plan (D5). Rejects a +dependency declared below `v1.0.0-0`. Keeps the existing +`generateLocalDependencies` staging. + +### Patch 12 — `client.dang` + `main.dang`: standalone clients + +`generateClient(ws, module, path)`, `initClient` and the `@generate` rollup +`generateAllClient`, mirroring the Go SDK's surface. The rollup reads +`currentModule.asSDK(workspace: ws).clients` with the bound module's identity +and schema selected as data — the engine owns the list, the cwd policy and the +resolution of each bound module, exactly as it does for modules. `initClient` +seeds the pom, rendered from `client-template/` so there is one source of +truth. A second `@generate` function alongside `generateAll` is accepted by the +engine: `dagger generate java-sdk` runs both. + +### Patch 13 — `README`: layout, entry points, and a migration recipe + +With no shim, every existing Java module breaks on the next `dagger generate`. +The README carries the `sed` recipe for the import moves. + +### Patch 14 — `e2e`: fixtures and checks + +Two real Java modules under `fixtures/clients/` — `dep`, and `app` depending on +it — with a workspace config of their own that the checks place at the +workspace root: the engine scopes a local dependency's generation to +`Workspace.generators(include: [])` read from the root, so it has to be +the same config the SDK's module list comes from. Three checks: generation from +nothing (core, the dependency client, the self client, then a self call added +and picked up, then an idempotent run); the standalone client (byte-identical +to the vendored dependency client, sees `Host`, builds with `mvn package` and a +main that uses it); and a registered client (`initClient`, then the rollup). + +### Patch 15 — lock the shared Maven cache + +Every generate now runs two installs into the shared `~/.m2` volume, and +concurrent installs corrupt `maven-metadata-local.xml`. All mounts of that +volume use `CacheSharingMode.LOCKED`. + +### Verification + +Local, before hand-off: + +- `mvn -Ptests --projects dagger-codegen-maven-plugin test` for the codegen unit + tests. The bare `mvn -f sdk/pom.xml test` in an earlier draft **does not work**: + JUnit and AssertJ live behind the `tests` profile (`sdk/pom.xml:205`), and the + full reactor additionally needs an explicit `-Ddaggerengine.schema` and an + installed plugin, as `packager:unit-tests` does. +- `dagger check` for the e2e and packager checks; +- `dagger generate` on this repository's own modules, with an empty changeset + expected on a second run. + +CI must be green on: the existing e2e checks, `sdk-sdk` contract and chain +checks, `packager:unit-tests`, `packager:generate`, and the new e2e checks. + +## Progress + +- **Phase 0 — orientation: done.** Repository `dagger/java-sdk`, base + `upstream/main` @ `d806484` (the fork's `origin/main` is strictly behind). + Worktree + `/home/yves/.tailcall/worktrees/dagger-java-sdk-577e555c72f0/unified-clients-lead-c699e437-1cb176ae`, + branch `unified-clients-lead-c699e437`. Design home `hack/designs/`, archive + `hack/designs/done/`. VCS: StGit. Host: GitHub, fork remote `origin` = + `eunomie/java-sdk`, upstream `dagger/java-sdk`. CI: Dagger Cloud checks + (`dagger check`), no GitHub Actions workflows. Provenance: + `Signed-off-by: Yves Brissaud `, no AI attribution. +- **Phase 1/2 — feature doc and plan: done.** +- **Phase 3 — adversarial plan review: done, one round.** A Codex skeptic and a + Claude design reviewer both rejected the first draft. Verified findings folded + in above: D1 is not a regression (the engine already forbids it), the serve + probe removed in favour of unconditional serving, D6 (public transport) added + as a blocker that would otherwise not compile, D7 (core from the engine + schema) added to break a bootstrap circularity, the self client cut, alias + identity corrected, byte-identity narrowed to a stated tuple, `prebuilt/m2` + regeneration added, and the verification command fixed. +- **Kickoff answers, round 2 (Yves):** one PR, organized as an stg series; the + self client is a must-have (D5 restored with the staged-workspace bootstrap); + sessions modelled on the Go and TypeScript SDKs (D8); D7 kept. +- **Phase 5 — code review and fix: done, one round.** A Claude reviewer and a + Codex reviewer on the implemented diff; 18 curated findings (A–R) applied by + a fixer and folded into the owning patches — among them: `main.dang` is + rendered from `main.dang.tmpl` (the entry points now live in the template); + the byte-identity check compares digests after levelling file modes, which + the workspace layer rewrites differently on the two paths; static shims start + their serve from a root builder; `withoutDirectory` before every overlay, + since `Workspace.withNewDirectory` merges; a module named after a core type + and two modules that name the same package fail loudly; schema arguments + that shadow generated locals are escaped; the serve preamble caches exact + tuples per client after success; the session and connection lifecycle no + longer leaks a `dagger session` process. Full suite before the fixes: 34 of + 36 green; after: the four affected checks green, unit tests 49 / 14 / 3. +- **Phase 4 — implementation: done.** Patches 1–13 landed. The whole + pipeline has run in the real engine: the e2e generate check exercised pass 1 + (core + dependency clients), the entrypoint, the engine's bootstrap load of + the module from the staged workspace, and pass 2 (the self client). Found on + the way and fixed: generated scalar constructors were package-private and + `QueryBuilder` instantiates them reflectively from `io.dagger.sdk`; Dang folds + over engine object lists only through `{{...}}` record selections; and + concurrent Maven installs corrupt the shared m2 cache (now `LOCKED`). Codegen + 42 tests, SDK 10, processor 3, all green. +- **Known limit, to state in the PR:** no e2e invokes a generated client at + runtime through the engine — that needs committed generated fixtures or a + git-bound module. The module build and bootstrap, the compile-time API, the + identical-bytes property and the serve preamble's requests are covered. From ddee292090c747d4cebc8a2a3ea4db8329e2faa9 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Wed, 26 Aug 2026 22:19:05 +0200 Subject: [PATCH 02/17] codegen: read @sourceMap module attribution The introspection schema already records which module contributed each type and each field: the engine emits @sourceMap(module: "") on both, and core carries none. That is the exact partition a per-module client generator needs, so expose it rather than inventing a name-prefix heuristic later. The directive value arrives JSON-encoded, so it is unquoted the same way getExpectedType already does, and an empty module reads as core. Tests are shaped after a real clientSchemaIntrospectionJSON dump, which is where the interesting case lives: a module-owned field on a core type (Query.e2E, Binding.asE2E) belongs to the module while the type hosting it stays core. Signed-off-by: Yves Brissaud --- .../codegen/introspection/Directive.java | 23 ++++ .../dagger/codegen/introspection/Field.java | 8 ++ .../io/dagger/codegen/introspection/Type.java | 5 + .../SourceMapAttributionTest.java | 118 ++++++++++++++++++ 4 files changed, 154 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..36dca76 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,29 @@ public static String getExpectedType(List directives) { return null; } + /** + * Get the owning module name from a list of directives, if present. Returns the unquoted module + * name from @sourceMap(module: "foo"); null for core types and fields, which carry no module. + */ + 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) { + // The value comes as a JSON-encoded string, e.g. "\"hello\"" + if (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 47454fe..ff5c594 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 @@ -94,6 +94,14 @@ public String getExpectedType() { return Directive.getExpectedType(directives); } + /** + * Returns the module that contributed this field, or null if it belongs to core. A module-owned + * field on a core type (Query.hello, Binding.asHello) is how a module extends core. + */ + public String getOwningModule() { + return Directive.getSourceMapModule(directives); + } + boolean hasArgs() { return getArgs().size() > 0; } 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..04c51d1 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 @@ -91,6 +91,11 @@ public void setDirectives(List directives) { this.directives = directives; } + /** Returns the module that contributed this type, or null if it belongs to core. */ + public String getOwningModule() { + return Directive.getSourceMapModule(directives); + } + /** * 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/SourceMapAttributionTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java new file mode 100644 index 0000000..4fa4b29 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SourceMapAttributionTest.java @@ -0,0 +1,118 @@ +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 { + + /** + * Shaped after a real clientSchemaIntrospectionJSON dump: the module contributes its own type, + * the fields on it, and a field on the core Query type. Core types and fields carry no directive. + */ + private static final String SCHEMA = + """ + {"__schema":{"types":[ + {"name":"Query","kind":"OBJECT","fields":[ + {"name":"container","type":{"kind":"OBJECT","name":"Container"}}, + {"name":"e2E","type":{"kind":"NON_NULL","ofType":{"kind":"OBJECT","name":"E2E"}}, + "directives":[{"name":"sourceMap","args":[{"name":"module","value":"\\"e2e\\""}]}]} + ]}, + {"name":"Container","kind":"OBJECT","fields":[ + {"name":"withExec","type":{"kind":"OBJECT","name":"Container"}} + ]}, + {"name":"Binding","kind":"OBJECT","fields":[ + {"name":"asString","type":{"kind":"SCALAR","name":"String"}}, + {"name":"asE2E","type":{"kind":"OBJECT","name":"E2E"}, + "directives":[{"name":"sourceMap","args":[{"name":"module","value":"\\"e2e\\""}]}]} + ]}, + {"name":"E2E","kind":"OBJECT", + "directives":[{"name":"sourceMap","args":[{"name":"module","value":"\\"e2e\\""}]}], + "fields":[ + {"name":"initCheck","type":{"kind":"SCALAR","name":"String"}, + "directives":[{"name":"sourceMap","args":[{"name":"module","value":"\\"e2e\\""}]}]} + ]} + ]}} + """; + + @Test + void coreTypesAndFieldsHaveNoOwningModule() throws Exception { + Schema schema = parse(); + assertThat(type(schema, "Container").getOwningModule()).isNull(); + assertThat(type(schema, "Query").getOwningModule()).isNull(); + assertThat(field(schema, "Container", "withExec").getOwningModule()).isNull(); + assertThat(field(schema, "Query", "container").getOwningModule()).isNull(); + } + + @Test + void moduleTypesAreAttributedToTheirModule() throws Exception { + assertThat(type(parse(), "E2E").getOwningModule()).isEqualTo("e2e"); + } + + @Test + void moduleFieldsOnCoreTypesAreAttributedToTheModule() throws Exception { + Schema schema = parse(); + // The extension points: a module reaches core through Query and Binding. + assertThat(field(schema, "Query", "e2E").getOwningModule()).isEqualTo("e2e"); + assertThat(field(schema, "Binding", "asE2E").getOwningModule()).isEqualTo("e2e"); + // ...while the core type hosting them stays core. + assertThat(type(schema, "Binding").getOwningModule()).isNull(); + } + + @Test + void moduleFieldsOnModuleTypesAreAttributedToTheModule() throws Exception { + assertThat(field(parse(), "E2E", "initCheck").getOwningModule()).isEqualTo("e2e"); + } + + @Test + void theDirectiveValueIsUnquoted() { + assertThat( + Directive.getSourceMapModule( + java.util.List.of(directive("sourceMap", "module", "\"hello\"")))) + .isEqualTo("hello"); + } + + @Test + void anEmptyOrAbsentModuleReadsAsCore() { + assertThat( + Directive.getSourceMapModule( + java.util.List.of(directive("sourceMap", "module", "\"\"")))) + .isNull(); + assertThat( + Directive.getSourceMapModule( + java.util.List.of(directive("expectedType", "name", "\"Container\"")))) + .isNull(); + assertThat(Directive.getSourceMapModule(null)).isNull(); + } + + private static Directive directive(String name, String argName, String value) { + DirectiveArg arg = new DirectiveArg(); + arg.setName(argName); + arg.setValue(value); + Directive d = new Directive(); + d.setName(name); + d.setArgs(java.util.List.of(arg)); + return d; + } + + private static Schema parse() throws Exception { + return Schema.initialize( + new ByteArrayInputStream(SCHEMA.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.10"); + } + + private static Type type(Schema schema, String name) { + return schema.getTypes().stream() + .filter(t -> name.equals(t.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("no type " + name)); + } + + private static Field field(Schema schema, String typeName, String fieldName) { + return type(schema, typeName).getFields().stream() + .filter(f -> fieldName.equals(f.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("no field " + typeName + "." + fieldName)); + } +} From 2246f0b4373793092819b3c0d6cc42e389840709 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:20:49 +0200 Subject: [PATCH 03/17] codegen: partition a schema into core and one module A generated package is one slice of a schema: core is every unowned type with owned fields stripped, and a module's client is every type it owns plus its fields on core types. Those fields (Query.hello, Binding.asHello) are how a module extends core; they have no home of their own in Java, so the partition keeps them aside as extensions for the entry point to emit as shims rather than silently dropping the LLM/agent surface. Version and the IDAble helpers are not schema types, so they do not fall out of the partition; they belong to core alone, or every client package would carry a colliding copy. The full schema stays reachable for lookups; only the emitted types are narrowed, on copies, so the schema is never mutated. A client partition for a module the schema does not contain is an error: an empty client is a misconfiguration, never a result. Signed-off-by: Yves Brissaud --- .../dagger/codegen/introspection/Schema.java | 35 +-- .../introspection/SchemaPartition.java | 160 +++++++++++++ .../io/dagger/codegen/introspection/Type.java | 20 ++ .../introspection/SchemaPartitionTest.java | 216 ++++++++++++++++++ 4 files changed, 400 insertions(+), 31 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..9f9cd0f 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"); + /** GraphQL scalars with a direct Java counterpart, which are never generated. */ + static final List BUILTIN_SCALARS = + List.of("Boolean", "String", "Float", "Int", "DateTime"); + public static class SchemaContainer { @JsonbProperty("__schema") @@ -100,37 +104,6 @@ public Type query() { .get(); } - public void visit(SchemaVisitor visitor) { - List filteredTypes = types.stream().filter(t -> !t.getName().startsWith("_")).toList(); - - filteredTypes.stream() - .filter(t -> t.getKind() == TypeKind.SCALAR) - .filter( - t -> !List.of("Boolean", "String", "Float", "Int", "DateTime").contains(t.getName())) - .forEach(visitor::visitScalar); - - filteredTypes.stream() - .filter(t -> t.getKind() == TypeKind.INPUT_OBJECT) - .forEach(visitor::visitInput); - - filteredTypes.stream() - .filter(t -> t.getKind() == TypeKind.INTERFACE) - .forEach(visitor::visitInterface); - - filteredTypes.stream() - .filter(t -> t.getKind() == TypeKind.OBJECT) - .forEach(visitor::visitObject); - - filteredTypes.stream().filter(t -> t.getKind() == TypeKind.ENUM).forEach(visitor::visitEnum); - - visitor.visitVersion(version); - - visitor.visitIDAbles( - filteredTypes.stream() - .filter(t -> t.getKind() == TypeKind.OBJECT && t.providesId()) - .toList()); - } - @Override public String toString() { return "Schema{" + "queryType=" + queryType + ", types=" + types + '}'; 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..f0f836c --- /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.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * One package's worth of a schema: the types and fields a single generated package emits. + * + *

The engine marks every module-contributed type and field with {@code @sourceMap(module:)}; + * core carries no mark. That is the whole partition: + * + *

    + *
  • {@link #core}: every unowned type, with owned fields removed. Because everything a module + * contributed is stripped, the result does not depend on which module's schema it came from. + *
  • {@link #client}: every type owned by one module, plus that module's fields on core types — + * its {@link #extensions() extensions} of core ({@code Query.hello}, {@code + * Binding.asHello}), which have no home of their own in Java and are emitted as shims on the + * module's entry point. + *
+ * + * The full schema stays available 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 unowned part of a schema, with owned fields stripped from core types. */ + public static SchemaPartition core(Schema schema) { + List types = + emittable(schema) + .filter(type -> type.getOwningModule() == null) + .map(type -> type.withFields(unownedFields(type))) + .toList(); + return new SchemaPartition(schema, null, types, Map.of()); + } + + /** + * The part of a schema owned by {@code module}. Fails when the schema contains nothing owned by + * that module: generating an empty client is always a misconfiguration, never a result. + */ + 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 = ownedFields(type, module); + if (!owned.isEmpty()) { + extensions.put(type.getName(), owned); + } + }); + if (types.isEmpty() && extensions.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "schema contains nothing owned by module %s (owned modules: %s)", + module, ownedModules(schema))); + } + // Not Map.copyOf: the shims are emitted 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, narrowed to the fields it owns, in schema order. */ + public List types() { + return types; + } + + /** Core types carrying fields owned by this partition's module, by type name. Empty for core. */ + public Map> extensions() { + return extensions; + } + + /** Every type name owned by any module anywhere in the schema. */ + public Set ownedTypeNames() { + return emittable(schema) + .filter(type -> type.getOwningModule() != null) + .map(Type::getName) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Walk what this partition emits, in the order the generator needs. The non-schema emissions + * ({@code Version}, the IDAble helpers) belong to core alone: emitted into every client package + * they would collide with core's. + */ + 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 java.util.stream.Stream emittable(Schema schema) { + return schema.getTypes().stream().filter(t -> !t.getName().startsWith("_")); + } + + private static List unownedFields(Type type) { + if (type.getFields() == null) { + return null; + } + return type.getFields().stream().filter(f -> f.getOwningModule() == null).toList(); + } + + private static List ownedFields(Type type, String module) { + if (type.getFields() == null) { + return List.of(); + } + return type.getFields().stream().filter(f -> module.equals(f.getOwningModule())).toList(); + } + + private static List ownedModules(Schema schema) { + List modules = new ArrayList<>(); + emittable(schema) + .map(Type::getOwningModule) + .filter(Objects::nonNull) + .distinct() + .forEach(modules::add); + return modules; + } +} 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 04c51d1..01fce3c 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 @@ -96,6 +96,26 @@ public String getOwningModule() { return Directive.getSourceMapModule(directives); } + /** + * A copy of this type carrying only the given fields, which are re-parented to the copy. Used to + * narrow a type to one partition without touching the schema it came from. + */ + 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; + // The fields are shared with the schema this narrows, and re-parenting them there would + // rewrite it; the visitors only ever read the parent's name, which the copy keeps. + 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..f3aa536 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/SchemaPartitionTest.java @@ -0,0 +1,216 @@ +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 java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class SchemaPartitionTest { + + /** A client schema for module {@code hello}: core plus one module, as the engine emits it. */ + private static final String HELLO = clientSchema("hello", "Hello", "hello", "asHello"); + + /** The same core bound to a different module, to check core does not depend on the module. */ + private static final String BUILDER = clientSchema("builder", "Builder", "builder", "asBuilder"); + + @Test + void corePartitionKeepsOnlyUnownedTypes() throws Exception { + SchemaPartition core = SchemaPartition.core(parse(HELLO)); + assertThat(names(core.types())) + .containsExactly("Binding", "Container", "ID", "Query", "String"); + assertThat(core.module()).isNull(); + assertThat(core.extensions()).isEmpty(); + } + + @Test + void corePartitionStripsOwnedFieldsFromCoreTypes() throws Exception { + SchemaPartition core = SchemaPartition.core(parse(HELLO)); + assertThat(fieldNames(core, "Query")).containsExactly("container"); + assertThat(fieldNames(core, "Binding")).containsExactly("asString"); + } + + @Test + void narrowingDoesNotTouchTheSchemaItCameFrom() throws Exception { + Schema schema = parse(HELLO); + SchemaPartition.core(schema); + Type query = + schema.getTypes().stream().filter(t -> "Query".equals(t.getName())).findFirst().get(); + assertThat(query.getFields()).extracting(Field::getName).containsExactly("container", "hello"); + // container is the field the core partition keeps, so it is the one narrowing could re-parent. + assertThat(query.getFields().get(0).getParentObject()).isSameAs(query); + assertThat(query.getFields().get(1).getParentObject()).isSameAs(query); + } + + @Test + void coreIsTheSameWhicheverModuleTheSchemaWasBoundTo() throws Exception { + SchemaPartition fromHello = SchemaPartition.core(parse(HELLO)); + SchemaPartition fromBuilder = SchemaPartition.core(parse(BUILDER)); + assertThat(shape(fromHello)).isEqualTo(shape(fromBuilder)); + } + + @Test + void clientPartitionKeepsOnlyTheModulesTypes() throws Exception { + SchemaPartition client = SchemaPartition.client(parse(HELLO), "hello"); + assertThat(names(client.types())).containsExactly("Hello"); + assertThat(client.module()).isEqualTo("hello"); + assertThat(fieldNames(client, "Hello")).containsExactly("greet"); + } + + @Test + void clientPartitionCollectsTheModulesFieldsOnCoreTypesAsExtensions() throws Exception { + SchemaPartition client = SchemaPartition.client(parse(HELLO), "hello"); + assertThat(client.extensions().keySet()).containsExactlyInAnyOrder("Query", "Binding"); + assertThat(client.extensions().get("Query")) + .extracting(Field::getName) + .containsExactly("hello"); + assertThat(client.extensions().get("Binding")) + .extracting(Field::getName) + .containsExactly("asHello"); + } + + @Test + void ownedTypeNamesCoverEveryModuleInTheSchema() throws Exception { + assertThat(SchemaPartition.core(parse(HELLO)).ownedTypeNames()).containsExactly("Hello"); + assertThat(SchemaPartition.client(parse(HELLO), "hello").ownedTypeNames()) + .containsExactly("Hello"); + } + + @Test + void aClientForAModuleTheSchemaDoesNotContainIsAnError() throws Exception { + assertThatThrownBy(() -> SchemaPartition.client(parse(HELLO), "nope")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("nothing owned by module nope") + .hasMessageContaining("hello"); + } + + @Test + void introspectionTypesAreNeverEmitted() throws Exception { + assertThat(names(SchemaPartition.core(parse(HELLO)).types())).noneMatch(n -> n.startsWith("_")); + } + + @Test + void versionAndIdAbleHelpersAreEmittedByCoreOnly() throws Exception { + RecordingVisitor core = new RecordingVisitor(); + SchemaPartition.core(parse(HELLO)).visit(core); + assertThat(core.version).isEqualTo("v1.0.0-beta.10"); + assertThat(core.idAbles).containsExactly("Container"); + assertThat(core.objects).containsExactly("Binding", "Container", "Query"); + + RecordingVisitor client = new RecordingVisitor(); + SchemaPartition.client(parse(HELLO), "hello").visit(client); + assertThat(client.version).isNull(); + assertThat(client.idAbles).isNull(); + assertThat(client.objects).containsExactly("Hello"); + } + + private static final class RecordingVisitor implements SchemaVisitor { + final List objects = new ArrayList<>(); + String version; + List idAbles; + + @Override + public void visitScalar(Type type) {} + + @Override + public void visitObject(Type type) { + objects.add(type.getName()); + } + + @Override + public void visitInterface(Type type) {} + + @Override + public void visitInput(Type type) {} + + @Override + public void visitEnum(Type type) {} + + @Override + public void visitVersion(String version) { + this.version = version; + } + + @Override + public void visitIDAbles(List types) { + this.idAbles = names(types); + } + } + + private static List shape(SchemaPartition partition) { + return partition.types().stream() + .map( + t -> + t.getName() + + (t.getFields() == null + ? "" + : t.getFields().stream().map(Field::getName).toList().toString())) + .toList(); + } + + private static List names(List types) { + return types.stream().map(Type::getName).toList(); + } + + private static List fieldNames(SchemaPartition partition, String typeName) { + return partition.types().stream() + .filter(t -> typeName.equals(t.getName())) + .findFirst() + .orElseThrow() + .getFields() + .stream() + .map(Field::getName) + .toList(); + } + + private static Schema parse(String json) throws Exception { + return Schema.initialize( + new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.10"); + } + + private static String owned(String module) { + return "\"directives\":[{\"name\":\"sourceMap\",\"args\":[{\"name\":\"module\",\"value\":\"\\\"" + + module + + "\\\"\"}]}]"; + } + + private static String clientSchema(String module, String root, String entry, String binding) { + return "{\"__schema\":{\"queryType\":{\"name\":\"Query\"},\"types\":[" + + "{\"name\":\"__Schema\",\"kind\":\"OBJECT\",\"fields\":[]}," + + "{\"name\":\"String\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"ID\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"Query\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"container\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}," + + " {\"name\":\"" + + entry + + "\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}}," + + owned(module) + + "}]}," + + "{\"name\":\"Container\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"id\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"SCALAR\",\"name\":\"ID\"}}}," + + " {\"name\":\"withExec\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}]}," + + "{\"name\":\"Binding\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"asString\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}," + + " {\"name\":\"" + + binding + + "\",\"args\":[],\"type\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}," + + owned(module) + + "}]}," + + "{\"name\":\"" + + root + + "\",\"kind\":\"OBJECT\"," + + owned(module) + + ",\"fields\":[" + + " {\"name\":\"greet\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}," + + owned(module) + + "}]}" + + "]}}"; + } +} From 0a113b426d6bb893285cf33e6ca266b11bdbcbee Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:25:34 +0200 Subject: [PATCH 04/17] codegen: resolve type references through a registry Every generated reference used ClassName.bestGuess on a simple name, which is only right because everything landed in one package. Route schema types, the hand-written runtime, and the type being generated through a TypeRegistry instead, and let CodeWriter take its package from it. That is the seam a second package needs; nothing else changes here. TypeRef is the actual type-reference resolver and goes through the registry too, as do the string-interpolated class names ($L) that only worked because the name was in scope: with a package on the ClassName, $L would print it fully qualified, so they become $T. Behaviour is unchanged. The registry is built with every package set to io.dagger.client, and regenerating the vendored client from a real engine schema before and after this patch differs in exactly one way: executeQuery(java.lang.String.class) is now executeQuery(String.class), because $T elides the implicit java.lang import where $L printed the qualified name. The two in-scope references to a method's nested *Arguments class stay package-less on purpose: they name a member of the class being written, not something in another package. 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 | 93 ++++++++++++++++ .../codegen/introspection/VersionVisitor.java | 4 +- .../NullableObjectCodegenTest.java | 12 +- 16 files changed, 283 insertions(+), 158 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..4d7d972 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/TypeRegistry.java @@ -0,0 +1,93 @@ +package io.dagger.codegen.introspection; + +import com.palantir.javapoet.ClassName; +import java.util.Set; + +/** + * Where every Java class a generated package refers to lives. + * + *

Generated code names three kinds of classes: schema types, which live in the core package or — + * when a module owns them — in that module's client package; the hand-written runtime ({@code + * QueryBuilder}, {@code Arguments}, ...); and itself. Resolving all of them here, rather than by + * simple name, is what lets one generator emit into more than one package. + */ +public final class TypeRegistry { + + private final String targetPackage; + private final String corePackage; + private final String clientPackage; + private final String runtimePackage; + private final Set ownedTypeNames; + + private TypeRegistry( + String targetPackage, + String corePackage, + String clientPackage, + String runtimePackage, + Set ownedTypeNames) { + this.targetPackage = targetPackage; + this.corePackage = corePackage; + this.clientPackage = clientPackage; + this.runtimePackage = runtimePackage; + this.ownedTypeNames = Set.copyOf(ownedTypeNames); + } + + /** Everything in one package: the shape generated before packages were split. */ + public static TypeRegistry singlePackage(String pkg) { + return new TypeRegistry(pkg, pkg, pkg, pkg, Set.of()); + } + + /** Emitting the core package, with the runtime elsewhere. */ + public static TypeRegistry core(String corePackage, String runtimePackage) { + return new TypeRegistry(corePackage, corePackage, corePackage, runtimePackage, Set.of()); + } + + /** Emitting one module's client package; every type it does not own resolves to core. */ + public static TypeRegistry client( + String clientPackage, String corePackage, String runtimePackage, Set ownedTypeNames) { + return new TypeRegistry( + clientPackage, corePackage, clientPackage, runtimePackage, ownedTypeNames); + } + + /** The package this registry emits into. */ + public String targetPackage() { + return targetPackage; + } + + /** + * The Java class generated for a GraphQL type. {@code Query} is {@code Client}; the builtin + * scalars are their {@code java.lang} counterparts; a module-owned type is in its client package; + * everything else is core. + */ + 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: + String pkg = ownedTypeNames.contains(graphqlName) ? clientPackage : corePackage; + return ClassName.get(pkg, 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(runtimePackage, simpleName); + } + + /** A hand-written runtime class in a runtime subpackage. */ + public ClassName runtime(String subpackage, String simpleName) { + return ClassName.get(runtimePackage + "." + 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 4631f75..2bdaae1 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 @@ -193,14 +195,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))); } } @@ -244,9 +246,9 @@ 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 { From a9096eb7ea315cc452b08fcdcab8c20da1c4c9db Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:27:57 +0200 Subject: [PATCH 05/17] sdk: make the query transport public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated code is about to leave io.dagger.client for packages of its own, and 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, InputValue, Arguments.merge and Scalar.convert become public, the generated constructors that take a QueryBuilder or a Connection become public, and the generated Client gains a queryBuilder() accessor for code that has to start a chain from the root — the serve preamble of a generated module client, chiefly. 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. Signed-off-by: Yves Brissaud --- .../introspection/InterfaceVisitor.java | 1 + .../codegen/introspection/ObjectVisitor.java | 15 ++++++++- .../codegen/introspection/ScalarVisitor.java | 2 ++ .../main/java/io/dagger/client/Arguments.java | 2 +- .../java/io/dagger/client/InputValue.java | 3 +- .../java/io/dagger/client/QueryBuilder.java | 32 ++++++++++++------- .../main/java/io/dagger/client/Scalar.java | 2 +- 7 files changed, 41 insertions(+), 16 deletions(-) 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/ObjectVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ObjectVisitor.java index ec53087..1117f46 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 @@ -42,6 +42,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())") @@ -80,6 +81,16 @@ TypeSpec generateType(Type type) { .endControlFlow() .build()); + // queryBuilder: the root builder, for code that has to start a chain from the client — + // the serve preamble of a generated module client, chiefly. + classBuilder.addMethod( + MethodSpec.methodBuilder("queryBuilder") + .addModifiers(Modifier.PUBLIC) + .returns(registry().runtime("QueryBuilder")) + .addJavadoc("The query builder at the root of this client.\n") + .addStatement("return this.queryBuilder") + .build()); + // nodeQueryBuilder: create a QueryBuilder for node(id:) + inline fragment classBuilder.addMethod( MethodSpec.methodBuilder("nodeQueryBuilder") @@ -149,9 +160,11 @@ TypeSpec generateType(Type type) { } } - // Object constructor for query building + // Object constructor for query building. Public: a generated client package builds core + // types it returns, and a core type is loaded by ID from any package. 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/ScalarVisitor.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ScalarVisitor.java index 09d40bc..a0dee3e 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 @@ -31,8 +31,10 @@ TypeSpec generateType(Type type) { .addMember("value", "$T.class", registry().runtime("ScalarStringDeserializer")) .build()); + // Public: QueryBuilder instantiates scalars reflectively from io.dagger.sdk. MethodSpec constructor = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) .addParameter(ClassName.get(String.class), "value") .addStatement("super(value)") .build(); 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..4f07450 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 ({@code io.dagger.core}, one {@code + * io.dagger.client.} per bound module) 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 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,11 @@ private QueryBuilder( this.inlineFragmentType = inlineFragmentType; } - QueryBuilder chain(String operation) { + 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 +83,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 +93,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 +107,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 +173,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 +222,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 +232,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 +276,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 27e68c631f117900d8c2c01f85db82a1309fbf6e Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:32:11 +0200 Subject: [PATCH 06/17] sdk: move the hand-written runtime to io.dagger.sdk io.dagger.client is about to mean "a generated client": one package segment per bound module, nothing else. QueryBuilder is not a client, and leaving the transport under that prefix would make the package name a lie, so the hand-written runtime moves to io.dagger.sdk with its subpackages (engineconn, exception, graphql, telemetry) intact. Nothing generated moves yet: the vendored bindings stay in io.dagger.client, which is why Dagger and AutoCloseableClient now import the generated Client, and why the annotation processor, templates, samples, e2e fixture and README only change the imports of runtime classes. The codegen resolves the runtime through the registry already; the statements that still named QueryBuilder and Arguments as literal text become $T so the emitted import follows the package. Mechanical: every moved file is its previous content with the package and runtime imports rewritten, and the full reactor test path is green. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 4 +- sdk/README.md | 2 +- .../io/dagger/codegen/DaggerCodegenMojo.java | 2 +- .../introspection/InterfaceVisitor.java | 20 +++++++--- .../codegen/introspection/ObjectVisitor.java | 32 ++++++++++----- .../codegen/introspection/TypeRegistry.java | 5 --- .../NullableObjectCodegenTest.java | 39 +++++++++---------- .../DaggerModuleAnnotationProcessor.java | 8 ++-- .../annotation/processor/DaggerType.java | 4 +- .../annotation/processor/DaggerTypeTest.java | 6 +-- .../io/dagger/{client => sdk}/Arguments.java | 6 +-- .../{client => sdk}/AutoCloseableClient.java | 5 ++- .../io/dagger/{client => sdk}/Dagger.java | 5 ++- .../{client => sdk}/FieldsStrategy.java | 2 +- .../io/dagger/{client => sdk}/IDAble.java | 4 +- .../{client => sdk}/IDAbleSerializer.java | 4 +- .../io/dagger/{client => sdk}/InputValue.java | 2 +- .../PrivateVisibilityStrategy.java | 2 +- .../dagger/{client => sdk}/QueryBuilder.java | 16 ++++---- .../io/dagger/{client => sdk}/QueryPart.java | 4 +- .../io/dagger/{client => sdk}/Scalar.java | 2 +- .../{client => sdk}/ScalarSerializer.java | 2 +- .../ScalarStringDeserializer.java | 2 +- .../engineconn/Connection.java | 4 +- .../exception/DaggerExceptionConstants.java | 2 +- .../exception/DaggerExceptionUtils.java | 24 ++++++------ .../exception/DaggerExecException.java | 4 +- .../exception/DaggerQueryException.java | 4 +- .../graphql/GraphQLClient.java | 2 +- .../{client => sdk}/graphql/GraphQLError.java | 2 +- .../graphql/GraphQLResponse.java | 2 +- .../graphql/GraphQLValues.java | 4 +- .../{client => sdk}/telemetry/Telemetry.java | 2 +- .../telemetry/TelemetryInitializer.java | 2 +- .../telemetry/TelemetrySupplier.java | 2 +- .../telemetry/TelemetryTracer.java | 2 +- .../{client => sdk}/QueryBuilderTest.java | 4 +- .../daggermoduleplaceholder/DaggerModule.java | 4 +- .../daggermoduleplaceholder/DaggerModule.java | 4 +- 39 files changed, 131 insertions(+), 115 deletions(-) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/Arguments.java (96%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/AutoCloseableClient.java (70%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/Dagger.java (95%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/FieldsStrategy.java (94%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/IDAble.java (79%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/IDAbleSerializer.java (87%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/InputValue.java (87%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/PrivateVisibilityStrategy.java (93%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/QueryBuilder.java (96%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/QueryPart.java (87%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/Scalar.java (90%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/ScalarSerializer.java (93%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/ScalarStringDeserializer.java (94%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/engineconn/Connection.java (95%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/exception/DaggerExceptionConstants.java (95%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/exception/DaggerExceptionUtils.java (79%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/exception/DaggerExecException.java (89%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/exception/DaggerQueryException.java (88%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/graphql/GraphQLClient.java (98%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/graphql/GraphQLError.java (98%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/graphql/GraphQLResponse.java (97%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/graphql/GraphQLValues.java (92%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/telemetry/Telemetry.java (98%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/telemetry/TelemetryInitializer.java (98%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/telemetry/TelemetrySupplier.java (72%) rename sdk/dagger-java-sdk/src/main/java/io/dagger/{client => sdk}/telemetry/TelemetryTracer.java (96%) rename sdk/dagger-java-sdk/src/test/java/io/dagger/{client => sdk}/QueryBuilderTest.java (97%) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index cf989b8..a9ea388 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -235,7 +235,7 @@ type E2e { .withDirectory(".", initialized.layer) # Module-relative paths of the two artifacts generation stages. - let vendoredClient = "sdk/src/main/java/io/dagger/client/Dagger.java" + let vendoredClient = "sdk/src/main/java/io/dagger/sdk/Dagger.java" let entrypoint = "src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java" # From the module's parent the module is one level down, so its changes are @@ -301,7 +301,7 @@ type E2e { let nullableReturnSource: String! { "package io.dagger.modules.generateapp;\n" + "\n" - + "import static io.dagger.client.Dagger.dag;\n" + + "import static io.dagger.sdk.Dagger.dag;\n" + "\n" + "import io.dagger.client.Directory;\n" + "import io.dagger.module.annotation.Function;\n" diff --git a/sdk/README.md b/sdk/README.md index 92a66d3..fc257da 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -150,7 +150,7 @@ Here is a code snippet using the Dagger client package io.dagger.sample; import io.dagger.client.Client; -import io.dagger.client.Dagger; +import io.dagger.sdk.Dagger; import java.util.List; 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..32eba5b 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 @@ -65,7 +65,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { SchemaVisitor codegen = new CodegenVisitor( schema, - TypeRegistry.singlePackage("io.dagger.client"), + TypeRegistry.core("io.dagger.client", "io.dagger.sdk"), dest, Charset.forName(outputEncoding)); schema.visit( 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..c1cd312 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,15 +171,19 @@ 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( - "QueryBuilder nextQueryBuilder = this.queryBuilder.chain($S, fieldArgs)", + "$T nextQueryBuilder = this.queryBuilder.chain($S, fieldArgs)", + registry().runtime("QueryBuilder"), field.getName()); } else { fieldMethodBuilder.addStatement( - "QueryBuilder nextQueryBuilder = this.queryBuilder.chain($S)", field.getName()); + "$T nextQueryBuilder = this.queryBuilder.chain($S)", + registry().runtime("QueryBuilder"), + field.getName()); } if (field.getTypeRef().isListOfObject()) { @@ -190,7 +195,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 @@ -219,7 +226,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); 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 1117f46..aa09a10 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 @@ -45,7 +45,9 @@ TypeSpec generateType(Type type) { .addModifiers(Modifier.PUBLIC) .addParameter(registry().runtime("engineconn", "Connection"), "connection") .addStatement("this.connection = connection") - .addStatement("this.queryBuilder = new QueryBuilder(connection.getGraphQLClient())") + .addStatement( + "this.queryBuilder = new $T(connection.getGraphQLClient())", + registry().runtime("QueryBuilder")) .build(); classBuilder.addMethod(constructor); classBuilder.addField( @@ -73,9 +75,11 @@ TypeSpec generateType(Type type) { .addJavadoc("Load any object by its ID using node(id:) with an inline fragment.\n") .beginControlFlow("try") .addStatement( - "QueryBuilder qb = this.queryBuilder.chainNode(clazz.getSimpleName(), id)") + "$T qb = this.queryBuilder.chainNode(clazz.getSimpleName(), id)", + registry().runtime("QueryBuilder")) .addStatement( - "return clazz.getDeclaredConstructor(QueryBuilder.class).newInstance(qb)") + "return clazz.getDeclaredConstructor($T.class).newInstance(qb)", + registry().runtime("QueryBuilder")) .nextControlFlow("catch (Exception e)") .addStatement("throw new RuntimeException(\"Failed to load object from ID\", e)") .endControlFlow() @@ -260,7 +264,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() @@ -269,18 +274,22 @@ 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 = this.queryBuilder.chain($S, fieldArgs)", + registry().runtime("QueryBuilder"), field.getName()); } else { fieldMethodBuilder.addStatement( - "QueryBuilder nextQueryBuilder = this.queryBuilder.chain($S)", field.getName()); + "$T nextQueryBuilder = this.queryBuilder.chain($S)", + registry().runtime("QueryBuilder"), + field.getName()); } if (field.getTypeRef().isListOfObject()) { @@ -293,7 +302,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 @@ -322,7 +333,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); @@ -409,7 +421,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(); 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 4d7d972..f69e7e7 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 @@ -32,11 +32,6 @@ private TypeRegistry( this.ownedTypeNames = Set.copyOf(ownedTypeNames); } - /** Everything in one package: the shape generated before packages were split. */ - public static TypeRegistry singlePackage(String pkg) { - return new TypeRegistry(pkg, pkg, pkg, pkg, Set.of()); - } - /** Emitting the core package, with the runtime elsewhere. */ public static TypeRegistry core(String corePackage, String runtimePackage) { return new TypeRegistry(corePackage, corePackage, corePackage, runtimePackage, Set.of()); 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 2bdaae1..32f7eb8 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,7 +22,8 @@ class NullableObjectCodegenTest { - private static final TypeRegistry REGISTRY = TypeRegistry.singlePackage("io.dagger.client"); + private static final TypeRegistry REGISTRY = + TypeRegistry.core("io.dagger.client", "io.dagger.sdk"); @TempDir Path compilationOutputDirectory; @@ -134,8 +135,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 @@ -156,24 +157,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()"); @@ -220,24 +218,24 @@ private static Map supportSources() { "package io.dagger.client; public interface Animal {}", "io.dagger.client.AnimalClient", "package io.dagger.client; public class AnimalClient implements Animal {" - + " AnimalClient(QueryBuilder queryBuilder) {} }", + + " public AnimalClient(io.dagger.sdk.QueryBuilder queryBuilder) {} }", "io.dagger.client.Dog", "package io.dagger.client; public class Dog implements Animal {" - + " Dog(QueryBuilder queryBuilder) {} }", + + " public Dog(io.dagger.sdk.QueryBuilder queryBuilder) {} }", "io.dagger.client.Pet", "package io.dagger.client; public interface Pet {}", "io.dagger.client.PetClient", "package io.dagger.client; public class PetClient implements Pet {" - + " PetClient(QueryBuilder queryBuilder) {} }", - "io.dagger.client.QueryBuilder", - "package io.dagger.client; public class QueryBuilder {" - + " QueryBuilder chain(String field) { return this; }" - + " QueryBuilder executeNullableObjectQuery(String typeName)" + + " public PetClient(io.dagger.sdk.QueryBuilder queryBuilder) {} }", + "io.dagger.sdk.QueryBuilder", + "package io.dagger.sdk; public class QueryBuilder {" + + " public QueryBuilder chain(String field) { return this; }" + + " public QueryBuilder executeNullableObjectQuery(String typeName)" + " throws InterruptedException, java.util.concurrent.ExecutionException," - + " io.dagger.client.exception.DaggerQueryException { return this; }" + + " io.dagger.sdk.exception.DaggerQueryException { return this; }" + " }", - "io.dagger.client.exception.DaggerQueryException", - "package io.dagger.client.exception;" + "io.dagger.sdk.exception.DaggerQueryException", + "package io.dagger.sdk.exception;" + " public class DaggerQueryException extends Exception {}"); } @@ -247,7 +245,8 @@ private static String javaFile(TypeSpec typeSpec) { private static String generateInterface(Type type, String version) throws Exception { return javaFile( - new InterfaceVisitor(schemaAtVersion(version), REGISTRY, Path.of("."), StandardCharsets.UTF_8) + new InterfaceVisitor( + schemaAtVersion(version), REGISTRY, Path.of("."), StandardCharsets.UTF_8) .generateType(type)); } 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..d9940a7 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 @@ -11,16 +11,16 @@ import com.palantir.javapoet.MethodSpec; import com.palantir.javapoet.ParameterizedTypeName; import com.palantir.javapoet.TypeSpec; -import io.dagger.client.Dagger; +import io.dagger.sdk.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.telemetry.Telemetry; +import io.dagger.sdk.exception.DaggerExecException; +import io.dagger.sdk.exception.DaggerQueryException; +import io.dagger.sdk.telemetry.Telemetry; import io.dagger.module.annotation.Check; import io.dagger.module.annotation.Default; import io.dagger.module.annotation.DefaultPath; 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..dff3c3c 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,7 +3,7 @@ import com.palantir.javapoet.ClassName; import com.palantir.javapoet.CodeBlock; import com.palantir.javapoet.ParameterizedTypeName; -import io.dagger.client.Dagger; +import io.dagger.sdk.Dagger; import io.dagger.client.TypeDefKind; import io.dagger.module.info.TypeInfo; import java.util.Set; @@ -72,7 +72,7 @@ public static DaggerType of(TypeInfo ti) { var clazz = Class.forName(name); if (clazz.isEnum()) { return new Enum(name, name.substring(name.lastIndexOf('.') + 1)); - } else if (io.dagger.client.Scalar.class.isAssignableFrom(clazz)) { + } else if (io.dagger.sdk.Scalar.class.isAssignableFrom(clazz)) { return new Scalar(name, name.substring(name.lastIndexOf('.') + 1)); } } catch (ClassNotFoundException e) { 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..e509f05 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 @@ -15,7 +15,7 @@ void optionalObjectReturnsAreRegisteredAsOptionalAndUnwrappedForSerialization() assertThat(type.toDaggerTypeDef().toString()) .isEqualTo( - "io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); + "io.dagger.sdk.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); assertThat(type.toJavaType().toString()) .isEqualTo("java.util.Optional"); assertThat(type.valueForSerialization("result").toString()).isEqualTo("result.orElse(null)"); @@ -26,7 +26,7 @@ void nonOptionalReturnsSerializeAsThemselves() { DaggerType type = declared("io.dagger.client.Container"); assertThat(type.toDaggerTypeDef().toString()) - .isEqualTo("io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\")"); + .isEqualTo("io.dagger.sdk.Dagger.dag().typeDef().withObject(\"Container\")"); assertThat(type.valueForSerialization("result").toString()).isEqualTo("result"); } @@ -45,7 +45,7 @@ void optionalObjectFieldsAreRegisteredAsOptional() { assertThat(DaggerType.of(field.type()).toDaggerTypeDef().toString()) .isEqualTo( - "io.dagger.client.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); + "io.dagger.sdk.Dagger.dag().typeDef().withObject(\"Container\").withOptional(true)"); } private static DaggerType declared(String typeName) { 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/sdk/Arguments.java similarity index 96% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Arguments.java index 64ba306..08ff0dc 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Arguments.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Arguments.java @@ -1,7 +1,7 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.exception.DaggerQueryException; -import io.dagger.client.graphql.GraphQLValues; +import io.dagger.sdk.exception.DaggerQueryException; +import io.dagger.sdk.graphql.GraphQLValues; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; 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/sdk/AutoCloseableClient.java similarity index 70% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java index 823f0e1..0e6fed1 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/AutoCloseableClient.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java @@ -1,6 +1,7 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.engineconn.Connection; +import io.dagger.client.Client; +import io.dagger.sdk.engineconn.Connection; public class AutoCloseableClient extends Client implements AutoCloseable { AutoCloseableClient(Connection 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/sdk/Dagger.java similarity index 95% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java index 163545b..3152fd3 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Dagger.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java @@ -1,6 +1,7 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.engineconn.Connection; +import io.dagger.client.Client; +import io.dagger.sdk.engineconn.Connection; import java.io.IOException; public class Dagger { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/FieldsStrategy.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/FieldsStrategy.java similarity index 94% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/FieldsStrategy.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/FieldsStrategy.java index 00e9516..afa858e 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/FieldsStrategy.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/FieldsStrategy.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; import jakarta.json.bind.config.PropertyVisibilityStrategy; import java.lang.reflect.Field; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAble.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAble.java similarity index 79% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAble.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAble.java index 32a2072..30d68c6 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAble.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAble.java @@ -1,6 +1,6 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.exception.DaggerQueryException; +import io.dagger.sdk.exception.DaggerQueryException; import jakarta.json.bind.annotation.JsonbTypeSerializer; import java.util.concurrent.ExecutionException; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAbleSerializer.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAbleSerializer.java similarity index 87% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAbleSerializer.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAbleSerializer.java index 925b8de..66bd2d1 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/IDAbleSerializer.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/IDAbleSerializer.java @@ -1,6 +1,6 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.exception.DaggerQueryException; +import io.dagger.sdk.exception.DaggerQueryException; import jakarta.json.bind.serializer.JsonbSerializer; import jakarta.json.bind.serializer.SerializationContext; import jakarta.json.stream.JsonGenerator; 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/sdk/InputValue.java similarity index 87% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/InputValue.java index 05d5341..df9ca12 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/InputValue.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/InputValue.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; import java.util.Map; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/PrivateVisibilityStrategy.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/PrivateVisibilityStrategy.java similarity index 93% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/PrivateVisibilityStrategy.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/PrivateVisibilityStrategy.java index 0338b83..f7a20dd 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/PrivateVisibilityStrategy.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/PrivateVisibilityStrategy.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; import jakarta.json.bind.config.PropertyVisibilityStrategy; import java.lang.reflect.Field; 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/sdk/QueryBuilder.java similarity index 96% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java index 4f07450..abca4fd 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryBuilder.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java @@ -1,13 +1,13 @@ -package io.dagger.client; +package io.dagger.sdk; -import static io.dagger.client.exception.DaggerExceptionConstants.TYPE_EXEC_ERROR_VALUE; -import static io.dagger.client.exception.DaggerExceptionConstants.TYPE_KEY; +import static io.dagger.sdk.exception.DaggerExceptionConstants.TYPE_EXEC_ERROR_VALUE; +import static io.dagger.sdk.exception.DaggerExceptionConstants.TYPE_KEY; -import io.dagger.client.exception.DaggerExecException; -import io.dagger.client.exception.DaggerQueryException; -import io.dagger.client.graphql.GraphQLClient; -import io.dagger.client.graphql.GraphQLError; -import io.dagger.client.graphql.GraphQLResponse; +import io.dagger.sdk.exception.DaggerExecException; +import io.dagger.sdk.exception.DaggerQueryException; +import io.dagger.sdk.graphql.GraphQLClient; +import io.dagger.sdk.graphql.GraphQLError; +import io.dagger.sdk.graphql.GraphQLResponse; import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.json.JsonString; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryPart.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryPart.java similarity index 87% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryPart.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryPart.java index b268b63..2db7570 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/QueryPart.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryPart.java @@ -1,6 +1,6 @@ -package io.dagger.client; +package io.dagger.sdk; -import io.dagger.client.exception.DaggerQueryException; +import io.dagger.sdk.exception.DaggerQueryException; import java.util.concurrent.ExecutionException; class QueryPart { 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/sdk/Scalar.java similarity index 90% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Scalar.java index 3b4ee97..c3d52b3 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/Scalar.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Scalar.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; public class Scalar { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarSerializer.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarSerializer.java similarity index 93% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarSerializer.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarSerializer.java index 7ed0473..1390bfd 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarSerializer.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarSerializer.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; import jakarta.json.bind.serializer.JsonbSerializer; import jakarta.json.bind.serializer.SerializationContext; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarStringDeserializer.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarStringDeserializer.java similarity index 94% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarStringDeserializer.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarStringDeserializer.java index 44d9cc5..b2ff4c5 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/ScalarStringDeserializer.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ScalarStringDeserializer.java @@ -1,4 +1,4 @@ -package io.dagger.client; +package io.dagger.sdk; import jakarta.json.bind.serializer.DeserializationContext; import jakarta.json.bind.serializer.JsonbDeserializer; 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/sdk/engineconn/Connection.java similarity index 95% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/engineconn/Connection.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/Connection.java index 09be6ca..22d5035 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/sdk/engineconn/Connection.java @@ -1,6 +1,6 @@ -package io.dagger.client.engineconn; +package io.dagger.sdk.engineconn; -import io.dagger.client.graphql.GraphQLClient; +import io.dagger.sdk.graphql.GraphQLClient; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import java.io.IOException; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionConstants.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionConstants.java similarity index 95% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionConstants.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionConstants.java index 8c7f083..d3f3992 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionConstants.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionConstants.java @@ -1,4 +1,4 @@ -package io.dagger.client.exception; +package io.dagger.sdk.exception; public class DaggerExceptionConstants { public static final String CMD_KEY = "cmd"; 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/sdk/exception/DaggerExceptionUtils.java similarity index 79% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExceptionUtils.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionUtils.java index 66dc581..18db81d 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/sdk/exception/DaggerExceptionUtils.java @@ -1,15 +1,15 @@ -package io.dagger.client.exception; - -import static io.dagger.client.exception.DaggerExceptionConstants.CMD_KEY; -import static io.dagger.client.exception.DaggerExceptionConstants.ENHANCED_MESSAGE; -import static io.dagger.client.exception.DaggerExceptionConstants.EXIT_CODE_KEY; -import static io.dagger.client.exception.DaggerExceptionConstants.FULL_MESSAGE; -import static io.dagger.client.exception.DaggerExceptionConstants.SIMPLE_MESSAGE; -import static io.dagger.client.exception.DaggerExceptionConstants.STDERR_KEY; -import static io.dagger.client.exception.DaggerExceptionConstants.STDOUT_KEY; -import static io.dagger.client.exception.DaggerExceptionConstants.TYPE_KEY; - -import io.dagger.client.graphql.GraphQLError; +package io.dagger.sdk.exception; + +import static io.dagger.sdk.exception.DaggerExceptionConstants.CMD_KEY; +import static io.dagger.sdk.exception.DaggerExceptionConstants.ENHANCED_MESSAGE; +import static io.dagger.sdk.exception.DaggerExceptionConstants.EXIT_CODE_KEY; +import static io.dagger.sdk.exception.DaggerExceptionConstants.FULL_MESSAGE; +import static io.dagger.sdk.exception.DaggerExceptionConstants.SIMPLE_MESSAGE; +import static io.dagger.sdk.exception.DaggerExceptionConstants.STDERR_KEY; +import static io.dagger.sdk.exception.DaggerExceptionConstants.STDOUT_KEY; +import static io.dagger.sdk.exception.DaggerExceptionConstants.TYPE_KEY; + +import io.dagger.sdk.graphql.GraphQLError; import jakarta.json.JsonArray; import jakarta.json.JsonString; import java.util.Arrays; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExecException.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExecException.java similarity index 89% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExecException.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExecException.java index c5bf8e2..da6b223 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerExecException.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExecException.java @@ -1,6 +1,6 @@ -package io.dagger.client.exception; +package io.dagger.sdk.exception; -import io.dagger.client.graphql.GraphQLError; +import io.dagger.sdk.graphql.GraphQLError; import java.util.List; public class DaggerExecException extends DaggerQueryException { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerQueryException.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerQueryException.java similarity index 88% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerQueryException.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerQueryException.java index eb6cca6..5eba6ef 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/exception/DaggerQueryException.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerQueryException.java @@ -1,6 +1,6 @@ -package io.dagger.client.exception; +package io.dagger.sdk.exception; -import io.dagger.client.graphql.GraphQLError; +import io.dagger.sdk.graphql.GraphQLError; public class DaggerQueryException extends Exception { 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/sdk/graphql/GraphQLClient.java similarity index 98% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLClient.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLClient.java index 538980a..efc1c65 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/sdk/graphql/GraphQLClient.java @@ -1,4 +1,4 @@ -package io.dagger.client.graphql; +package io.dagger.sdk.graphql; import java.io.IOException; import java.net.URI; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLError.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLError.java similarity index 98% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLError.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLError.java index 222ffda..832bfe4 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLError.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLError.java @@ -1,4 +1,4 @@ -package io.dagger.client.graphql; +package io.dagger.sdk.graphql; import jakarta.json.JsonArray; import jakarta.json.JsonObject; 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/sdk/graphql/GraphQLResponse.java similarity index 97% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLResponse.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLResponse.java index 380518b..46f6982 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/sdk/graphql/GraphQLResponse.java @@ -1,4 +1,4 @@ -package io.dagger.client.graphql; +package io.dagger.sdk.graphql; import jakarta.json.Json; import jakarta.json.JsonArray; 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/sdk/graphql/GraphQLValues.java similarity index 92% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/graphql/GraphQLValues.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLValues.java index 90cb425..aae1b39 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/sdk/graphql/GraphQLValues.java @@ -1,4 +1,4 @@ -package io.dagger.client.graphql; +package io.dagger.sdk.graphql; import java.util.List; import java.util.Map; @@ -6,7 +6,7 @@ /** * 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 + * produced by io.dagger.sdk.Arguments: null, String, Integer, Long, Boolean, List and Map * (input objects). */ public final class GraphQLValues { 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/sdk/telemetry/Telemetry.java similarity index 98% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/Telemetry.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/Telemetry.java index 7d2ac7c..d1c9e87 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/sdk/telemetry/Telemetry.java @@ -1,4 +1,4 @@ -package io.dagger.client.telemetry; +package io.dagger.sdk.telemetry; import io.dagger.client.FunctionCall; import io.dagger.client.FunctionCallArgValue; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryInitializer.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryInitializer.java similarity index 98% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryInitializer.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryInitializer.java index cf45d2b..39ba4af 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryInitializer.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryInitializer.java @@ -1,4 +1,4 @@ -package io.dagger.client.telemetry; +package io.dagger.sdk.telemetry; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetrySupplier.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetrySupplier.java similarity index 72% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetrySupplier.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetrySupplier.java index 3254e98..b8452c9 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetrySupplier.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetrySupplier.java @@ -1,4 +1,4 @@ -package io.dagger.client.telemetry; +package io.dagger.sdk.telemetry; @FunctionalInterface public interface TelemetrySupplier { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryTracer.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryTracer.java similarity index 96% rename from sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryTracer.java rename to sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryTracer.java index c3dc5ab..f0400a7 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/client/telemetry/TelemetryTracer.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/TelemetryTracer.java @@ -1,4 +1,4 @@ -package io.dagger.client.telemetry; +package io.dagger.sdk.telemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/QueryBuilderTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java similarity index 97% rename from sdk/dagger-java-sdk/src/test/java/io/dagger/client/QueryBuilderTest.java rename to sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java index 84ed7f7..1badb2b 100644 --- a/sdk/dagger-java-sdk/src/test/java/io/dagger/client/QueryBuilderTest.java +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java @@ -1,10 +1,10 @@ -package io.dagger.client; +package io.dagger.sdk; import static org.assertj.core.api.Assertions.assertThat; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; -import io.dagger.client.graphql.GraphQLClient; +import io.dagger.sdk.graphql.GraphQLClient; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; 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..b6462a5 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,9 +1,9 @@ package io.dagger.modules.daggermoduleplaceholder; -import static io.dagger.client.Dagger.dag; +import static io.dagger.sdk.Dagger.dag; import io.dagger.client.Container; -import io.dagger.client.exception.DaggerQueryException; +import io.dagger.sdk.exception.DaggerQueryException; import io.dagger.client.Directory; import io.dagger.client.Workspace; import io.dagger.module.annotation.Default; 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..ed3c7e2 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,9 +1,9 @@ package io.dagger.modules.daggermoduleplaceholder; -import static io.dagger.client.Dagger.dag; +import static io.dagger.sdk.Dagger.dag; import io.dagger.client.Container; -import io.dagger.client.exception.DaggerQueryException; +import io.dagger.sdk.exception.DaggerQueryException; import io.dagger.client.Directory; import io.dagger.module.annotation.Function; import io.dagger.module.annotation.Object; From 378c5bc2a5dbf0e7c573e1a30b5f98bc8f5c08f5 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:35:00 +0200 Subject: [PATCH 07/17] sdk: serve bound modules, and open a session when there is none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModuleBinding.ensureServed is the serve preamble every generated module client calls before its bindings can resolve: moduleSource(ref, pin) for a git module, currentWorkspace.moduleSource(path) for a local one, then withName(finalName).asModule.serve. It serves unconditionally. The engine keys served modules by name, deduplicates a repeat of the same source and pin, and rejects a different source under the same name — so a probe would only ever hide the one conflict worth reporting, and a cache would only ever race. Inside a module, where the engine has already served every dependency and the module itself, the call is a no-op by the engine's own rule; in a standalone client it is the bootstrap. The name is the module's final one, after any dependency alias, because that is what namespaces its types. Connection.get opens a session again when the environment has none: DAGGER_SESSION_PORT/TOKEN first, else `dagger session` from _EXPERIMENTAL_DAGGER_CLI_BIN or the PATH, parsed from its announcement line and stopped with the connection (and at JVM exit, for the global client that is never closed). The Go and TypeScript SDKs do the same and additionally download a CLI when none is found; that is left out, like a test framework that uses the Docker the host has. ProcessBuilder is enough, so the fluent-process dependency that the old CLIRunner needed stays out. Signed-off-by: Yves Brissaud --- .../src/main/java/io/dagger/sdk/Dagger.java | 2 +- .../java/io/dagger/sdk/ModuleBinding.java | 85 ++++++++ .../main/java/io/dagger/sdk/QueryBuilder.java | 13 ++ .../io/dagger/sdk/engineconn/CLISession.java | 200 ++++++++++++++++++ .../io/dagger/sdk/engineconn/Connection.java | 51 +++-- .../test/java/io/dagger/sdk/FakeEngine.java | 58 +++++ .../java/io/dagger/sdk/ModuleBindingTest.java | 87 ++++++++ .../java/io/dagger/sdk/QueryBuilderTest.java | 62 ++---- .../dagger/sdk/engineconn/CLISessionTest.java | 85 ++++++++ 9 files changed, 584 insertions(+), 59 deletions(-) create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ModuleBinding.java create mode 100644 sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/CLISession.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/FakeEngine.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/ModuleBindingTest.java create mode 100644 sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/engineconn/CLISessionTest.java diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java index 3152fd3..4c0dfa5 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java @@ -15,7 +15,7 @@ public class Dagger { * * @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/sdk/ModuleBinding.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ModuleBinding.java new file mode 100644 index 0000000..76f3127 --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/ModuleBinding.java @@ -0,0 +1,85 @@ +package io.dagger.sdk; + +import io.dagger.sdk.exception.DaggerQueryException; +import io.dagger.sdk.graphql.GraphQLClient; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +/** + * The serve preamble of a generated module client. + * + *

A generated client is bound to one module. Before its bindings can resolve, that module has to + * be served into the session, and {@link #ensureServed} does exactly that. The engine keys served + * modules by name and deduplicates a serve of the same source and pin, and rejects one whose source + * differs, so the first serve is both idempotent and the only way to learn about a conflict. Inside + * a module the engine has already served every dependency, and the module itself, so it costs one + * round trip and changes nothing; in a standalone client it is the bootstrap. + * + *

An exact binding tuple that has been served successfully is remembered per session, so the + * second and later calls on the same client cost nothing. That is safe where a schema probe was + * not: a conflicting module of the same name would already have failed the first call, so the cache + * can only ever skip a serve the engine would have deduplicated. + * + *

The generated code carries data only: the module's final name (after any dependency alias), + * where its source lives, and how to reach it. A git module serves from its canonical ref and pin, + * which resolve from anywhere. A local module serves by its workspace-root-absolute path (leading + * "/") through {@code currentWorkspace}, so it resolves from the workspace root whatever the cwd + * is, and nowhere outside that workspace. + */ +public final class ModuleBinding { + + // Weak in the session: a client that has been closed must not pin its served set. + private static final Map> SERVED = + Collections.synchronizedMap(new WeakHashMap<>()); + + private ModuleBinding() {} + + /** + * Serve the bound module into the session this query builder is attached to. + * + * @param root the query builder at the root of the client + * @param name the module's final name, which namespaces its types in the schema + * @param kind the module source kind as the engine reports it: {@code GIT_SOURCE} or {@code + * LOCAL_SOURCE} + * @param ref the canonical git ref for a git module, the workspace-root-absolute path (leading + * "/") for a local one + * @param pin the resolved commit for a git module; ignored for a local one + */ + public static void ensureServed( + QueryBuilder root, String name, String kind, String ref, String pin) + throws ExecutionException, InterruptedException, DaggerQueryException { + Set served = + SERVED.computeIfAbsent(root.client(), client -> ConcurrentHashMap.newKeySet()); + String binding = String.join("\u0000", name, kind, ref, pin == null ? "" : pin); + if (served.contains(binding)) { + return; + } + QueryBuilder source; + switch (kind) { + case "GIT_SOURCE", "GIT" -> { + Arguments.Builder args = Arguments.newBuilder().add("refString", ref); + if (pin != null && !pin.isEmpty()) { + args.add("refPin", pin); + } + source = root.chain("moduleSource", args.build()); + } + case "LOCAL_SOURCE", "LOCAL" -> + source = + root.chain("currentWorkspace") + .chain("moduleSource", Arguments.newBuilder().add("path", ref).build()); + default -> + throw new IllegalArgumentException( + "module " + name + " has source kind " + kind + ", which a client cannot serve"); + } + source + .chain("withName", Arguments.newBuilder().add("name", name).build()) + .chain("asModule") + .chain("serve") + .executeQuery(); + served.add(binding); + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java index abca4fd..2c1ead6 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/QueryBuilder.java @@ -69,6 +69,19 @@ private QueryBuilder( this.inlineFragmentType = inlineFragmentType; } + /** + * A builder on the same session with no selection, for a query that has to start at the root + * rather than continue this one. + */ + public QueryBuilder root() { + return new QueryBuilder(this.client); + } + + /** The session this builder talks to, as the identity of that session. */ + GraphQLClient client() { + return this.client; + } + public QueryBuilder chain(String operation) { return chain(operation, Arguments.noArgs()); } diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/CLISession.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/CLISession.java new file mode 100644 index 0000000..556c79f --- /dev/null +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/CLISession.java @@ -0,0 +1,200 @@ +package io.dagger.sdk.engineconn; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@code dagger session} started by this process, 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: like a test framework using whatever Docker the host + * has, this uses whatever Dagger the host has, and says so clearly when there is none. The Go and + * TypeScript SDKs do the same, plus a download when nothing is found; that is deliberately not + * reproduced here. + */ +public final class CLISession implements AutoCloseable { + + static final Logger LOG = LoggerFactory.getLogger(CLISession.class); + + private final Process process; + private final int port; + private final String sessionToken; + private volatile Thread shutdownHook; + + private CLISession(Process process, int port, String sessionToken) { + this.process = process; + this.port = port; + this.sessionToken = sessionToken; + } + + /** The CLI to run: {@code _EXPERIMENTAL_DAGGER_CLI_BIN}, else {@code dagger} on the PATH. */ + public static String resolveCLI() { + String bin = System.getenv("_EXPERIMENTAL_DAGGER_CLI_BIN"); + return bin == null || bin.isBlank() ? "dagger" : bin; + } + + /** + * Start a session with the given CLI, rooted at {@code workingDir}, and wait for it to announce + * its port and token. + */ + public static CLISession start(String cli, Path workingDir, boolean loadWorkspaceModules) + throws IOException { + List command = new ArrayList<>(); + command.add(cli); + command.add("session"); + command.add("--label"); + command.add("dagger.io/sdk.name:java"); + command.add("--label"); + command.add("dagger.io/sdk.version:" + sdkVersion()); + if (loadWorkspaceModules) { + command.add("--load-workspace-modules"); + } + ProcessBuilder builder = + new ProcessBuilder(command) + .directory(workingDir.toFile()) + .redirectError(ProcessBuilder.Redirect.INHERIT); + Process process; + try { + process = builder.start(); + } catch (IOException e) { + throw new IOException( + "could not run `" + + cli + + " session`: no Dagger session in the environment (DAGGER_SESSION_PORT and" + + " DAGGER_SESSION_TOKEN) and no dagger CLI found; install one, or point" + + " _EXPERIMENTAL_DAGGER_CLI_BIN at it", + e); + } + LOG.debug("opening session: {}", command); + BufferedReader stdout = + new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + try { + String line; + while ((line = stdout.readLine()) != null) { + if (line.contains("session_token")) { + CLISession session = announced(process, line); + session.drain(stdout); + return session; + } + LOG.info(line); + } + } catch (IOException | RuntimeException e) { + // Nothing owns the process until a session is handed back, so it would outlive the failure. + stop(process); + throw e; + } + int exit = waitForExit(process); + throw new IOException( + "`" + cli + " session` exited with code " + exit + " before announcing a session"); + } + + /** The session a {@code {"port":…,"session_token":…}} line announces. */ + private static CLISession announced(Process process, String line) throws IOException { + try (JsonReader reader = Json.createReader(new StringReader(line))) { + JsonObject params = reader.readObject(); + if (!params.containsKey("port") || !params.containsKey("session_token")) { + throw new IOException("`dagger session` announced no port and session token: " + line); + } + return new CLISession(process, params.getInt("port"), params.getString("session_token")); + } catch (RuntimeException e) { + throw new IOException("`dagger session` announced a line this SDK cannot read: " + line, e); + } + } + + public int port() { + return port; + } + + public String sessionToken() { + return sessionToken; + } + + /** Whether the session process is still running. */ + boolean isAlive() { + return process.isAlive(); + } + + /** Stop the session. Idempotent; also runs at JVM exit so a session never outlives its owner. */ + @Override + public void close() { + removeShutdownHook(); + if (process.isAlive()) { + stop(process); + } + } + + /** Stop a session process, waiting for it so it is reaped rather than left as a zombie. */ + private static void stop(Process process) { + process.destroy(); + try { + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly().waitFor(); + } + } 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. + } + } + + // The session keeps writing to stdout after the announcement; an unread pipe would block it. + private void drain(BufferedReader stdout) { + Thread drain = + new Thread( + () -> { + try { + String line; + while ((line = stdout.readLine()) != null) { + LOG.info(line); + } + } catch (IOException ignored) { + // the process is gone + } + }, + "dagger-session-stdout"); + drain.setDaemon(true); + drain.start(); + shutdownHook = new Thread(this::close, "dagger-session-shutdown"); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + private static int waitForExit(Process process) throws IOException { + try { + return process.waitFor(); + } catch (InterruptedException e) { + stop(process); + Thread.currentThread().interrupt(); + throw new IOException("interrupted while waiting for the dagger session to exit", e); + } + } + + private static String sdkVersion() { + String version = CLISession.class.getPackage().getImplementationVersion(); + return version == null ? "dev" : version; + } +} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/Connection.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/Connection.java index 22d5035..0263f57 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/engineconn/Connection.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/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,40 +15,65 @@ public final class Connection { static final Logger LOG = LoggerFactory.getLogger(Connection.class); private final GraphQLClient graphQLClient; + private final CLISession session; Connection(GraphQLClient graphQLClient) { + this(graphQLClient, null); + } + + Connection(GraphQLClient graphQLClient, CLISession session) { this.graphQLClient = graphQLClient; + this.session = session; } public GraphQLClient getGraphQLClient() { return this.graphQLClient; } + /** Close the client, and the session too when this connection started it. */ public void close() throws Exception { - this.graphQLClient.close(); + try { + this.graphQLClient.close(); + } finally { + if (session != null) { + session.close(); + } + } } public static Connection get(String workingDir) throws IOException { return get(workingDir, false); } + /** + * Connect to the session in the environment ({@code DAGGER_SESSION_PORT} and {@code + * DAGGER_SESSION_TOKEN}, as a module runtime or {@code dagger run} provide), or start one with + * the dagger CLI when there is none. A started session is closed with the connection. + */ public static Connection get(String workingDir, boolean loadWorkspaceModules) throws IOException { String portStr = System.getenv("DAGGER_SESSION_PORT"); String sessionToken = System.getenv("DAGGER_SESSION_TOKEN"); - 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."); + if (portStr != null && sessionToken != null) { + try { + return getConnection(Integer.parseInt(portStr), sessionToken, null); + } catch (NumberFormatException nfe) { + throw new IOException("invalid port value in DAGGER_SESSION_PORT", nfe); + } } - try { - return getConnection(Integer.parseInt(portStr), sessionToken); - } catch (NumberFormatException nfe) { - throw new IOException("invalid port value in DAGGER_SESSION_PORT", nfe); + if (portStr != null || sessionToken != null) { + throw new IOException( + "DAGGER_SESSION_PORT and DAGGER_SESSION_TOKEN must be set together; only one is"); } + return fromCLI(CLISession.resolveCLI(), workingDir, loadWorkspaceModules); + } + + static Connection fromCLI(String cli, String workingDir, boolean loadWorkspaceModules) + throws IOException { + CLISession session = CLISession.start(cli, Path.of(workingDir), loadWorkspaceModules); + return getConnection(session.port(), session.sessionToken(), session); } - private static Connection getConnection(int port, String token) { + private static Connection getConnection(int port, String token, CLISession session) { // Inject OpenTelemetry context into headers Map headers = new HashMap<>(); GlobalOpenTelemetry.getPropagators() @@ -55,6 +81,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/sdk/FakeEngine.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/FakeEngine.java new file mode 100644 index 0000000..77cbbd9 --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/FakeEngine.java @@ -0,0 +1,58 @@ +package io.dagger.sdk; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.dagger.sdk.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; + +/** A GraphQL endpoint serving one canned response, so a real client can be exercised. */ +record FakeEngine(HttpServer http, GraphQLClient client, List requests) + implements AutoCloseable { + + static FakeEngine replying(String payload) throws IOException { + List requests = new CopyOnWriteArrayList<>(); + HttpServer http = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + http.createContext("/query", exchange -> respond(exchange, payload, requests)); + http.start(); + String url = "http://127.0.0.1:" + http.getAddress().getPort() + "/query"; + return new FakeEngine(http, new GraphQLClient(url, "token", Map.of()), requests); + } + + /** The last request body received. */ + String request() { + return requests.isEmpty() ? null : requests.get(requests.size() - 1); + } + + /** The GraphQL query inside the last request, unescaped. */ + String query() { + try (JsonReader reader = Json.createReader(new StringReader(request()))) { + return reader.readObject().getString("query"); + } + } + + // GraphQLClient sets no request timeout, so every path must send a response. + private static void respond(HttpExchange exchange, String payload, List requests) + throws IOException { + try (exchange) { + requests.add(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + byte[] body = payload.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("content-type", "application/json"); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + } + } + + @Override + public void close() { + client.close(); + http.stop(0); + } +} diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/ModuleBindingTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/ModuleBindingTest.java new file mode 100644 index 0000000..4068cf3 --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/ModuleBindingTest.java @@ -0,0 +1,87 @@ +package io.dagger.sdk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class ModuleBindingTest { + + private static final String SERVED = "{\"data\":{\"moduleSource\":{}}}"; + + @Test + void aLocalModuleIsServedByWorkspacePathUnderItsFinalName() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + ModuleBinding.ensureServed( + new QueryBuilder(engine.client()), "hello", "LOCAL_SOURCE", "dagger/modules/hello", ""); + assertThat(engine.query()) + .contains("currentWorkspace {moduleSource(path:\"dagger/modules/hello\")") + .contains("withName(name:\"hello\") {asModule {serve}}"); + } + } + + @Test + void aGitModuleIsServedByCanonicalRefAndPin() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + ModuleBinding.ensureServed( + new QueryBuilder(engine.client()), + "alias", + "GIT_SOURCE", + "github.com/dagger/hello", + "0123abc"); + assertThat(engine.query()) + .contains("moduleSource(refString:\"github.com/dagger/hello\"") + .contains("refPin:\"0123abc\"") + .contains("withName(name:\"alias\") {asModule {serve}}") + .doesNotContain("currentWorkspace"); + } + } + + @Test + void anUnpinnedGitModuleSendsNoPin() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + ModuleBinding.ensureServed( + new QueryBuilder(engine.client()), "hello", "GIT", "github.com/dagger/hello", ""); + assertThat(engine.query()).doesNotContain("refPin"); + } + } + + @Test + void aBindingServesOncePerClient() throws Exception { + // The first call is the one that can fail: the engine rejects a different source under the + // same name. Once it has succeeded, a repeat of the exact tuple can only be deduplicated. + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + QueryBuilder root = new QueryBuilder(engine.client()); + ModuleBinding.ensureServed(root, "hello", "LOCAL", "hello", ""); + ModuleBinding.ensureServed(root, "hello", "LOCAL", "hello", ""); + assertThat(engine.requests()).hasSize(1); + + ModuleBinding.ensureServed(root, "other", "LOCAL", "hello", ""); + assertThat(engine.requests()).hasSize(2); + } + } + + @Test + void aSecondClientServesAgain() throws Exception { + try (FakeEngine first = FakeEngine.replying(SERVED); + FakeEngine second = FakeEngine.replying(SERVED)) { + ModuleBinding.ensureServed(new QueryBuilder(first.client()), "hello", "LOCAL", "hello", ""); + ModuleBinding.ensureServed(new QueryBuilder(second.client()), "hello", "LOCAL", "hello", ""); + assertThat(first.requests()).hasSize(1); + assertThat(second.requests()).hasSize(1); + } + } + + @Test + void aSourceKindAClientCannotServeIsRejected() throws Exception { + try (FakeEngine engine = FakeEngine.replying(SERVED)) { + assertThatThrownBy( + () -> + ModuleBinding.ensureServed( + new QueryBuilder(engine.client()), "hello", "DIR_SOURCE", "/tmp/x", "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("DIR_SOURCE"); + assertThat(engine.requests()).isEmpty(); + } + } +} diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java index 1badb2b..b71fdd8 100644 --- a/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/QueryBuilderTest.java @@ -2,32 +2,33 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpServer; -import io.dagger.sdk.graphql.GraphQLClient; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; class QueryBuilderTest { + @Test + void rootDropsTheSelectionAndKeepsTheSession() throws Exception { + try (FakeEngine server = FakeEngine.replying("{\"data\":{}}")) { + QueryBuilder deep = new QueryBuilder(server.client()).chain("env").chain("bindings"); + + assertThat(deep.root().chain("currentWorkspace").buildQuery()) + .isEqualTo("query {currentWorkspace}"); + } + } + @Test void nullableObjectQueryRebuildsTheObjectFromItsId() throws Exception { - AtomicReference request = new AtomicReference<>(); - try (Server server = - Server.replying( - "{\"data\":{\"typeDef\":{\"asObject\":{\"id\":\"ObjectTypeDef@abc\"}}}}", request)) { + try (FakeEngine server = + FakeEngine.replying( + "{\"data\":{\"typeDef\":{\"asObject\":{\"id\":\"ObjectTypeDef@abc\"}}}}")) { QueryBuilder resolved = new QueryBuilder(server.client()) .chain("typeDef") .chain("asObject") .executeNullableObjectQuery("ObjectTypeDef"); - assertThat(request.get()).contains("query {typeDef {asObject {id}}}"); + assertThat(server.request()).contains("query {typeDef {asObject {id}}}"); assertThat(resolved).isNotNull(); assertThat(resolved.chain(List.of("id")).buildQuery()) .isEqualTo("query {node(id:\"ObjectTypeDef@abc\") {... on ObjectTypeDef {id}}}"); @@ -36,46 +37,15 @@ void nullableObjectQueryRebuildsTheObjectFromItsId() throws Exception { @Test void nullableObjectQueryReturnsNullWhenTheFieldIsNull() throws Exception { - AtomicReference request = new AtomicReference<>(); - try (Server server = Server.replying("{\"data\":{\"typeDef\":{\"asObject\":null}}}", request)) { + try (FakeEngine server = FakeEngine.replying("{\"data\":{\"typeDef\":{\"asObject\":null}}}")) { QueryBuilder resolved = new QueryBuilder(server.client()) .chain("typeDef") .chain("asObject") .executeNullableObjectQuery("ObjectTypeDef"); - assertThat(request.get()).contains("query {typeDef {asObject {id}}}"); + assertThat(server.request()).contains("query {typeDef {asObject {id}}}"); assertThat(resolved).isNull(); } } - - /** A GraphQL endpoint serving one canned response, so a real client can be exercised. */ - private record Server(HttpServer http, GraphQLClient client) implements AutoCloseable { - - static Server replying(String payload, AtomicReference request) throws IOException { - HttpServer http = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); - http.createContext("/query", exchange -> respond(exchange, payload, request)); - http.start(); - String url = "http://127.0.0.1:" + http.getAddress().getPort() + "/query"; - return new Server(http, new GraphQLClient(url, "token", Map.of())); - } - - // GraphQLClient sets no request timeout, so every path must send a response. - private static void respond( - HttpExchange exchange, String payload, AtomicReference request) throws IOException { - try (exchange) { - request.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); - byte[] body = payload.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().add("content-type", "application/json"); - exchange.sendResponseHeaders(200, body.length); - exchange.getResponseBody().write(body); - } - } - - @Override - public void close() { - client.close(); - http.stop(0); - } - } } diff --git a/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/engineconn/CLISessionTest.java b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/engineconn/CLISessionTest.java new file mode 100644 index 0000000..e074e8c --- /dev/null +++ b/sdk/dagger-java-sdk/src/test/java/io/dagger/sdk/engineconn/CLISessionTest.java @@ -0,0 +1,85 @@ +package io.dagger.sdk.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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class CLISessionTest { + + @TempDir Path dir; + + @Test + void parsesTheSessionAnnouncementAndStopsTheProcessOnClose() throws Exception { + Path cli = + fakeCli( + "echo \"$@\" > \"$PWD/args\"\n" + + "echo 'noise before the announcement'\n" + + "echo '{\"port\":54321,\"session_token\":\"tok\"}'\n" + + "exec sleep 30\n"); + CLISession session = CLISession.start(cli.toString(), dir, true); + assertThat(session.port()).isEqualTo(54321); + assertThat(session.sessionToken()).isEqualTo("tok"); + assertThat(session.isAlive()).isTrue(); + assertThat(Files.readString(dir.resolve("args"))) + .contains("session") + .contains("--label dagger.io/sdk.name:java") + .contains("--load-workspace-modules"); + + session.close(); + assertThat(session.isAlive()).isFalse(); + session.close(); + assertThat(session.isAlive()).isFalse(); + } + + @Test + void anAnnouncementThisSdkCannotReadLeavesNoProcessBehind() throws Exception { + Path cli = + fakeCli( + "echo $$ > \"$PWD/pid\"\n" + "echo '{\"session_token\" oops}'\n" + "exec sleep 30\n"); + assertThatThrownBy(() -> CLISession.start(cli.toString(), dir, false)) + .isInstanceOf(IOException.class) + .hasMessageContaining("cannot read"); + assertThat(isRunning(Files.readString(dir.resolve("pid")).trim())).isFalse(); + } + + @Test + void anAnnouncementWithoutAPortIsAnError() throws Exception { + Path cli = fakeCli("echo '{\"session_token\":\"tok\"}'\nexec sleep 30\n"); + assertThatThrownBy(() -> CLISession.start(cli.toString(), dir, false)) + .isInstanceOf(IOException.class) + .hasMessageContaining("no port and session token"); + } + + private static boolean isRunning(String pid) { + return ProcessHandle.of(Long.parseLong(pid)).map(ProcessHandle::isAlive).orElse(false); + } + + @Test + void aCliThatExitsWithoutAnnouncingIsAnError() throws Exception { + Path cli = fakeCli("echo 'starting' \nexit 3\n"); + assertThatThrownBy(() -> CLISession.start(cli.toString(), dir, false)) + .isInstanceOf(IOException.class) + .hasMessageContaining("exited with code 3"); + } + + @Test + void aMissingCliIsExplained() { + assertThatThrownBy(() -> CLISession.start(dir.resolve("no-such-dagger").toString(), dir, false)) + .isInstanceOf(IOException.class) + .hasMessageContaining("no dagger CLI found") + .hasMessageContaining("_EXPERIMENTAL_DAGGER_CLI_BIN"); + } + + 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 2465a1c73bb86c00cf8ef1748e3b940895597d6a Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:36:31 +0200 Subject: [PATCH 08/17] sdk: apply the formatter Formatter-only changes to files the build reformats on every run, so that later patches carry only their own edits. Signed-off-by: Yves Brissaud --- .../java/io/dagger/sdk/exception/DaggerExceptionUtils.java | 4 +--- .../src/main/java/io/dagger/sdk/graphql/GraphQLClient.java | 7 ++----- .../main/java/io/dagger/sdk/graphql/GraphQLResponse.java | 4 +--- .../src/main/java/io/dagger/sdk/graphql/GraphQLValues.java | 4 ++-- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionUtils.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionUtils.java index 18db81d..2cb3bcb 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/exception/DaggerExceptionUtils.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/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/sdk/graphql/GraphQLClient.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLClient.java index efc1c65..188fb1c 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLClient.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/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/sdk/graphql/GraphQLResponse.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLResponse.java index 46f6982..2c7d4b5 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLResponse.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/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/sdk/graphql/GraphQLValues.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLValues.java index aae1b39..09fd7e7 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLValues.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/graphql/GraphQLValues.java @@ -6,8 +6,8 @@ /** * Renders Java values as GraphQL literals. Supported inputs are the normalized argument values - * produced by io.dagger.sdk.Arguments: null, String, Integer, Long, Boolean, List and Map - * (input objects). + * produced by io.dagger.sdk.Arguments: null, String, Integer, Long, Boolean, List and Map (input + * objects). */ public final class GraphQLValues { From 3c085900f69c71c2b2dae4f082816cb4876a0c72 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:40:53 +0200 Subject: [PATCH 09/17] codegen: emit the module entry point, its alias, and core-type shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module client is more than its types. Its root type gains a static from(Client, ) that serves the bound module through ModuleBinding and returns the root, and an alias named after the module that delegates to it — the static-import form, hello(dag()), is what keeps a self call readable inside the module, where the authored Hello is already in scope. Both take the module's constructor arguments, optional ones included, exactly as the instance methods would, because they are generated by the same code: a field method now chains from a receiver, which is this for an instance method and a first parameter for a static one, and declares the serve call's exceptions when it carries that preamble. Java has no extension methods, so the fields a module adds to Binding and Env are emitted the same way, as static shims on the root type taking the core object first. Without them the whole LLM surface of a module's types would silently vanish from the partition. Every generated object exposes its query builder for that reason. The root type and the entry field are read off the schema — the return type of the one Query field the module owns — never derived from the module name, which gives E2e where the engine says E2E. The binding baked into the preamble is the module's final name, kind, ref and pin. Signed-off-by: Yves Brissaud --- .../io/dagger/codegen/DaggerCodegenMojo.java | 1 + .../codegen/introspection/ClientBinding.java | 28 +++ .../introspection/ClientEntryPoint.java | 77 ++++++ .../codegen/introspection/CodegenVisitor.java | 11 +- .../dagger/codegen/introspection/Helpers.java | 38 ++- .../codegen/introspection/ObjectVisitor.java | 195 +++++++++++++-- .../codegen/introspection/CompileSupport.java | 63 +++++ .../codegen/introspection/Fixtures.java | 132 ++++++++++ .../ModuleClientCodegenTest.java | 236 ++++++++++++++++++ .../NullableObjectCodegenTest.java | 54 +--- .../introspection/SchemaPartitionTest.java | 48 +--- 11 files changed, 758 insertions(+), 125 deletions(-) create mode 100644 sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientBinding.java 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/CompileSupport.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/Fixtures.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ModuleClientCodegenTest.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 32eba5b..1d3d0e4 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 @@ -66,6 +66,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { new CodegenVisitor( schema, TypeRegistry.core("io.dagger.client", "io.dagger.sdk"), + null, dest, Charset.forName(outputEncoding)); schema.visit( diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientBinding.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientBinding.java new file mode 100644 index 0000000..2f7a754 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientBinding.java @@ -0,0 +1,28 @@ +package io.dagger.codegen.introspection; + +/** + * Where the module a client binds to lives, as baked into the client's serve preamble. + * + * @param module the module's final name, after any dependency alias + * @param kind the engine's source kind: {@code GIT_SOURCE} or {@code LOCAL_SOURCE} + * @param ref the canonical git ref, or the workspace-root-absolute path of a local module — the + * leading "/" is what makes it resolve from the workspace root rather than from the client's + * cwd + * @param pin the resolved commit of a git module; empty for a local one + */ +public record ClientBinding(String module, String kind, String ref, String pin) { + + public ClientBinding { + if (module == null || module.isBlank()) { + throw new IllegalArgumentException("a client binding needs a module name"); + } + if (pin == null) { + pin = ""; + } + } + + /** The same binding under another final name, as a dependency alias renames a module. */ + public ClientBinding withModule(String module) { + return new ClientBinding(module, kind, ref, pin); + } +} 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..42cfbea --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/ClientEntryPoint.java @@ -0,0 +1,77 @@ +package io.dagger.codegen.introspection; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * What a module client exposes beyond its types: the static {@code from(Client)} factory on its + * root type, the static-import alias named after the module, and one static shim per field the + * module adds to a core type other than {@code Query}. + * + *

Everything here is read off the schema. The root type is 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 record ClientEntryPoint(SchemaPartition client, ClientBinding binding) { + + public ClientEntryPoint { + if (client.module() == null) { + throw new IllegalArgumentException("an entry point needs a client partition, not core"); + } + if (!client.module().equals(binding.module())) { + throw new IllegalArgumentException( + String.format( + "binding is for module %s but the partition is for %s", + binding.module(), client.module())); + } + String root = entryField(client).getTypeRef().getTypeName(); + if (client.types().stream().noneMatch(type -> root.equals(type.getName()))) { + throw new IllegalArgumentException( + String.format( + "module %s enters on the core type %s, which it does not own, so there is no client" + + " to generate: a module named after a core type collides with it. Rename the" + + " module, or alias the dependency.", + client.module(), root)); + } + } + + /** The module's constructor: the {@code Query} field it owns. */ + public Field entryField() { + return entryField(client); + } + + /** The GraphQL name of the module's root object type. */ + public String rootTypeName() { + return entryField().getTypeRef().getTypeName(); + } + + private static Field entryField(SchemaPartition client) { + List entries = client.extensions().getOrDefault("Query", List.of()); + if (entries.size() != 1) { + throw new IllegalStateException( + String.format( + "module %s owns %d fields on Query, expected exactly one: %s", + client.module(), + entries.size(), + entries.stream().map(Field::getName).collect(Collectors.toList()))); + } + return entries.get(0); + } + + /** + * Module-owned fields on core types other than {@code Query}, by type name, in the partition's + * order so the emitted shims come out the same on every run. + */ + public Map> shims() { + return client.extensions().entrySet().stream() + .filter(e -> !"Query".equals(e.getKey())) + .collect( + Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (first, second) -> first, + LinkedHashMap::new)); + } +} 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..78816c7 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,12 +15,19 @@ public class CodegenVisitor implements SchemaVisitor { private final VersionVisitor versionVisitor; private final IDAbleVisitor idAbleVisitor; + /** + * @param entryPoint the module entry point to emit on its root type, or null when generating core + */ public CodegenVisitor( - Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + Schema schema, + TypeRegistry registry, + ClientEntryPoint entryPoint, + 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, 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/Helpers.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/introspection/Helpers.java index 9e65f79..f469aac 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,6 +65,22 @@ public class Helpers { "super", "while"); + /** + * The locals a generated field method declares around the arguments it takes. A schema argument + * with one of these names shadows the local — "variable already defined" — so it is escaped + * exactly as a Java keyword is. + */ + private static final List RESERVED_LOCALS = + List.of( + "dag", + "root", + "builder", + "builders", + "fieldArgs", + "optArgs", + "nextQueryBuilder", + "objectQueryBuilder"); + static ClassName convertScalarToObject( TypeRegistry registry, String typeName, String expectedType) { if (expectedType != null && !expectedType.isEmpty()) { @@ -124,6 +140,26 @@ static List getArrayField(Field field, Schema schema) { return schemaType.getFields().stream().filter(f -> f.getTypeRef().isScalar()).toList(); } + /** + * The package segment a module's client lives under: {@code io.dagger.client.}. Only + * lowercase letters and digits survive, so {@code my-module} is {@code mymodule}; a leading digit + * or a Java keyword is escaped rather than rejected. + */ + static String packageSegment(String moduleName) { + String segment = moduleName.toLowerCase().replaceAll("[^a-z0-9]", ""); + if (segment.isEmpty()) { + throw new IllegalArgumentException( + "module name " + moduleName + " has no letters or digits to name a package with"); + } + if (Character.isDigit(segment.charAt(0))) { + segment = "_" + segment; + } + if (JAVA_KEYWORDS.contains(segment)) { + segment = segment + "_"; + } + return segment; + } + static String formatName(Type type) { return formatName(type.getName()); } @@ -148,7 +184,7 @@ static String formatName(Field field) { } static String formatName(InputObject arg) { - if (JAVA_KEYWORDS.contains(arg.getName())) { + if (JAVA_KEYWORDS.contains(arg.getName()) || RESERVED_LOCALS.contains(arg.getName())) { return "_" + arg.getName(); } else { return arg.getName(); 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 aa09a10..3cea26b 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,9 +17,46 @@ import javax.lang.model.element.Modifier; class ObjectVisitor extends AbstractVisitor { + + private final ClientEntryPoint entryPoint; + public ObjectVisitor( - Schema schema, TypeRegistry registry, Path targetDirectory, Charset encoding) { + Schema schema, + TypeRegistry registry, + ClientEntryPoint entryPoint, + Path targetDirectory, + Charset encoding) { super(schema, registry, targetDirectory, encoding); + this.entryPoint = entryPoint; + } + + /** + * Who a generated field method chains from. An instance method chains from {@code + * this.queryBuilder}; a static one takes its receiver as a first parameter and chains from that + * receiver's builder, optionally after a preamble — the serve call of a module entry point. + */ + private record Receiver(ClassName type, String name, String method, CodeBlock preamble) { + static final Receiver THIS = new Receiver(null, null, null, null); + + boolean isStatic() { + return type != null; + } + + /** + * A shim on a core type: static, and named after the schema field. The entry point is static + * too but renames itself to {@code from}. + */ + boolean isShim() { + return isStatic() && method == null; + } + + /** + * Every shim is emitted into the module's one root class, so two receivers carrying the same + * field name would name the same nested optional-arguments class. + */ + String helperPrefix() { + return isShim() ? type.simpleName() : ""; + } } @Override @@ -85,16 +122,6 @@ TypeSpec generateType(Type type) { .endControlFlow() .build()); - // queryBuilder: the root builder, for code that has to start a chain from the client — - // the serve preamble of a generated module client, chiefly. - classBuilder.addMethod( - MethodSpec.methodBuilder("queryBuilder") - .addModifiers(Modifier.PUBLIC) - .returns(registry().runtime("QueryBuilder")) - .addJavadoc("The query builder at the root of this client.\n") - .addStatement("return this.queryBuilder") - .build()); - // nodeQueryBuilder: create a QueryBuilder for node(id:) + inline fragment classBuilder.addMethod( MethodSpec.methodBuilder("nodeQueryBuilder") @@ -174,15 +201,29 @@ TypeSpec generateType(Type type) { .build(); classBuilder.addMethod(constructor); + // The builder behind this object, for code that chains from it without being it: the serve + // preamble of a module client, and the shims a module adds to core types. + 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); + buildFieldArgumentsHelpers(classBuilder, field, type, Receiver.THIS); buildFieldMethod(classBuilder, field, true); } buildFieldMethod(classBuilder, field, false); } + if (entryPoint != null && type.getName().equals(entryPoint.rootTypeName())) { + buildEntryPoint(classBuilder, type); + } + if (List.of("Container", "Directory").contains(type.getName())) { String argName = type.getName().toLowerCase() + "Func"; classBuilder.addMethod( @@ -219,10 +260,83 @@ private TypeName resolveReturnType(Field field) { return field.getTypeRef().formatInput(registry(), expectedType); } + /** + * The module entry point on its root type: {@code from(Client, )} serves the + * bound module and returns its root; the alias named after the module delegates to it, for a + * static import; and one static shim per field the module adds to a core type, taking that core + * object as its first argument, since Java has no extension methods. + */ + private void buildEntryPoint(TypeSpec.Builder classBuilder, Type type) { + ClientBinding binding = entryPoint.binding(); + CodeBlock serve = + CodeBlock.of( + "$T.ensureServed(root, $S, $S, $S, $S);\n", + registry().runtime("ModuleBinding"), + binding.module(), + binding.kind(), + binding.ref(), + binding.pin()); + Field entry = entryPoint.entryField(); + Receiver dag = new Receiver(registry().forType("Query"), "dag", "from", serve); + if (entry.hasOptionalArgs()) { + buildFieldArgumentsHelpers(classBuilder, entry, type, dag); + classBuilder.addMethod(alias(buildFieldMethod(classBuilder, entry, true, dag), entry)); + } + classBuilder.addMethod(alias(buildFieldMethod(classBuilder, entry, false, dag), entry)); + + entryPoint + .shims() + .forEach( + (typeName, fields) -> { + ClassName coreType = registry().forType(typeName); + String receiverName = + Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + Receiver receiver = new Receiver(coreType, receiverName, null, serve); + for (Field shim : fields) { + if (shim.hasOptionalArgs()) { + buildFieldArgumentsHelpers(classBuilder, shim, type, receiver); + buildFieldMethod(classBuilder, shim, true, receiver); + } + buildFieldMethod(classBuilder, shim, false, receiver); + } + }); + } + + /** The static-import alias of {@code from}: same signature, named after the module. */ + private MethodSpec alias(MethodSpec from, Field entry) { + String args = + from.parameters().stream() + .map(p -> p.name()) + .collect(java.util.stream.Collectors.joining(", ")); + return MethodSpec.methodBuilder(Helpers.formatName(entry)) + .addModifiers(from.modifiers()) + .returns(from.returnType()) + .addParameters(from.parameters()) + .addExceptions(from.exceptions()) + .addJavadoc( + "Alias for {@link #from}, for a static import: {@code $L(dag())}.\n", + Helpers.formatName(entry)) + .addStatement("return from($L)", args) + .build(); + } + private void buildFieldMethod( TypeSpec.Builder classBuilder, Field field, boolean withOptionalArgs) { + buildFieldMethod(classBuilder, field, withOptionalArgs, Receiver.THIS); + } + + private MethodSpec buildFieldMethod( + TypeSpec.Builder classBuilder, Field field, boolean withOptionalArgs, Receiver receiver) { + String methodName = receiver.method() != null ? receiver.method() : Helpers.formatName(field); MethodSpec.Builder fieldMethodBuilder = - MethodSpec.methodBuilder(Helpers.formatName(field)).addModifiers(Modifier.PUBLIC); + MethodSpec.methodBuilder(methodName).addModifiers(Modifier.PUBLIC); + if (receiver.isStatic()) { + fieldMethodBuilder.addModifiers(Modifier.STATIC); + fieldMethodBuilder.addParameter( + ParameterSpec.builder(receiver.type(), receiver.name()) + .addJavadoc("the $L to chain from\n", receiver.type().simpleName()) + .build()); + } TypeName returnType = resolveReturnType(field); TypeName objectReturnType = returnType; boolean nullableObject = @@ -248,21 +362,33 @@ 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, receiver), "optArgs") .addJavadoc("$L optional arguments\n", Helpers.formatName(field)) .build()); } fieldMethodBuilder.addJavadoc(Helpers.escapeJavadoc(field.getDescription())); - if (field.getTypeRef().isScalar() + if (!receiver.isStatic() + && field.getTypeRef().isScalar() && !Helpers.isIdToConvert(field) && !"Query".equals(field.getParentObject().getName())) { fieldMethodBuilder.beginControlFlow("if (this.$L != null)", Helpers.formatName(field)); fieldMethodBuilder.addStatement("return $L", Helpers.formatName(field)); fieldMethodBuilder.endControlFlow(); } + String builder = "this.queryBuilder"; + if (receiver.isStatic()) { + builder = receiver.name() + ".queryBuilder()"; + if (receiver.preamble() != null) { + // A shim's receiver is a core object mid-chain, so its builder carries a selection path + // the serve query has no business continuing. + fieldMethodBuilder.addStatement( + "$T root = $L.queryBuilder().root()", + registry().runtime("QueryBuilder"), + receiver.name()); + fieldMethodBuilder.addCode(receiver.preamble()); + } + } if (field.hasArgs()) { fieldMethodBuilder.addStatement( "$1T.Builder builder = $1T.newBuilder()", registry().runtime("Arguments")); @@ -282,13 +408,15 @@ private void buildFieldMethod( } if (field.hasArgs()) { fieldMethodBuilder.addStatement( - "$T nextQueryBuilder = this.queryBuilder.chain($S, fieldArgs)", + "$T nextQueryBuilder = $L.chain($S, fieldArgs)", registry().runtime("QueryBuilder"), + builder, field.getName()); } else { fieldMethodBuilder.addStatement( - "$T nextQueryBuilder = this.queryBuilder.chain($S)", + "$T nextQueryBuilder = $L.chain($S)", registry().runtime("QueryBuilder"), + builder, field.getName()); } @@ -321,7 +449,7 @@ 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", receiver.isStatic() ? receiver.name() : "this"); fieldMethodBuilder .addException(InterruptedException.class) .addException(ExecutionException.class) @@ -363,12 +491,22 @@ private void buildFieldMethod( .addException(registry().runtime("exception", "DaggerQueryException")); } + if (receiver.preamble() != null) { + // The serve call in the preamble can fail even when the field itself is lazy. + fieldMethodBuilder + .addException(InterruptedException.class) + .addException(ExecutionException.class) + .addException(registry().runtime("exception", "DaggerQueryException")); + } + if (field.isDeprecated()) { fieldMethodBuilder.addAnnotation(Deprecated.class); fieldMethodBuilder.addJavadoc("@deprecated $L\n", field.getDeprecationReason()); } - classBuilder.addMethod(fieldMethodBuilder.build()); + MethodSpec method = fieldMethodBuilder.build(); + classBuilder.addMethod(method); + return method; } /** @@ -378,12 +516,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, Receiver receiver) { + ClassName fieldArgumentsClassName = argumentsClass(field, receiver); /* Inner class XXXArguments */ TypeSpec.Builder fieldArgumentsClassBuilder = - TypeSpec.classBuilder(fieldArgumentsClassName) + TypeSpec.classBuilder(fieldArgumentsClassName.simpleName()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC); List optionalArgFields = field.getOptionalArgs().stream() @@ -402,7 +541,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); @@ -432,4 +571,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, Receiver receiver) { + return ClassName.bestGuess( + receiver.helperPrefix() + capitalize(Helpers.formatName(field)) + "Arguments"); + } } diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/CompileSupport.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/CompileSupport.java new file mode 100644 index 0000000..8d29ffb --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/CompileSupport.java @@ -0,0 +1,63 @@ +package io.dagger.codegen.introspection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; + +/** Compiles generated sources in memory, so a test can prove they are valid Java. */ +final class CompileSupport { + + private CompileSupport() {} + + static void assertCompiles(Path outputDirectory, Map sources) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertThat(compiler).isNotNull(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + List compilationUnits = + sources.entrySet().stream() + .map(entry -> new SourceFile(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + boolean compiled = + compiler + .getTask( + null, + null, + diagnostics, + List.of("--release", "17", "-proc:none", "-d", outputDirectory.toString()), + null, + compilationUnits) + .call(); + assertThat(compiled) + .withFailMessage( + "Generated sources did not compile:%n%s", + diagnostics.getDiagnostics().stream() + .map(Object::toString) + .collect(Collectors.joining("\n"))) + .isTrue(); + } + + private static final class SourceFile extends SimpleJavaFileObject { + private final String source; + + SourceFile(String qualifiedName, String source) { + super( + URI.create("string:///" + qualifiedName.replace('.', '/') + Kind.SOURCE.extension), + Kind.SOURCE); + this.source = source; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return source; + } + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/Fixtures.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/Fixtures.java new file mode 100644 index 0000000..032c9fd --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/Fixtures.java @@ -0,0 +1,132 @@ +package io.dagger.codegen.introspection; + +/** Introspection JSON shaped after the engine's real clientSchemaIntrospectionJSON. */ +public final class Fixtures { + + private Fixtures() {} + + /** + * A client schema for one module: core ({@code Query}, {@code Container}, {@code Binding}) plus + * the module's root type, its constructor on {@code Query} (with one required and one optional + * argument) and its accessor on {@code Binding}. + */ + static String owned(String module) { + return "\"directives\":[{\"name\":\"sourceMap\",\"args\":[{\"name\":\"module\",\"value\":\"\\\"" + + module + + "\\\"\"}]}]"; + } + + /** + * A client schema whose module contributes a {@code Query} field returning a core type and owns + * nothing of its own: what the engine emits for a module named after a core type. + */ + public static String coreNamedSchema(String module, String entry) { + return "{\"__schema\":{\"queryType\":{\"name\":\"Query\"},\"types\":[" + + "{\"name\":\"String\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"Query\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"" + + entry + + "\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}," + + owned(module) + + "}]}," + + "{\"name\":\"Container\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"withExec\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}]}" + + "]}}"; + } + + /** + * A client schema whose module adds the same field, taking an optional argument, to two core + * types: two shims that would name one nested arguments class in the module's root class. + */ + public static String twoShimsSchema(String module, String root, String entry, String shim) { + String shimField = + " {\"name\":\"" + + shim + + "\",\"args\":[{\"name\":\"tag\",\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}]," + + "\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}}," + + owned(module) + + "}"; + return "{\"__schema\":{\"queryType\":{\"name\":\"Query\"},\"types\":[" + + "{\"name\":\"String\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"Query\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"" + + entry + + "\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}}," + + owned(module) + + "}]}," + + "{\"name\":\"Container\",\"kind\":\"OBJECT\",\"fields\":[" + + shimField + + "]}," + + "{\"name\":\"Workspace\",\"kind\":\"OBJECT\",\"fields\":[" + + shimField + + "]}," + + "{\"name\":\"" + + root + + "\",\"kind\":\"OBJECT\"," + + owned(module) + + ",\"fields\":[" + + " {\"name\":\"greet\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}," + + owned(module) + + "}]}" + + "]}}"; + } + + public static String clientSchema(String module, String root, String entry, String binding) { + return clientSchema(module, root, entry, binding, "name", "greeting"); + } + + /** The same schema with the entry point's required and optional argument named as asked. */ + public static String clientSchema( + String module, + String root, + String entry, + String binding, + String requiredArg, + String optionalArg) { + return "{\"__schema\":{\"queryType\":{\"name\":\"Query\"},\"types\":[" + + "{\"name\":\"__Schema\",\"kind\":\"OBJECT\",\"fields\":[]}," + + "{\"name\":\"String\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"ID\",\"kind\":\"SCALAR\"}," + + "{\"name\":\"Query\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"container\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}," + + " {\"name\":\"" + + entry + + "\",\"args\":[" + + " {\"name\":\"" + + requiredArg + + "\",\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}}," + + " {\"name\":\"" + + optionalArg + + "\",\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}" + + " ],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}}," + + owned(module) + + "}]}," + + "{\"name\":\"Container\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"id\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"SCALAR\",\"name\":\"ID\"}}}," + + " {\"name\":\"withExec\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}]}," + + "{\"name\":\"Binding\",\"kind\":\"OBJECT\",\"fields\":[" + + " {\"name\":\"asString\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}," + + " {\"name\":\"" + + binding + + "\",\"args\":[],\"type\":{\"kind\":\"OBJECT\",\"name\":\"" + + root + + "\"}," + + owned(module) + + "}]}," + + "{\"name\":\"" + + root + + "\",\"kind\":\"OBJECT\"," + + owned(module) + + ",\"fields\":[" + + " {\"name\":\"greet\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}," + + owned(module) + + "}]}" + + "]}}"; + } +} diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ModuleClientCodegenTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ModuleClientCodegenTest.java new file mode 100644 index 0000000..76d85cf --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/introspection/ModuleClientCodegenTest.java @@ -0,0 +1,236 @@ +package io.dagger.codegen.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.palantir.javapoet.JavaFile; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ModuleClientCodegenTest { + + private static final ClientBinding LOCAL = + new ClientBinding("hello", "LOCAL_SOURCE", "dagger/modules/hello", ""); + private static final ClientBinding GIT = + new ClientBinding("hello", "GIT_SOURCE", "github.com/dagger/hello", "0123abc"); + + @TempDir Path compilationOutputDirectory; + + @Test + void theRootTypeGetsAFactoryThatServesTheModuleAndTakesItsConstructorArguments() + throws Exception { + String hello = generate(Fixtures.clientSchema("hello", "Hello", "hello", "asHello"), LOCAL); + assertThat(hello) + .contains("public static Hello from(Client dag, String name)") + .contains("public static Hello from(Client dag, String name, HelloArguments optArgs)") + .contains("QueryBuilder root = dag.queryBuilder().root();") + .contains( + "ModuleBinding.ensureServed(root, \"hello\", \"LOCAL_SOURCE\", \"dagger/modules/hello\", \"\");") + .contains("dag.queryBuilder().chain(\"hello\", fieldArgs)"); + } + + @Test + void theAliasIsNamedAfterTheModuleAndDelegates() throws Exception { + String hello = generate(Fixtures.clientSchema("hello", "Hello", "hello", "asHello"), LOCAL); + assertThat(hello) + .contains("public static Hello hello(Client dag, String name)") + .contains("return from(dag, name);") + .contains("public static Hello hello(Client dag, String name, HelloArguments optArgs)") + .contains("return from(dag, name, optArgs);"); + } + + @Test + void aGitBindingBakesItsRefAndPin() throws Exception { + String hello = generate(Fixtures.clientSchema("hello", "Hello", "hello", "asHello"), GIT); + assertThat(hello) + .contains( + "ModuleBinding.ensureServed(root, \"hello\", \"GIT_SOURCE\", \"github.com/dagger/hello\", \"0123abc\");"); + } + + @Test + void aModulesFieldOnACoreTypeBecomesAStaticShim() throws Exception { + String hello = generate(Fixtures.clientSchema("hello", "Hello", "hello", "asHello"), LOCAL); + // Binding.asHello is nullable in the fixture, so the shim resolves to Optional like any + // nullable object field would on an instance method. + assertThat(hello) + .contains("public static Optional asHello(Binding binding)") + .contains("QueryBuilder root = binding.queryBuilder().root();") + .contains("binding.queryBuilder().chain(\"asHello\")") + .contains("executeNullableObjectQuery(\"Hello\")"); + } + + @Test + void twoShimsOfTheSameFieldNameGetHelperClassesOfTheirOwn() throws Exception { + String hello = generate(Fixtures.twoShimsSchema("hello", "Hello", "hello", "configure"), LOCAL); + assertThat(hello) + .contains("public static class ContainerConfigureArguments") + .contains("public static class WorkspaceConfigureArguments") + .contains( + "public static Hello configure(Container container, ContainerConfigureArguments optArgs)") + .contains( + "public static Hello configure(Workspace workspace, WorkspaceConfigureArguments optArgs)"); + + Map sources = new HashMap<>(runtimeStubs()); + sources.put( + "io.dagger.core.Workspace", + "package io.dagger.core; public class Workspace {" + + " public io.dagger.sdk.QueryBuilder queryBuilder() { return null; } }"); + sources.put( + "io.dagger.core.Container", + "package io.dagger.core; public class Container {" + + " public io.dagger.sdk.QueryBuilder queryBuilder() { return null; } }"); + sources.put("io.dagger.client.hello.Hello", hello); + CompileSupport.assertCompiles(compilationOutputDirectory, sources); + } + + @Test + void theRootTypeComesFromTheSchemaNotFromTheModuleName() throws Exception { + ClientBinding e2e = new ClientBinding("e2e", "LOCAL_SOURCE", ".dagger/modules/e2e", ""); + ClientEntryPoint entryPoint = + new ClientEntryPoint( + SchemaPartition.client( + parse(Fixtures.clientSchema("e2e", "E2E", "e2E", "asE2E")), "e2e"), + e2e); + assertThat(entryPoint.rootTypeName()).isEqualTo("E2E"); + assertThat(entryPoint.entryField().getName()).isEqualTo("e2E"); + String root = generate(Fixtures.clientSchema("e2e", "E2E", "e2E", "asE2E"), e2e); + assertThat(root).contains("public class E2E").contains("public static E2E e2E(Client dag"); + } + + @Test + void theGeneratedClientCompilesAgainstCoreAndTheRuntime() throws Exception { + Map sources = new HashMap<>(runtimeStubs()); + sources.put( + "io.dagger.client.hello.Hello", + generate(Fixtures.clientSchema("hello", "Hello", "hello", "asHello"), LOCAL)); + CompileSupport.assertCompiles(compilationOutputDirectory, sources); + } + + @Test + void aBindingForAnotherModuleThanThePartitionIsRejected() throws Exception { + SchemaPartition hello = + SchemaPartition.client( + parse(Fixtures.clientSchema("hello", "Hello", "hello", "asHello")), "hello"); + assertThatThrownBy(() -> new ClientEntryPoint(hello, GIT.withModule("other"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("other") + .hasMessageContaining("hello"); + } + + @Test + void aModuleNamedAfterACoreTypeIsRejected() throws Exception { + ClientBinding container = + new ClientBinding("container", "LOCAL_SOURCE", "/modules/container", ""); + SchemaPartition partition = + SchemaPartition.client( + parse(Fixtures.coreNamedSchema("container", "container")), "container"); + assertThatThrownBy(() -> new ClientEntryPoint(partition, container)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("module container") + .hasMessageContaining("core type Container"); + } + + @Test + void schemaArgumentsThatWouldShadowAGeneratedLocalAreEscaped() throws Exception { + assertThat(Helpers.formatName(arg("dag"))).isEqualTo("_dag"); + assertThat(Helpers.formatName(arg("root"))).isEqualTo("_root"); + assertThat(Helpers.formatName(arg("builder"))).isEqualTo("_builder"); + assertThat(Helpers.formatName(arg("fieldArgs"))).isEqualTo("_fieldArgs"); + assertThat(Helpers.formatName(arg("optArgs"))).isEqualTo("_optArgs"); + assertThat(Helpers.formatName(arg("nextQueryBuilder"))).isEqualTo("_nextQueryBuilder"); + assertThat(Helpers.formatName(arg("objectQueryBuilder"))).isEqualTo("_objectQueryBuilder"); + assertThat(Helpers.formatName(arg("builders"))).isEqualTo("_builders"); + assertThat(Helpers.formatName(arg("name"))).isEqualTo("name"); + + String hello = + generate( + Fixtures.clientSchema("hello", "Hello", "hello", "asHello", "dag", "builder"), LOCAL); + assertThat(hello) + .contains("public static Hello from(Client dag, String _dag)") + .contains("builder.add(\"dag\", _dag)") + .contains("public HelloArguments withBuilder(String _builder)"); + + Map sources = new HashMap<>(runtimeStubs()); + sources.put("io.dagger.client.hello.Hello", hello); + CompileSupport.assertCompiles(compilationOutputDirectory, sources); + } + + private static InputObject arg(String name) { + InputObject arg = new InputObject(); + arg.setName(name); + return arg; + } + + @Test + void packageSegmentsAreLegalJava() { + assertThat(Helpers.packageSegment("hello")).isEqualTo("hello"); + assertThat(Helpers.packageSegment("my-module")).isEqualTo("mymodule"); + assertThat(Helpers.packageSegment("Java_SDK")).isEqualTo("javasdk"); + assertThat(Helpers.packageSegment("1st")).isEqualTo("_1st"); + assertThat(Helpers.packageSegment("package")).isEqualTo("package_"); + assertThatThrownBy(() -> Helpers.packageSegment("---")) + .isInstanceOf(IllegalArgumentException.class); + } + + /** The root type's source, generated as the module's client package would. */ + private static String generate(String schemaJson, ClientBinding binding) throws Exception { + Schema schema = parse(schemaJson); + SchemaPartition client = SchemaPartition.client(schema, binding.module()); + ClientEntryPoint entryPoint = new ClientEntryPoint(client, binding); + String pkg = "io.dagger.client." + Helpers.packageSegment(binding.module()); + TypeRegistry registry = + TypeRegistry.client(pkg, "io.dagger.core", "io.dagger.sdk", client.ownedTypeNames()); + ObjectVisitor visitor = + new ObjectVisitor(schema, registry, entryPoint, Path.of("."), StandardCharsets.UTF_8); + Type root = + client.types().stream() + .filter(t -> t.getName().equals(entryPoint.rootTypeName())) + .findFirst() + .orElseThrow(); + return JavaFile.builder(pkg, visitor.generateType(root)).build().toString(); + } + + private static Schema parse(String json) throws Exception { + return Schema.initialize( + new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.10"); + } + + /** Just enough of io.dagger.core and io.dagger.sdk for a client's root type to compile. */ + private static Map runtimeStubs() { + return Map.of( + "io.dagger.sdk.QueryBuilder", + "package io.dagger.sdk; public class QueryBuilder {" + + " public QueryBuilder root() { return this; }" + + " public QueryBuilder chain(String f) { return this; }" + + " public QueryBuilder chain(String f, Arguments a) { return this; }" + + " public QueryBuilder executeNullableObjectQuery(String t)" + + " throws java.util.concurrent.ExecutionException, InterruptedException," + + " io.dagger.sdk.exception.DaggerQueryException { return this; }" + + " public T executeQuery(Class c) { return null; } }", + "io.dagger.sdk.Arguments", + "package io.dagger.sdk; public class Arguments {" + + " public static Builder newBuilder() { return new Builder(); }" + + " public Arguments merge(Arguments o) { return this; }" + + " public static class Builder {" + + " public Builder add(String n, String v) { return this; }" + + " public Arguments build() { return new Arguments(); } } }", + "io.dagger.sdk.ModuleBinding", + "package io.dagger.sdk; public final class ModuleBinding {" + + " public static void ensureServed(QueryBuilder q, String n, String k, String r, String p)" + + " throws java.util.concurrent.ExecutionException, InterruptedException," + + " io.dagger.sdk.exception.DaggerQueryException {} }", + "io.dagger.sdk.exception.DaggerQueryException", + "package io.dagger.sdk.exception; public class DaggerQueryException extends Exception {}", + "io.dagger.core.Client", + "package io.dagger.core; public class Client {" + + " public io.dagger.sdk.QueryBuilder queryBuilder() { return null; } }", + "io.dagger.core.Binding", + "package io.dagger.core; public class Binding {" + + " public io.dagger.sdk.QueryBuilder queryBuilder() { return null; } }"); + } +} 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 32f7eb8..f23912d 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 @@ -5,18 +5,11 @@ import com.palantir.javapoet.JavaFile; import com.palantir.javapoet.TypeSpec; import java.io.ByteArrayInputStream; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; -import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.ToolProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -200,7 +193,7 @@ 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, Path.of("."), StandardCharsets.UTF_8) .generateType(type))); } } @@ -296,49 +289,6 @@ private static TypeRef nonNull(TypeRef inner) { } private void assertCompiles(Map sources) { - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertThat(compiler).isNotNull(); - - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - List compilationUnits = - sources.entrySet().stream() - .map(entry -> new SourceFile(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - boolean compiled = - compiler - .getTask( - null, - null, - diagnostics, - List.of( - "--release", "17", "-proc:none", "-d", compilationOutputDirectory.toString()), - null, - compilationUnits) - .call(); - - assertThat(compiled) - .withFailMessage( - "Generated sources did not compile:%n%s", - diagnostics.getDiagnostics().stream() - .map(Object::toString) - .collect(Collectors.joining("\n"))) - .isTrue(); - } - - private static final class SourceFile extends SimpleJavaFileObject { - private final String source; - - private SourceFile(String className, String source) { - super( - URI.create( - "string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), - JavaFileObject.Kind.SOURCE); - this.source = source; - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) { - return source; - } + CompileSupport.assertCompiles(compilationOutputDirectory, sources); } } 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 index f3aa536..209d937 100644 --- 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 @@ -12,10 +12,11 @@ class SchemaPartitionTest { /** A client schema for module {@code hello}: core plus one module, as the engine emits it. */ - private static final String HELLO = clientSchema("hello", "Hello", "hello", "asHello"); + private static final String HELLO = Fixtures.clientSchema("hello", "Hello", "hello", "asHello"); /** The same core bound to a different module, to check core does not depend on the module. */ - private static final String BUILDER = clientSchema("builder", "Builder", "builder", "asBuilder"); + private static final String BUILDER = + Fixtures.clientSchema("builder", "Builder", "builder", "asBuilder"); @Test void corePartitionKeepsOnlyUnownedTypes() throws Exception { @@ -170,47 +171,4 @@ private static Schema parse(String json) throws Exception { return Schema.initialize( new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), "v1.0.0-beta.10"); } - - private static String owned(String module) { - return "\"directives\":[{\"name\":\"sourceMap\",\"args\":[{\"name\":\"module\",\"value\":\"\\\"" - + module - + "\\\"\"}]}]"; - } - - private static String clientSchema(String module, String root, String entry, String binding) { - return "{\"__schema\":{\"queryType\":{\"name\":\"Query\"},\"types\":[" - + "{\"name\":\"__Schema\",\"kind\":\"OBJECT\",\"fields\":[]}," - + "{\"name\":\"String\",\"kind\":\"SCALAR\"}," - + "{\"name\":\"ID\",\"kind\":\"SCALAR\"}," - + "{\"name\":\"Query\",\"kind\":\"OBJECT\",\"fields\":[" - + " {\"name\":\"container\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}," - + " {\"name\":\"" - + entry - + "\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"" - + root - + "\"}}," - + owned(module) - + "}]}," - + "{\"name\":\"Container\",\"kind\":\"OBJECT\",\"fields\":[" - + " {\"name\":\"id\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"SCALAR\",\"name\":\"ID\"}}}," - + " {\"name\":\"withExec\",\"args\":[],\"type\":{\"kind\":\"NON_NULL\",\"ofType\":{\"kind\":\"OBJECT\",\"name\":\"Container\"}}}]}," - + "{\"name\":\"Binding\",\"kind\":\"OBJECT\",\"fields\":[" - + " {\"name\":\"asString\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}}," - + " {\"name\":\"" - + binding - + "\",\"args\":[],\"type\":{\"kind\":\"OBJECT\",\"name\":\"" - + root - + "\"}," - + owned(module) - + "}]}," - + "{\"name\":\"" - + root - + "\",\"kind\":\"OBJECT\"," - + owned(module) - + ",\"fields\":[" - + " {\"name\":\"greet\",\"args\":[],\"type\":{\"kind\":\"SCALAR\",\"name\":\"String\"}," - + owned(module) - + "}]}" - + "]}}"; - } } From b60b1d8e128662dbb71aa34522ce09565338765f Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:44:13 +0200 Subject: [PATCH 10/17] codegen: generate core into io.dagger.core and clients from a plan The cutover, in one patch because the tree cannot build between its halves. The generator now runs a plan: one core entry and any number of client entries, each with its own schema and, for a client, the bound module's name and binding; all of it lands in one output tree from one Maven invocation, so a module with many dependencies does not pay one Maven run per package. The schema-only form the reactor and the packager use is a one-entry core plan, and the CLI-query fallback still stands behind it. Each package root is cleaned before it is written and no other root is touched, which is what a second, one-entry pass for the self client relies on. Core is generated into io.dagger.core, from whatever schema the consumer is entitled to: a module's own module-facing schema keeps Host and the other hidden types out of module code; a standalone client's schema hides nothing. A client goes into io.dagger.client., resolving every type it does not own to core. The runtime, the annotation processor, the templates, the e2e fixture and the README follow the generated types to io.dagger.core. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 2 +- sdk/README.md | 2 +- .../io/dagger/codegen/DaggerCLIUtils.java | 21 +- .../io/dagger/codegen/DaggerCodegenMojo.java | 104 +++----- .../io/dagger/codegen/GenerationPlan.java | 92 +++++++ .../java/io/dagger/codegen/Generator.java | 159 ++++++++++++ .../dagger/codegen/introspection/Helpers.java | 2 +- .../io/dagger/codegen/DaggerCLIUtilsTest.java | 36 +++ .../java/io/dagger/codegen/GeneratorTest.java | 231 ++++++++++++++++++ .../NullableObjectCodegenTest.java | 33 ++- .../DaggerModuleAnnotationProcessor.java | 41 ++-- .../annotation/processor/DaggerType.java | 4 +- .../annotation/processor/DaggerTypeTest.java | 9 +- sdk/dagger-java-sdk/pom.xml | 2 + .../dagger/module/annotation/DefaultPath.java | 2 +- .../io/dagger/sdk/AutoCloseableClient.java | 2 +- .../src/main/java/io/dagger/sdk/Dagger.java | 2 +- .../io/dagger/sdk/telemetry/Telemetry.java | 6 +- sdk/pom.xml | 2 + templates/default/pom.xml | 3 +- .../daggermoduleplaceholder/DaggerModule.java | 6 +- templates/empty/pom.xml | 3 +- templates/legacy/pom.xml | 3 +- .../daggermoduleplaceholder/DaggerModule.java | 4 +- 24 files changed, 633 insertions(+), 138 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/test/java/io/dagger/codegen/DaggerCLIUtilsTest.java create mode 100644 sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index a9ea388..31c4667 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -303,7 +303,7 @@ type E2e { + "\n" + "import static io.dagger.sdk.Dagger.dag;\n" + "\n" - + "import io.dagger.client.Directory;\n" + + "import io.dagger.core.Directory;\n" + "import io.dagger.module.annotation.Function;\n" + "import io.dagger.module.annotation.Object;\n" + "import java.util.Optional;\n" diff --git a/sdk/README.md b/sdk/README.md index fc257da..cbf31f7 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -149,7 +149,7 @@ Here is a code snippet using the Dagger client ```java package io.dagger.sample; -import io.dagger.client.Client; +import io.dagger.core.Client; import io.dagger.sdk.Dagger; import java.util.List; diff --git a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCLIUtils.java b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCLIUtils.java index 6404f11..9f038ba 100644 --- a/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCLIUtils.java +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/DaggerCLIUtils.java @@ -298,6 +298,9 @@ private static boolean isStandardVersionFormat(String input) { return matcher.matches(); } + private static final Pattern VERSION_LINE = + Pattern.compile("^version:\\s+(\\S+)", Pattern.MULTILINE); + /** * Gets the value given returned by "dagger version". If the version is of the form vX.Y.Z then * the "v" prefix is stripped @@ -306,12 +309,22 @@ private static boolean isStandardVersionFormat(String input) { * @return the version */ public static String getVersion(String binPath) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - String output = + return parseVersion( FluentProcess.start(binPath, "version") .withTimeout(Duration.of(60, ChronoUnit.SECONDS)) - .get(); - String version = output.split("\\s")[1]; + .get()); + } + + /** + * The version out of {@code dagger version}, which prints five aligned {@code key: value} lines. + * Splitting on whitespace lands on the padding, not on the value. + */ + static String parseVersion(String output) { + Matcher matcher = VERSION_LINE.matcher(output); + if (!matcher.find()) { + throw new IllegalStateException("`dagger version` printed no version line:\n" + output); + } + String version = matcher.group(1); return isStandardVersionFormat(version) ? version.substring(1) : version; } } 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 1d3d0e4..8e22695 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,12 +1,8 @@ package io.dagger.codegen; -import io.dagger.codegen.introspection.CodegenVisitor; -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.Files; import java.nio.file.Path; import java.util.List; import org.apache.maven.plugin.AbstractMojo; @@ -46,73 +42,36 @@ public class DaggerCodegenMojo extends AbstractMojo { @Parameter(defaultValue = "${project.build.directory}/generated-sources/dagger") private File outputDirectory; + /** A generation plan directory (see {@link GenerationPlan}); overrides the single schema. */ + @Parameter(property = "dagger.plan") + protected String plan; + + /** + * A module whose already generated client package a full plan leaves in place (see {@link + * Generator#generate}). + */ + @Parameter(property = "dagger.keep") + protected String keep; + @Override public void execute() throws MojoExecutionException, MojoFailureException { outputEncoding = validateEncoding(outputEncoding); - // Ensure that the output directory path is all intact so that - // we can just write into it. - // File outputDir = getOutputDirectory(); - if (!outputDir.exists()) { outputDir.mkdirs(); } - Path dest = outputDir.toPath(); - try (InputStream in = getInstrospectionJson()) { - Schema schema = Schema.initialize(in, version); - SchemaVisitor codegen = - new CodegenVisitor( - schema, - TypeRegistry.core("io.dagger.client", "io.dagger.sdk"), - null, - dest, - Charset.forName(outputEncoding)); - schema.visit( - new SchemaVisitor() { - @Override - public void visitScalar(Type type) { - getLog().info(String.format("Generating scalar %s", type.getName())); - codegen.visitScalar(type); - } - - @Override - public void visitObject(Type type) { - getLog().info(String.format("Generating object %s", type.getName())); - codegen.visitObject(type); - } - - @Override - public void visitInterface(Type type) { - getLog().info(String.format("Generating interface %s", type.getName())); - codegen.visitInterface(type); - } - - @Override - public void visitInput(Type type) { - getLog().info(String.format("Generating input %s", type.getName())); - codegen.visitInput(type); - } - - @Override - public void visitEnum(Type type) { - getLog().info(String.format("Generating enum %s", type.getName())); - codegen.visitEnum(type); - } - - @Override - public void visitVersion(String version) { - getLog().info(String.format("Generating interface Version")); - codegen.visitVersion(version); - } - - @Override - public void visitIDAbles(List types) { - getLog().info(String.format("Generate helpers for IDAbles")); - codegen.visitIDAbles(types); - } - }); + + try { + List entries; + if (plan != null && !plan.isBlank() && Files.isDirectory(Path.of(plan))) { + entries = GenerationPlan.read(Path.of(plan)); + } else { + entries = GenerationPlan.core(schemaFile()); + } + Generator.generate( + entries, version, dest, Charset.forName(outputEncoding), keep, getLog()::info); } catch (IOException | InterruptedException e) { throw new MojoFailureException(e); } @@ -123,25 +82,26 @@ public void visitIDAbles(List types) { } } - private InputStream getInstrospectionJson() - throws IOException, MojoFailureException, InterruptedException { + /** The schema to generate core from: the configured file, else the local CLI's own. */ + private Path schemaFile() throws IOException, MojoFailureException, InterruptedException { if (this.introspectionJson != null && !this.introspectionJson.isEmpty()) { File f = new File(this.introspectionJson); if (f.exists()) { - return new FileInputStream(f); + return f.toPath(); } } this.bin = DaggerCLIUtils.getBinary(this.bin); - return daggerSchema(); - } - - private InputStream daggerSchema() - throws IOException, InterruptedException, MojoFailureException { String actualVersion = DaggerCLIUtils.getVersion(this.bin); getLog() .info(String.format("Querying local dagger CLI for schema (version=%s)", actualVersion)); this.version = actualVersion; - return DaggerCLIUtils.query(DaggerCLIUtils.introspectionQuery(getClass()), this.bin); + Path schema = Files.createTempFile("dagger-schema", ".json"); + schema.toFile().deleteOnExit(); + try (InputStream in = + DaggerCLIUtils.query(DaggerCLIUtils.introspectionQuery(getClass()), this.bin)) { + Files.copy(in, schema, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + return schema; } public File getOutputDirectory() { 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..8cdbbad --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/GenerationPlan.java @@ -0,0 +1,92 @@ +package io.dagger.codegen; + +import io.dagger.codegen.introspection.ClientBinding; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +/** + * What one codegen run emits: a core package and any number of module client packages, each from + * its own schema, all into one output tree in one Maven invocation. + * + *

On disk a plan is a directory with one subdirectory per entry, each holding {@code + * schema.json} — the introspection JSON to generate from — and {@code meta.json}: + * + *

+ * {"mode":"core"}
+ * {"mode":"client","module":"hello",
+ *  "binding":{"kind":"LOCAL_SOURCE","ref":"dagger/modules/hello","pin":""}}
+ * 
+ * + * Entries are processed in name order so the output is deterministic. + */ +public final class GenerationPlan { + + /** One package to generate. {@code module} and {@code binding} are null for core. */ + public record Entry(String name, Path schema, String mode, String module, ClientBinding binding) { + + public boolean isCore() { + return "core".equals(mode); + } + } + + private GenerationPlan() {} + + /** A plan with a single core entry, for the schema-only invocation. */ + public static List core(Path schema) { + return List.of(new Entry("core", schema, "core", null, null)); + } + + public static List read(Path dir) throws IOException { + try (Stream children = Files.list(dir)) { + return children.filter(Files::isDirectory).sorted().map(GenerationPlan::entry).toList(); + } + } + + private static Entry entry(Path dir) { + Path schema = dir.resolve("schema.json"); + Path meta = dir.resolve("meta.json"); + if (!Files.isRegularFile(schema) || !Files.isRegularFile(meta)) { + throw new IllegalArgumentException( + "plan entry " + dir.getFileName() + " needs both schema.json and meta.json"); + } + JsonObject json; + try (JsonReader reader = Json.createReader(Files.newBufferedReader(meta))) { + json = reader.readObject(); + } catch (IOException e) { + throw new IllegalArgumentException("plan entry " + dir.getFileName() + ": " + meta, e); + } + String mode = json.getString("mode", null); + switch (mode == null ? "" : mode) { + case "core" -> { + return new Entry(dir.getFileName().toString(), schema, "core", null, null); + } + case "client" -> { + String module = json.getString("module", null); + JsonObject binding = json.getJsonObject("binding"); + if (module == null || binding == null) { + throw new IllegalArgumentException( + "plan entry " + dir.getFileName() + ": a client needs module and binding"); + } + return new Entry( + dir.getFileName().toString(), + schema, + "client", + module, + new ClientBinding( + module, + binding.getString("kind"), + binding.getString("ref"), + binding.getString("pin", ""))); + } + default -> + throw new IllegalArgumentException( + "plan entry " + dir.getFileName() + ": mode must be core or client, not " + mode); + } + } +} 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..345bc34 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/main/java/io/dagger/codegen/Generator.java @@ -0,0 +1,159 @@ +package io.dagger.codegen; + +import io.dagger.codegen.GenerationPlan.Entry; +import io.dagger.codegen.introspection.ClientEntryPoint; +import io.dagger.codegen.introspection.CodegenVisitor; +import io.dagger.codegen.introspection.Helpers; +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.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Runs a {@link GenerationPlan}: core into {@code io.dagger.core}, each module client into {@code + * io.dagger.client.}, both against the hand-written runtime in {@code io.dagger.sdk}. + * + *

Each package root is deleted before it is written, so a package never carries a type its + * schema no longer has. A plan that carries core is the whole picture, so it also removes every + * client package it does not mention — a dependency that was dropped, an alias that was renamed — + * except the one named by {@code keep}: a module's previously generated self client, carried + * through so the module still compiles while its own schema is read, and rewritten by a second, + * one-entry pass. A plan without core touches nothing but the roots it writes, which is what that + * second pass relies on. + */ +public final class Generator { + + public static final String CORE_PACKAGE = "io.dagger.core"; + public static final String CLIENT_PACKAGE_PREFIX = "io.dagger.client"; + public static final String RUNTIME_PACKAGE = "io.dagger.sdk"; + + private Generator() {} + + public static String clientPackage(String module) { + return CLIENT_PACKAGE_PREFIX + "." + Helpers.packageSegment(module); + } + + public static void generate( + List entries, String version, Path out, Charset encoding, Consumer log) + throws IOException { + generate(entries, version, out, encoding, null, log); + } + + /** + * @param keep the module whose existing client package survives a full plan that does not + * regenerate it, or null + */ + public static void generate( + List entries, + String version, + Path out, + Charset encoding, + String keep, + Consumer log) + throws IOException { + if (entries.stream().filter(Entry::isCore).count() > 1) { + throw new IllegalArgumentException("a plan holds at most one core entry"); + } + rejectClashingPackages(entries); + if (entries.stream().anyMatch(Entry::isCore)) { + cleanUnplannedClients(out, entries, keep, log); + } + for (Entry entry : entries) { + Schema schema; + try (InputStream in = Files.newInputStream(entry.schema())) { + schema = Schema.initialize(in, version); + } + if (entry.isCore()) { + SchemaPartition core = SchemaPartition.core(schema); + clean(out, CORE_PACKAGE); + log.accept(String.format("Generating %s (%d types)", CORE_PACKAGE, core.types().size())); + core.visit( + new CodegenVisitor( + schema, TypeRegistry.core(CORE_PACKAGE, RUNTIME_PACKAGE), null, out, encoding)); + } else { + SchemaPartition client = SchemaPartition.client(schema, entry.module()); + ClientEntryPoint entryPoint = new ClientEntryPoint(client, entry.binding()); + String pkg = clientPackage(entry.module()); + clean(out, pkg); + log.accept( + String.format( + "Generating %s for module %s (%d types, root %s)", + pkg, entry.module(), client.types().size(), entryPoint.rootTypeName())); + client.visit( + new CodegenVisitor( + schema, + TypeRegistry.client(pkg, CORE_PACKAGE, RUNTIME_PACKAGE, client.ownedTypeNames()), + entryPoint, + out, + encoding)); + } + } + } + + /** + * Two module names that normalize to the same package segment ({@code my-module} and {@code + * mymodule}) would emit into one package, the later entry deleting the earlier one's client. + */ + private static void rejectClashingPackages(List entries) { + Map bySegment = new LinkedHashMap<>(); + for (Entry entry : entries) { + if (entry.isCore()) { + continue; + } + String previous = bySegment.put(Helpers.packageSegment(entry.module()), entry.module()); + if (previous != null && !previous.equals(entry.module())) { + throw new IllegalArgumentException( + String.format( + "modules %s and %s both name the package %s; alias one of them", + previous, entry.module(), clientPackage(entry.module()))); + } + } + } + + private static void cleanUnplannedClients( + Path out, List entries, String keep, Consumer log) throws IOException { + Path clients = out.resolve(CLIENT_PACKAGE_PREFIX.replace('.', '/')); + if (!Files.isDirectory(clients)) { + return; + } + Set wanted = new HashSet<>(); + entries.stream() + .filter(e -> !e.isCore()) + .forEach(e -> wanted.add(Helpers.packageSegment(e.module()))); + if (keep != null && !keep.isBlank()) { + wanted.add(Helpers.packageSegment(keep)); + } + try (Stream roots = Files.list(clients)) { + for (Path root : roots.filter(Files::isDirectory).sorted().toList()) { + if (!wanted.contains(root.getFileName().toString())) { + log.accept("Removing stale client package " + clients.relativize(root)); + clean(out, CLIENT_PACKAGE_PREFIX + "." + root.getFileName()); + } + } + } + } + + private static void clean(Path out, String pkg) throws IOException { + Path root = out.resolve(pkg.replace('.', '/')); + if (!Files.exists(root)) { + return; + } + try (Stream files = Files.walk(root)) { + for (Path file : files.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(file); + } + } + } +} 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 f469aac..86e7234 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 @@ -145,7 +145,7 @@ static List getArrayField(Field field, Schema schema) { * lowercase letters and digits survive, so {@code my-module} is {@code mymodule}; a leading digit * or a Java keyword is escaped rather than rejected. */ - static String packageSegment(String moduleName) { + public static String packageSegment(String moduleName) { String segment = moduleName.toLowerCase().replaceAll("[^a-z0-9]", ""); if (segment.isEmpty()) { throw new IllegalArgumentException( diff --git a/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/DaggerCLIUtilsTest.java b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/DaggerCLIUtilsTest.java new file mode 100644 index 0000000..18562aa --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/DaggerCLIUtilsTest.java @@ -0,0 +1,36 @@ +package io.dagger.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class DaggerCLIUtilsTest { + + private static final String DAGGER_VERSION = + """ + version: v1.0.0-beta.10 + commit: 0e19eba6 + dirty: no + platform: linux/amd64 + runner-host: image://registry.dagger.io/engine:v1.0.0-beta.10 + """; + + @Test + void readsTheVersionOffTheAlignedOutput() { + assertThat(DaggerCLIUtils.parseVersion(DAGGER_VERSION)).isEqualTo("v1.0.0-beta.10"); + } + + @Test + void stripsTheVPrefixOfAPlainRelease() { + assertThat(DaggerCLIUtils.parseVersion("version: v0.21.4\ncommit: abc\n")) + .isEqualTo("0.21.4"); + } + + @Test + void outputWithoutAVersionLineIsAnError() { + assertThatThrownBy(() -> DaggerCLIUtils.parseVersion("something else entirely\n")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("no version line"); + } +} 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..a8aaac0 --- /dev/null +++ b/sdk/dagger-codegen-maven-plugin/src/test/java/io/dagger/codegen/GeneratorTest.java @@ -0,0 +1,231 @@ +package io.dagger.codegen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.dagger.codegen.introspection.Fixtures; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class GeneratorTest { + + private static final String HELLO = Fixtures.clientSchema("hello", "Hello", "hello", "asHello"); + private static final String BUILDER = + Fixtures.clientSchema("builder", "Builder", "builder", "asBuilder"); + + @TempDir Path dir; + + @Test + void aPlanEmitsCoreAndOnePackagePerClientIntoOneTree() throws Exception { + Path plan = plan(Map.of("core", core(HELLO), "client-hello", client(HELLO, "hello"))); + Path out = dir.resolve("out"); + + Generator.generate( + GenerationPlan.read(plan), "v1.0.0-beta.10", out, StandardCharsets.UTF_8, s -> {}); + + assertThat(out.resolve("io/dagger/core")) + .isDirectoryContaining("glob:**/Client.java") + .isDirectoryContaining("glob:**/Container.java") + .isDirectoryContaining("glob:**/Binding.java") + .isDirectoryContaining("glob:**/Version.java") + .isDirectoryContaining("glob:**/JsonConverter.java") + .isDirectoryNotContaining("glob:**/Hello.java"); + assertThat(out.resolve("io/dagger/client/hello")) + .isDirectoryContaining("glob:**/Hello.java") + .isDirectoryNotContaining("glob:**/Version.java") + .isDirectoryNotContaining("glob:**/JsonConverter.java") + .isDirectoryNotContaining("glob:**/Container.java"); + assertThat(Files.readString(out.resolve("io/dagger/client/hello/Hello.java"))) + .startsWith( + "// This class has been generated by dagger-java-sdk. DO NOT EDIT.\npackage io.dagger.client.hello;") + .contains("import io.dagger.core.Client;") + .contains("import io.dagger.sdk.ModuleBinding;") + .contains("public static Hello from(Client dag, String name)"); + assertThat(Files.readString(out.resolve("io/dagger/core/Client.java"))) + .contains("package io.dagger.core;") + .doesNotContain("hello("); + // The runtime builds scalars reflectively from another package. + assertThat(Files.readString(out.resolve("io/dagger/core/ID.java"))) + .contains("public ID(String value)"); + } + + @Test + void corePackageBytesDoNotDependOnWhichModuleTheSchemaWasBoundTo() throws Exception { + Path fromHello = dir.resolve("hello"); + Path fromBuilder = dir.resolve("builder"); + Generator.generate( + GenerationPlan.read(plan(Map.of("core", core(HELLO)))), + "v1.0.0-beta.10", + fromHello, + StandardCharsets.UTF_8, + s -> {}); + Generator.generate( + GenerationPlan.read(plan(Map.of("core", core(BUILDER)))), + "v1.0.0-beta.10", + fromBuilder, + StandardCharsets.UTF_8, + s -> {}); + assertThat(tree(fromHello.resolve("io/dagger/core"))) + .isNotEmpty() + .isEqualTo(tree(fromBuilder.resolve("io/dagger/core"))); + } + + @Test + void aFullPlanDropsClientPackagesItDoesNotMentionExceptTheKeptOne() throws Exception { + Path out = dir.resolve("out"); + Path dropped = out.resolve("io/dagger/client/dropped/Dropped.java"); + Path kept = out.resolve("io/dagger/client/self/Self.java"); + Path staleCore = out.resolve("io/dagger/core/Stale.java"); + Files.createDirectories(dropped.getParent()); + Files.createDirectories(kept.getParent()); + Files.createDirectories(staleCore.getParent()); + Files.writeString(dropped, "a dependency that is no longer declared"); + Files.writeString(kept, "the module's own client from the previous generation"); + Files.writeString(staleCore, "a core type the schema no longer has"); + + Generator.generate( + GenerationPlan.read( + plan(Map.of("core", core(HELLO), "client-hello", client(HELLO, "hello")))), + "v1.0.0-beta.10", + out, + StandardCharsets.UTF_8, + "self", + s -> {}); + + assertThat(dropped).doesNotExist(); + assertThat(staleCore).doesNotExist(); + assertThat(kept).exists(); + assertThat(out.resolve("io/dagger/client/hello/Hello.java")).exists(); + } + + @Test + void aPlanWithoutCoreLeavesEveryOtherClientPackageAlone() throws Exception { + Path out = dir.resolve("out"); + Path other = out.resolve("io/dagger/client/other/Other.java"); + Files.createDirectories(other.getParent()); + Files.writeString(other, "untouched"); + + Generator.generate( + GenerationPlan.read(plan(Map.of("client-hello", client(HELLO, "hello")))), + "v1.0.0-beta.10", + out, + StandardCharsets.UTF_8, + s -> {}); + + assertThat(other).exists(); + assertThat(out.resolve("io/dagger/client/hello/Hello.java")).exists(); + assertThat(out.resolve("io/dagger/core")).doesNotExist(); + } + + @Test + void aPlanHoldsAtMostOneCore() throws Exception { + Path plan = plan(Map.of("a", core(HELLO), "b", core(BUILDER))); + assertThatThrownBy( + () -> + Generator.generate( + GenerationPlan.read(plan), + "v", + dir.resolve("out"), + StandardCharsets.UTF_8, + s -> {})) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("at most one core"); + } + + @Test + void twoModulesThatNameTheSamePackageAreRejected() throws Exception { + Path plan = + plan( + Map.of( + "client-a", + client( + Fixtures.clientSchema("my-module", "MyModule", "myModule", "asMyModule"), + "my-module"), + "client-b", + client( + Fixtures.clientSchema("mymodule", "Mymodule", "mymodule", "asMymodule"), + "mymodule"))); + Path out = dir.resolve("out"); + + assertThatThrownBy( + () -> + Generator.generate( + GenerationPlan.read(plan), + "v1.0.0-beta.10", + out, + StandardCharsets.UTF_8, + s -> {})) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("my-module") + .hasMessageContaining("mymodule") + .hasMessageContaining("io.dagger.client.mymodule"); + assertThat(out).doesNotExist(); + } + + @Test + void anEntryWithoutItsFilesOrWithAnUnknownModeIsRejected() throws Exception { + Path incomplete = dir.resolve("incomplete"); + Files.createDirectories(incomplete.resolve("x")); + Files.writeString(incomplete.resolve("x/schema.json"), HELLO); + assertThatThrownBy(() -> GenerationPlan.read(incomplete)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("x") + .hasMessageContaining("meta.json"); + + Path odd = plan(Map.of("y", Map.of("schema.json", HELLO, "meta.json", "{\"mode\":\"weird\"}"))); + assertThatThrownBy(() -> GenerationPlan.read(odd)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("weird"); + } + + @Test + void clientPackagesAreNamedAfterTheModule() { + assertThat(Generator.clientPackage("hello")).isEqualTo("io.dagger.client.hello"); + assertThat(Generator.clientPackage("my-module")).isEqualTo("io.dagger.client.mymodule"); + } + + private static Map core(String schema) { + return Map.of("schema.json", schema, "meta.json", "{\"mode\":\"core\"}"); + } + + private static Map client(String schema, String module) { + return Map.of( + "schema.json", + schema, + "meta.json", + "{\"mode\":\"client\",\"module\":\"" + + module + + "\",\"binding\":{\"kind\":\"LOCAL_SOURCE\",\"ref\":\"dagger/modules/" + + module + + "\",\"pin\":\"\"}}"); + } + + private Path plan(Map> entries) throws IOException { + Path plan = Files.createTempDirectory(dir, "plan"); + for (Map.Entry> entry : entries.entrySet()) { + Path entryDir = Files.createDirectories(plan.resolve(entry.getKey())); + for (Map.Entry file : entry.getValue().entrySet()) { + Files.writeString(entryDir.resolve(file.getKey()), file.getValue()); + } + } + return plan; + } + + /** Relative path to content, for every file under a root. */ + private static Map tree(Path root) throws IOException { + Map tree = new HashMap<>(); + try (Stream files = Files.walk(root)) { + for (Path file : files.filter(Files::isRegularFile).toList()) { + tree.put(root.relativize(file).toString(), Files.readString(file)); + } + } + return tree; + } +} 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 f23912d..f290b1a 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 @@ -15,8 +15,7 @@ class NullableObjectCodegenTest { - private static final TypeRegistry REGISTRY = - TypeRegistry.core("io.dagger.client", "io.dagger.sdk"); + private static final TypeRegistry REGISTRY = TypeRegistry.core("io.dagger.core", "io.dagger.sdk"); @TempDir Path compilationOutputDirectory; @@ -164,11 +163,11 @@ void nullableFieldDoesNotPropagateBetweenSiblingInterfaces() throws Exception { Map generated = sources(root, nullableSibling, nonNullSibling, nullableImplementation, implementation); - assertThat(generated.get("io.dagger.client.NullableSibling")).contains("Optional child()"); - assertThat(generated.get("io.dagger.client.NonNullSibling")) + assertThat(generated.get("io.dagger.core.NullableSibling")).contains("Optional child()"); + assertThat(generated.get("io.dagger.core.NonNullSibling")) .contains("Foo child();") .doesNotContain("Optional child()"); - assertThat(generated.get("io.dagger.client.NonNullImplementation")) + assertThat(generated.get("io.dagger.core.NonNullImplementation")) .contains("Foo child()") .doesNotContain("Optional child()"); } @@ -183,7 +182,7 @@ private Map sources(Type... types) throws Exception { Map sources = new HashMap<>(supportSources()); for (Type type : types) { - String qualifiedName = "io.dagger.client." + type.getName(); + String qualifiedName = "io.dagger.core." + type.getName(); if (type.getKind() == TypeKind.INTERFACE) { InterfaceVisitor visitor = new InterfaceVisitor(schema, REGISTRY, Path.of("."), StandardCharsets.UTF_8); @@ -207,18 +206,18 @@ private Map sources(Type... types) throws Exception { */ private static Map supportSources() { return Map.of( - "io.dagger.client.Animal", - "package io.dagger.client; public interface Animal {}", - "io.dagger.client.AnimalClient", - "package io.dagger.client; public class AnimalClient implements Animal {" + "io.dagger.core.Animal", + "package io.dagger.core; public interface Animal {}", + "io.dagger.core.AnimalClient", + "package io.dagger.core; public class AnimalClient implements Animal {" + " public AnimalClient(io.dagger.sdk.QueryBuilder queryBuilder) {} }", - "io.dagger.client.Dog", - "package io.dagger.client; public class Dog implements Animal {" + "io.dagger.core.Dog", + "package io.dagger.core; public class Dog implements Animal {" + " public Dog(io.dagger.sdk.QueryBuilder queryBuilder) {} }", - "io.dagger.client.Pet", - "package io.dagger.client; public interface Pet {}", - "io.dagger.client.PetClient", - "package io.dagger.client; public class PetClient implements Pet {" + "io.dagger.core.Pet", + "package io.dagger.core; public interface Pet {}", + "io.dagger.core.PetClient", + "package io.dagger.core; public class PetClient implements Pet {" + " public PetClient(io.dagger.sdk.QueryBuilder queryBuilder) {} }", "io.dagger.sdk.QueryBuilder", "package io.dagger.sdk; public class QueryBuilder {" @@ -233,7 +232,7 @@ private static Map supportSources() { } private static String javaFile(TypeSpec typeSpec) { - return JavaFile.builder("io.dagger.client", typeSpec).build().toString(); + return JavaFile.builder("io.dagger.core", typeSpec).build().toString(); } private static String generateInterface(Type type, String version) throws Exception { 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 d9940a7..ae6fd00 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 @@ -11,16 +11,12 @@ import com.palantir.javapoet.MethodSpec; import com.palantir.javapoet.ParameterizedTypeName; import com.palantir.javapoet.TypeSpec; -import io.dagger.sdk.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.sdk.exception.DaggerExecException; -import io.dagger.sdk.exception.DaggerQueryException; -import io.dagger.sdk.telemetry.Telemetry; +import io.dagger.core.FunctionCall; +import io.dagger.core.FunctionCallArgValue; +import io.dagger.core.ID; +import io.dagger.core.JSON; +import io.dagger.core.JsonConverter; +import io.dagger.core.TypeDef; import io.dagger.module.annotation.Check; import io.dagger.module.annotation.Default; import io.dagger.module.annotation.DefaultPath; @@ -39,6 +35,10 @@ import io.dagger.module.info.ObjectInfo; import io.dagger.module.info.ParameterInfo; import io.dagger.module.info.TypeInfo; +import io.dagger.sdk.Dagger; +import io.dagger.sdk.exception.DaggerExecException; +import io.dagger.sdk.exception.DaggerQueryException; +import io.dagger.sdk.telemetry.Telemetry; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -286,7 +286,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.core.Client")) .map( param -> { TypeMirror tm = param.asType(); @@ -311,10 +311,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.core.Directory", + "io.dagger.core.File", + "io.dagger.core.GitRepository", + "io.dagger.core.GitRef") .contains(tm.toString())) { throw new IllegalArgumentException( "Parameter " @@ -338,7 +338,7 @@ 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.core.Directory")) { throw new IllegalArgumentException( "Parameter " + param.getSimpleName() @@ -382,8 +382,7 @@ static JavaFile generate(ModuleInfo moduleInfo) { .addException(ExecutionException.class) .addException(DaggerQueryException.class) .addException(InterruptedException.class) - .addCode( - "$T module = $T.dag().module()", io.dagger.client.Module.class, Dagger.class); + .addCode("$T module = $T.dag().module()", io.dagger.core.Module.class, Dagger.class); if (isNotBlank(moduleInfo.description())) { rm.addCode("\n .withDescription($S)", moduleInfo.description()); } @@ -407,7 +406,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()", io.dagger.core.TypeDef.class) .addCode(".withDescription($S)", fieldInfo.description()); } rm.addCode(")"); @@ -436,7 +435,7 @@ static JavaFile generate(ModuleInfo moduleInfo) { if (isNotBlank(enumValue.description())) { rm.addCode( ", new $T.WithEnumValueArguments().withDescription($S)", - io.dagger.client.TypeDef.class, + io.dagger.core.TypeDef.class, enumValue.description()); } rm.addCode(")"); // end of .withEnumValue( @@ -740,7 +739,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.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 dff3c3c..0120d50 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,9 +3,9 @@ import com.palantir.javapoet.ClassName; import com.palantir.javapoet.CodeBlock; import com.palantir.javapoet.ParameterizedTypeName; -import io.dagger.sdk.Dagger; -import io.dagger.client.TypeDefKind; +import io.dagger.core.TypeDefKind; import io.dagger.module.info.TypeInfo; +import io.dagger.sdk.Dagger; import java.util.Set; import javax.lang.model.type.TypeKind; 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 e509f05..302fc22 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,19 +11,19 @@ class DaggerTypeTest { @Test void optionalObjectReturnsAreRegisteredAsOptionalAndUnwrappedForSerialization() { - DaggerType type = declared("java.util.Optional"); + DaggerType type = declared("java.util.Optional"); assertThat(type.toDaggerTypeDef().toString()) .isEqualTo( "io.dagger.sdk.Dagger.dag().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.core.Container"); assertThat(type.toDaggerTypeDef().toString()) .isEqualTo("io.dagger.sdk.Dagger.dag().typeDef().withObject(\"Container\")"); @@ -40,8 +40,7 @@ void optionalObjectFieldsAreRegisteredAsOptional() { new FieldInfo( "maybeContainer", "", - new TypeInfo( - "java.util.Optional", TypeKind.DECLARED.name())); + new TypeInfo("java.util.Optional", TypeKind.DECLARED.name())); assertThat(DaggerType.of(field.type()).toDaggerTypeDef().toString()) .isEqualTo( diff --git a/sdk/dagger-java-sdk/pom.xml b/sdk/dagger-java-sdk/pom.xml index bc41b55..17f1a0f 100644 --- a/sdk/dagger-java-sdk/pom.xml +++ b/sdk/dagger-java-sdk/pom.xml @@ -81,6 +81,8 @@ ${daggerengine.version} ${daggerengine.schema} + ${daggerengine.plan} + ${daggerengine.keep} diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/DefaultPath.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/DefaultPath.java index f418dd5..33c6e83 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/DefaultPath.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/module/annotation/DefaultPath.java @@ -8,7 +8,7 @@ /** * Default load path * - *

This applies to io.dagger.client.Directory or io.dagger.client.File types. + *

This applies to io.dagger.core.Directory or io.dagger.core.File types. * *

Path is relative to root directory. */ diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java index 0e6fed1..b74145e 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/AutoCloseableClient.java @@ -1,6 +1,6 @@ package io.dagger.sdk; -import io.dagger.client.Client; +import io.dagger.core.Client; import io.dagger.sdk.engineconn.Connection; public class AutoCloseableClient extends Client implements AutoCloseable { diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java index 4c0dfa5..2eb5b1c 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/Dagger.java @@ -1,6 +1,6 @@ package io.dagger.sdk; -import io.dagger.client.Client; +import io.dagger.core.Client; import io.dagger.sdk.engineconn.Connection; import java.io.IOException; diff --git a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/Telemetry.java b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/Telemetry.java index d1c9e87..381d62f 100644 --- a/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/Telemetry.java +++ b/sdk/dagger-java-sdk/src/main/java/io/dagger/sdk/telemetry/Telemetry.java @@ -1,8 +1,8 @@ package io.dagger.sdk.telemetry; -import io.dagger.client.FunctionCall; -import io.dagger.client.FunctionCallArgValue; -import io.dagger.client.JsonConverter; +import io.dagger.core.FunctionCall; +import io.dagger.core.FunctionCallArgValue; +import io.dagger.core.JsonConverter; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Span; diff --git a/sdk/pom.xml b/sdk/pom.xml index bb7b189..ae80fb4 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -252,6 +252,8 @@ UTF-8 0.21.4 + + 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 b6462a5..5823d92 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 @@ -2,10 +2,10 @@ import static io.dagger.sdk.Dagger.dag; -import io.dagger.client.Container; +import io.dagger.core.Container; import io.dagger.sdk.exception.DaggerQueryException; -import io.dagger.client.Directory; -import io.dagger.client.Workspace; +import io.dagger.core.Directory; +import io.dagger.core.Workspace; import io.dagger.module.annotation.Default; import io.dagger.module.annotation.Function; import io.dagger.module.annotation.Object; diff --git a/templates/empty/pom.xml b/templates/empty/pom.xml index 85da6da..ef9b401 100644 --- a/templates/empty/pom.xml +++ b/templates/empty/pom.xml @@ -168,7 +168,8 @@ entrypoint as compilation roots: - sdk/src/main/java the hand-written SDK library - sdk/src/processor/java the annotation processor (generates the entrypoint) - - sdk/src/generated/java the client bindings generated from the engine schema + - sdk/src/generated/java io.dagger.core, the engine API, plus one + io.dagger.client. per bound module - src/generated/java io.dagger.gen.entrypoint.Entrypoint, generated from the module code (see maven-compiler-plugin) --> diff --git a/templates/legacy/pom.xml b/templates/legacy/pom.xml index 85da6da..ef9b401 100644 --- a/templates/legacy/pom.xml +++ b/templates/legacy/pom.xml @@ -168,7 +168,8 @@ entrypoint as compilation roots: - sdk/src/main/java the hand-written SDK library - sdk/src/processor/java the annotation processor (generates the entrypoint) - - sdk/src/generated/java the client bindings generated from the engine schema + - sdk/src/generated/java io.dagger.core, the engine API, plus one + io.dagger.client. per bound module - src/generated/java io.dagger.gen.entrypoint.Entrypoint, generated from the module code (see maven-compiler-plugin) --> 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 ed3c7e2..77942ac 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 @@ -2,9 +2,9 @@ import static io.dagger.sdk.Dagger.dag; -import io.dagger.client.Container; +import io.dagger.core.Container; import io.dagger.sdk.exception.DaggerQueryException; -import io.dagger.client.Directory; +import io.dagger.core.Directory; import io.dagger.module.annotation.Function; import io.dagger.module.annotation.Object; import java.util.List; From a502be5854812ef549671c18759e2903770a1fa5 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 07:49:45 +0200 Subject: [PATCH 11/17] prebuilt: refresh the committed codegen plugin mod.dang prefers the committed plugin repository under prebuilt/m2 whenever it exists, so every codegen change in this series is inert in module generation until the jar is rebuilt. This is packager:generate's reproducible output for the plugin as it now stands. Signed-off-by: Yves Brissaud --- .../dagger-codegen-maven-plugin-0.21.4.jar | Bin 71713 -> 93990 bytes .../0.21.4/dagger-sdk-parent-0.21.4.pom | 2 ++ 2 files changed, 2 insertions(+) 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 a68c20d6f2dc251f6328c20e9f5eb4df504de445..21c10941eb3da746b6a969cb3b1c4d5b6f13250e 100644 GIT binary patch delta 79691 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 0.21.4 + + + + + + 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.slf4j + slf4j-simple + 2.0.17 + runtime + + + org.eclipse + yasson + 3.0.4 + runtime + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-dagger-sdk-sources + + add-source + + + + sdk/src/main/java + sdk/src/generated/java + + + + + + + maven-compiler-plugin + 3.13.0 + + none + + + + + diff --git a/client.dang b/client.dang new file mode 100644 index 0000000..053901c --- /dev/null +++ b/client.dang @@ -0,0 +1,104 @@ +""" +A generated Java client: a plain Maven project bound to one Dagger module. + +The client is byte-for-byte the package a module receives when it declares that +same module as a dependency — the same generator, the same plan entry — with the +core API and the SDK runtime vendored next to it under sdk/ so the project +builds with a plain `mvn package`. +""" +type Client { + """ + Workspace-root-relative directory the client is written to. + """ + pub rootPath: String! + + """ + The bound module's final name. + """ + pub module: String! = "" + + """ + The bound module's source kind, JSON-encoded as the engine reports it. + """ + pub kindJSON: String! = "" + + """ + The bound module's canonical ref (a git module) or workspace-relative root. + """ + pub canonicalRef: String! = "" + + pub rootSubpath: String! = "" + + pub pin: String! = "" + + pub engineVersion: String! = "" + + """ + The bound module's client-facing schema, as introspection JSON. + """ + pub schemaJSON: String! = "" + + let codegen: Codegen! { Codegen() } + + """ + Generate the client under rootPath: the SDK runtime and the generated packages + under sdk/, and the given pom when the directory has none. Both core and the + client come from the bound module's client-facing schema, which hides nothing: + a client is allowed everything the CLI is. + """ + generate(ws: Workspace!, pom: File!): Changeset! { + let plan = codegen.withClientEntry( + codegen.corePlan(schemaJSON), + module, + kindJSON, + canonicalRef, + rootSubpath, + pin, + engineVersion, + schemaJSON, + ) + let built = codegen.sdkBuilt(plan, directory, "", "client-" + module) + let out = seeded(ws, pom) + .withoutDirectory("sdk") + .withDirectory("sdk/src/main/java", built.directory("/dagger-io/dagger-java-sdk/src/main/java")) + .withDirectory("sdk/src/generated/java", built.directory(codegen.generatedSourcesPath)) + # sdk/ is dropped on the workspace as well as in `out`: withNewDirectory + # merges into what the workspace already holds, so a package this + # generation no longer produces would survive the clean directory. + ws + .withoutDirectory(codegen.workspaceRef(joinPath("sdk"))) + .withNewDirectory(codegen.workspaceRef(rootPath), out) + .changes(ws) + } + + """ + Seed the client directory with the given pom, leaving anything already there + alone: init must never remove a user's files. + """ + init(ws: Workspace!, pom: File!): Changeset! { + ws.withNewDirectory(codegen.workspaceRef(rootPath), seeded(ws, pom)).changes(ws) + } + + """ + The client directory as it is, with the pom added when it has none. + """ + let seeded(ws: Workspace!, pom: File!): Directory! { + let existing = existingDir(ws) + if (existing.exists("pom.xml")) { existing } else { existing.withFile("pom.xml", pom) } + } + + """ + Join the client root with a sub-path, handling the root (".") client. + """ + let joinPath(sub: String!): String! { + if (rootPath == ".") { sub } else { rootPath + "/" + sub } + } + + """ + Existing contents of the client directory, empty when it doesn't exist yet. + """ + let existingDir(ws: Workspace!): Directory! { + let filtered = ws.directory("/", include: [rootPath + "/**"]) + if (filtered.exists(rootPath)) { filtered.directory(rootPath) } else { directory } + } +} diff --git a/main.dang b/main.dang index 0ecc12e..f5fd54b 100644 --- a/main.dang +++ b/main.dang @@ -61,6 +61,14 @@ type JavaSdk { Render a Java init template, substituting the requested module name. """ let renderedTemplate(name: String!, template: String!): Directory! { + renderedTemplateDir(name, "templates/" + template) + } + + """ + Render a template directory of the module source, substituting the requested + module name into every path and file it holds. + """ + let renderedTemplateDir(name: String!, dir: String!): Directory! { container .from("golang:1.25-alpine") .withoutEntrypoint @@ -76,7 +84,7 @@ type JavaSdk { # that then breaks the scaffolded module's build. .withDirectory( "/template", - currentModule.source.directory("templates/" + template), + currentModule.source.directory(dir), exclude: ["**/target/**"], ) .withWorkdir("/helper") @@ -122,4 +130,100 @@ type JavaSdk { .map { mod => mod.generate(ws) }, ) } + + """ + Generate a typed Java client for the module at `module`, written to `path`. + `module` is a workspace path (leading "/", "./" or a bare path) or a git ref. + """ + generateClient(ws: Workspace!, module: String!, path: String!): Changeset! { + let clientPath = cleanClientPath(path) + # A git ref opens on its host, so only a dot in the first segment makes one: + # `libs/my.mod` is a workspace path, `github.com/dagger/hello` is not. + let firstSegment = module.split("/")[0] ?? module + let local = module.hasPrefix("/") + or module.hasPrefix("./") + or module == "." + or (firstSegment.contains(".") == false) + let src = if (local) { + ws.moduleSource("/" + module.trimPrefix("./").trimPrefix("/")) + } else { + moduleSource(refString: module) + } + Client( + rootPath: clientPath, + module: src.moduleName, + kindJSON: JSON.encode(src.kind), + canonicalRef: src.asString, + rootSubpath: src.sourceRootSubpath, + pin: src.pin, + engineVersion: src.engineVersion, + schemaJSON: src.clientSchemaIntrospectionJSON.contents, + ).generate(ws, clientPom(clientPath)) + } + + """ + Seed the SDK-owned files a new Java client needs at `path`: a pom that builds + the generated sources. The engine records the managed client in workspace + config and materializes the client itself through the `@generate` hook. + """ + initClient( + ws: Workspace!, + path: String!, + module: String!, + dev: Boolean! = false, + ): Changeset! { + let clientPath = cleanClientPath(path) + Client(rootPath: clientPath).init(ws, clientPom(clientPath)) + } + + """ + Regenerate every Java client registered on this SDK that is visible from the + client's current location (runs at `dagger generate`). As with modules, the + engine owns the list, the cwd policy, and the resolution of each bound module. + """ + generateAllClient(ws: Workspace!): Changeset! @generate { + changeset.withChangesets( + currentModule + .asSDK(workspace: ws) + .clients + .{{path, moduleSource.{{moduleName, kind, asString, sourceRootSubpath, pin, engineVersion, clientSchemaIntrospectionJSON.{{contents}}}}}} + .map { client => + Client( + rootPath: client.path, + module: client.moduleSource.moduleName, + kindJSON: JSON.encode(client.moduleSource.kind), + canonicalRef: client.moduleSource.asString, + rootSubpath: client.moduleSource.sourceRootSubpath, + pin: client.moduleSource.pin, + engineVersion: client.moduleSource.engineVersion, + schemaJSON: client.moduleSource.clientSchemaIntrospectionJSON.contents, + ).generate(ws, clientPom(client.path)) + }, + ) + } + + """ + The pom of a client at `path`, named after its directory. + """ + let clientPom(path: String!): File! { + let name = if (path == ".") { "client" } else { path.split("/").reduce("client") { acc, seg => seg } } + renderedTemplateDir(name, "client-template").file("pom.xml") + } + + """ + A client path as workspace-root-relative, "." for the root. + """ + let cleanClientPath(path: String!): String! { + let rawPath = path.trimPrefix("./").trimPrefix("/") + if (rawPath == "" or rawPath == ".") { + "." + } else if (rawPath == ".." + or rawPath.trimPrefix("../") != rawPath + or rawPath.contains("/../") + or rawPath.trimSuffix("/..") != rawPath) { + raise "path escapes workspace: " + rawPath + } else { + rawPath.trimSuffix("/") + } + } } diff --git a/main.dang.tmpl b/main.dang.tmpl index 228a5e2..4a6a5f6 100644 --- a/main.dang.tmpl +++ b/main.dang.tmpl @@ -61,6 +61,14 @@ type JavaSdk { Render a Java init template, substituting the requested module name. """ let renderedTemplate(name: String!, template: String!): Directory! { + renderedTemplateDir(name, "templates/" + template) + } + + """ + Render a template directory of the module source, substituting the requested + module name into every path and file it holds. + """ + let renderedTemplateDir(name: String!, dir: String!): Directory! { container .from("golang:1.25-alpine") .withoutEntrypoint @@ -76,7 +84,7 @@ type JavaSdk { # that then breaks the scaffolded module's build. .withDirectory( "/template", - currentModule.source.directory("templates/" + template), + currentModule.source.directory(dir), exclude: ["**/target/**"], ) .withWorkdir("/helper") @@ -122,4 +130,100 @@ type JavaSdk { .map { mod => mod.generate(ws) }, ) } + + """ + Generate a typed Java client for the module at `module`, written to `path`. + `module` is a workspace path (leading "/", "./" or a bare path) or a git ref. + """ + generateClient(ws: Workspace!, module: String!, path: String!): Changeset! { + let clientPath = cleanClientPath(path) + # A git ref opens on its host, so only a dot in the first segment makes one: + # `libs/my.mod` is a workspace path, `github.com/dagger/hello` is not. + let firstSegment = module.split("/")[0] ?? module + let local = module.hasPrefix("/") + or module.hasPrefix("./") + or module == "." + or (firstSegment.contains(".") == false) + let src = if (local) { + ws.moduleSource("/" + module.trimPrefix("./").trimPrefix("/")) + } else { + moduleSource(refString: module) + } + Client( + rootPath: clientPath, + module: src.moduleName, + kindJSON: JSON.encode(src.kind), + canonicalRef: src.asString, + rootSubpath: src.sourceRootSubpath, + pin: src.pin, + engineVersion: src.engineVersion, + schemaJSON: src.clientSchemaIntrospectionJSON.contents, + ).generate(ws, clientPom(clientPath)) + } + + """ + Seed the SDK-owned files a new Java client needs at `path`: a pom that builds + the generated sources. The engine records the managed client in workspace + config and materializes the client itself through the `@generate` hook. + """ + initClient( + ws: Workspace!, + path: String!, + module: String!, + dev: Boolean! = false, + ): Changeset! { + let clientPath = cleanClientPath(path) + Client(rootPath: clientPath).init(ws, clientPom(clientPath)) + } + + """ + Regenerate every Java client registered on this SDK that is visible from the + client's current location (runs at `dagger generate`). As with modules, the + engine owns the list, the cwd policy, and the resolution of each bound module. + """ + generateAllClient(ws: Workspace!): Changeset! @generate { + changeset.withChangesets( + currentModule + .asSDK(workspace: ws) + .clients + .{{path, moduleSource.{{moduleName, kind, asString, sourceRootSubpath, pin, engineVersion, clientSchemaIntrospectionJSON.{{contents}}}}}} + .map { client => + Client( + rootPath: client.path, + module: client.moduleSource.moduleName, + kindJSON: JSON.encode(client.moduleSource.kind), + canonicalRef: client.moduleSource.asString, + rootSubpath: client.moduleSource.sourceRootSubpath, + pin: client.moduleSource.pin, + engineVersion: client.moduleSource.engineVersion, + schemaJSON: client.moduleSource.clientSchemaIntrospectionJSON.contents, + ).generate(ws, clientPom(client.path)) + }, + ) + } + + """ + The pom of a client at `path`, named after its directory. + """ + let clientPom(path: String!): File! { + let name = if (path == ".") { "client" } else { path.split("/").reduce("client") { acc, seg => seg } } + renderedTemplateDir(name, "client-template").file("pom.xml") + } + + """ + A client path as workspace-root-relative, "." for the root. + """ + let cleanClientPath(path: String!): String! { + let rawPath = path.trimPrefix("./").trimPrefix("/") + if (rawPath == "" or rawPath == ".") { + "." + } else if (rawPath == ".." + or rawPath.trimPrefix("../") != rawPath + or rawPath.contains("/../") + or rawPath.trimSuffix("/..") != rawPath) { + raise "path escapes workspace: " + rawPath + } else { + rawPath.trimSuffix("/") + } + } } From 15233654c072c720c563912a9394e9834b8bff64 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 08:05:30 +0200 Subject: [PATCH 14/17] README: document the layout, the clients, and the migration The module tree gains io.dagger.core and one io.dagger.client. per dependency plus the module's own; dependencies become clients and a self call goes through the engine like any other; standalone clients are generated by generate-client and registered clients regenerated by dagger generate; and, with no shim, the import moves every existing module needs are given as a sed one-liner. Signed-off-by: Yves Brissaud --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b677c60..1758c0c 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,80 @@ the generated, committed sources: ``` src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java # generated entrypoint - sdk/src/main/java/... # vendored SDK library + sdk/src/main/java/io/dagger/sdk/... # vendored SDK runtime sdk/src/processor/java/... # vendored annotation processor - sdk/src/generated/java/... # client bindings (from the engine schema) + sdk/src/generated/java/io/dagger/core/... # the core API (Container, Directory, ...) + sdk/src/generated/java/io/dagger/client//... # this module's own client + sdk/src/generated/java/io/dagger/client//... # one client per declared dependency ``` +## Modules have clients, not dependencies + +A dependency declared in `dagger-module.toml` becomes a **generated client**: +a package `io.dagger.client.` holding that module's types and an +entry point on its root type. The module's own API gets the same treatment, so a +self call goes through the engine like any other: + +```java +import static io.dagger.sdk.Dagger.dag; +import static io.dagger.client.hello.Hello.hello; // a dependency named hello +import static io.dagger.client.app.App.app; // this module, named app + +hello(dag()).greet("world"); // the dependency +app(dag()).build(source); // ourselves, through the engine +``` + +`Hello.from(dag())` is the same entry point without the static import. An entry +point serves its module into the session before its first call on a given +client: inside a module the engine has already served it and the serve only +confirms it, in a standalone client it is the bootstrap. Later calls on the same +client skip it. Core types (`io.dagger.core.Container`, `Directory`, ...) are shared by +every client in the tree; a type authored by one module does not cross into +another module's client, which is the rule the engine already enforces for +module APIs. + +## Standalone clients + +The same generator produces a client for any module, for a test or an +application that is not itself a module: + +```sh +dagger call java-sdk generate-client --module= --path=

+``` + +``` +/pom.xml # created when absent, yours afterwards +/sdk/src/main/java/io/dagger/sdk/... # SDK runtime +/sdk/src/generated/java/io/dagger/core/... # core API +/sdk/src/generated/java/io/dagger/client//... # the bound module's client +``` + +`io/dagger/client//**` is byte-identical to what a module depending on +`` receives. Clients registered in the workspace config are regenerated by +`dagger generate` like modules are. With no session in its environment the +client starts one with the `dagger` CLI on the `PATH` (or in +`_EXPERIMENTAL_DAGGER_CLI_BIN`): + +```java +try (var dag = Dagger.connect()) { + hello(dag).greet("world"); +} +``` + +## Migrating a module + +Generated types moved: the runtime from `io.dagger.client` to `io.dagger.sdk`, +the core API to `io.dagger.core`. In a module's own sources: + +```sh +sed -i -E 's/io\.dagger\.client\.(Dagger|AutoCloseableClient|Arguments|IDAbleSerializer|IDAble|InputValue|QueryBuilder|ScalarStringDeserializer|ScalarSerializer|Scalar|FieldsStrategy|ModuleBinding|exception|engineconn|graphql|telemetry)/io.dagger.sdk.\1/g; s/io\.dagger\.client\.([A-Z])/io.dagger.core.\1/g' $(git ls-files 'src/main/java/*.java' 'src/main/java/**/*.java') +dagger generate +``` + +The first rule lists the runtime classes a module writes against by name, +because the second one cannot tell them from a core type. `**` does not match +files directly under `src/main/java`, hence the two patterns. + Because everything is committed and the pom defaults `dagger.proc=none`, the module builds with a plain `mvn package` (no annotation processor at build time) — in an IDE or CI, without Dagger. @@ -53,10 +122,13 @@ module builds with a plain `mvn package` (no annotation processor at build time) ## How generation works `generate` runs Maven in containers it controls: it builds the vendored codegen -plugin, generates the client bindings from the engine's introspection schema, -vendors the SDK library and annotation processor as source, and runs the -processor once to produce the entrypoint. It does not delegate code generation -back to the engine. +plugin, generates `io.dagger.core` from the module-facing schema and one +`io.dagger.client.` per declared dependency from that dependency's +client-facing schema, vendors the SDK runtime and annotation processor as +source, and runs the processor once to produce the entrypoint. With the SDK and +the entrypoint staged the module builds, so the engine can load it and hand back +its own client-facing schema; a second codegen pass turns that into the module's +self client. It does not delegate code generation back to the engine. ## The codegen flag From cf0249e160cdca70c56871d2f852005bf2bcb224 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 08:54:14 +0200 Subject: [PATCH 15/17] e2e: dependencies as clients, the self client, standalone and registered clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real Java modules under fixtures/clients — dep, and app depending on it — carry their own workspace config, which the checks place at the workspace root so the SDK's module registry sees them without touching the module inventory the discovery checks assert. clients-generate-check generates app from nothing and expects core, the dep client with its serve preamble bound by workspace path, and app's own client; then swaps in a source that calls app through that client and generates again, which is the carry-over case; then generates once more with no edits and expects an empty changeset. standalone-client-check generates a client for dep on its own, expects it byte-identical to the one app vendors, expects Host present — a client hides nothing — and builds the project with a plain mvn package around a main that uses it. registered-client-check seeds a client with initClient and materializes it through the generate hook off an as-sdk.clients entry. Signed-off-by: Yves Brissaud --- .../e2e/fixtures/clients/app-self/App.java | 42 ++++ .../fixtures/clients/app/dagger-module.toml | 15 ++ .../main/java/io/dagger/modules/app/App.java | 26 ++ .../e2e/fixtures/clients/dep-client/Main.java | 15 ++ .../fixtures/clients/dep/dagger-module.toml | 5 + .../main/java/io/dagger/modules/dep/Dep.java | 21 ++ .../e2e/fixtures/clients/workspace.toml | 15 ++ .dagger/modules/e2e/main.dang | 225 ++++++++++++++++++ 8 files changed, 364 insertions(+) create mode 100644 .dagger/modules/e2e/fixtures/clients/app-self/App.java create mode 100644 .dagger/modules/e2e/fixtures/clients/app/dagger-module.toml create mode 100644 .dagger/modules/e2e/fixtures/clients/app/src/main/java/io/dagger/modules/app/App.java create mode 100644 .dagger/modules/e2e/fixtures/clients/dep-client/Main.java create mode 100644 .dagger/modules/e2e/fixtures/clients/dep/dagger-module.toml create mode 100644 .dagger/modules/e2e/fixtures/clients/dep/src/main/java/io/dagger/modules/dep/Dep.java create mode 100644 .dagger/modules/e2e/fixtures/clients/workspace.toml diff --git a/.dagger/modules/e2e/fixtures/clients/app-self/App.java b/.dagger/modules/e2e/fixtures/clients/app-self/App.java new file mode 100644 index 0000000..d0ed0ee --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/app-self/App.java @@ -0,0 +1,42 @@ +package io.dagger.modules.app; + +import static io.dagger.client.app.App.app; +import static io.dagger.client.dep.Dep.dep; +import static io.dagger.client.greeter.Greeter.greeter; +import static io.dagger.sdk.Dagger.dag; + +import io.dagger.module.annotation.Function; +import io.dagger.module.annotation.Object; +import io.dagger.sdk.exception.DaggerQueryException; +import java.util.concurrent.ExecutionException; + +@Object +public class App { + /** A call on a dependency, through its generated client. */ + @Function + public String greetViaDep(String name) + throws InterruptedException, ExecutionException, DaggerQueryException { + return dep(dag()).greet(name); + } + + /** A core type returned by the dependency's client, used through core: the same Java type. */ + @Function + public String depFileViaCore() + throws InterruptedException, ExecutionException, DaggerQueryException { + return dep(dag()).scratch().file("dep.txt").contents(); + } + + /** The same dependency under an alias: a second client, on the same session. */ + @Function + public String greetViaAlias(String name) + throws InterruptedException, ExecutionException, DaggerQueryException { + return greeter(dag()).greet(name); + } + + /** A self call, through this module's own generated client. */ + @Function + public String greetSelf(String name) + throws InterruptedException, ExecutionException, DaggerQueryException { + return app(dag()).greetViaDep(name); + } +} diff --git a/.dagger/modules/e2e/fixtures/clients/app/dagger-module.toml b/.dagger/modules/e2e/fixtures/clients/app/dagger-module.toml new file mode 100644 index 0000000..f930219 --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/app/dagger-module.toml @@ -0,0 +1,15 @@ +name = "app" +engineVersion = "v1.0.0-beta.10" + +[runtime] + source = "java" + +[[dependencies]] + name = "dep" + source = "../dep" + +# The same module under another name: the engine applies the alias with +# withName, so its client has to chain and serve "greeter", not "dep". +[[dependencies]] + name = "greeter" + source = "../dep" diff --git a/.dagger/modules/e2e/fixtures/clients/app/src/main/java/io/dagger/modules/app/App.java b/.dagger/modules/e2e/fixtures/clients/app/src/main/java/io/dagger/modules/app/App.java new file mode 100644 index 0000000..73416be --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/app/src/main/java/io/dagger/modules/app/App.java @@ -0,0 +1,26 @@ +package io.dagger.modules.app; + +import static io.dagger.client.dep.Dep.dep; +import static io.dagger.sdk.Dagger.dag; + +import io.dagger.module.annotation.Function; +import io.dagger.module.annotation.Object; +import io.dagger.sdk.exception.DaggerQueryException; +import java.util.concurrent.ExecutionException; + +@Object +public class App { + /** A call on a dependency, through its generated client. */ + @Function + public String greetViaDep(String name) + throws InterruptedException, ExecutionException, DaggerQueryException { + return dep(dag()).greet(name); + } + + /** A core type returned by the dependency's client, used through core: the same Java type. */ + @Function + public String depFileViaCore() + throws InterruptedException, ExecutionException, DaggerQueryException { + return dep(dag()).scratch().file("dep.txt").contents(); + } +} diff --git a/.dagger/modules/e2e/fixtures/clients/dep-client/Main.java b/.dagger/modules/e2e/fixtures/clients/dep-client/Main.java new file mode 100644 index 0000000..1d25bf8 --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/dep-client/Main.java @@ -0,0 +1,15 @@ +package io.dagger.clients.depclient; + +import static io.dagger.client.dep.Dep.dep; + +import io.dagger.sdk.AutoCloseableClient; +import io.dagger.sdk.Dagger; + +/** A standalone client: opens its own session and reaches the module through the preamble. */ +public class Main { + public static void main(String[] args) throws Exception { + try (AutoCloseableClient dag = Dagger.connect()) { + System.out.println(dep(dag).greet("client")); + } + } +} diff --git a/.dagger/modules/e2e/fixtures/clients/dep/dagger-module.toml b/.dagger/modules/e2e/fixtures/clients/dep/dagger-module.toml new file mode 100644 index 0000000..4e12fbe --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/dep/dagger-module.toml @@ -0,0 +1,5 @@ +name = "dep" +engineVersion = "v1.0.0-beta.10" + +[runtime] + source = "java" diff --git a/.dagger/modules/e2e/fixtures/clients/dep/src/main/java/io/dagger/modules/dep/Dep.java b/.dagger/modules/e2e/fixtures/clients/dep/src/main/java/io/dagger/modules/dep/Dep.java new file mode 100644 index 0000000..a492741 --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/dep/src/main/java/io/dagger/modules/dep/Dep.java @@ -0,0 +1,21 @@ +package io.dagger.modules.dep; + +import static io.dagger.sdk.Dagger.dag; + +import io.dagger.core.Directory; +import io.dagger.module.annotation.Function; +import io.dagger.module.annotation.Object; + +@Object +public class Dep { + @Function + public String greet(String name) { + return "hello " + name; + } + + /** A core type handed across the client boundary. */ + @Function + public Directory scratch() { + return dag().directory().withNewFile("dep.txt", "from dep"); + } +} diff --git a/.dagger/modules/e2e/fixtures/clients/workspace.toml b/.dagger/modules/e2e/fixtures/clients/workspace.toml new file mode 100644 index 0000000..27e262a --- /dev/null +++ b/.dagger/modules/e2e/fixtures/clients/workspace.toml @@ -0,0 +1,15 @@ +# Placed at the workspace root by the e2e checks, so that both the SDK's module +# list and the engine's generator rollup for local dependencies see the same +# config. Not named dagger.toml so that find-up from another fixture never +# reads it. +[modules.java-sdk] +source = "." + +[modules.java-sdk.as-sdk] +name = "java" + +[[modules.java-sdk.as-sdk.modules]] +path = ".dagger/modules/e2e/fixtures/clients/dep" + +[[modules.java-sdk.as-sdk.modules]] +path = ".dagger/modules/e2e/fixtures/clients/app" diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 31c4667..e8761cb 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -14,6 +14,12 @@ type E2e { let managedTomlModulePath: String! = fixtureRoot + "/managed-toml/app" # A module using a different SDK; this SDK must never manage it. let nonJavaModulePath: String! = fixtureRoot + "/lookup/not-java" + # Two real Java modules, app depending on dep, registered by a workspace + # config of their own (fixtures/clients/workspace.toml, placed at the root by + # clientsWS) so the module inventory the discovery checks assert stays as it is. + let clientsRoot: String! = fixtureRoot + "/clients" + let depModulePath: String! = clientsRoot + "/dep" + let appModulePath: String! = clientsRoot + "/app" """ Fail the current check when a condition is false. @@ -319,4 +325,223 @@ type E2e { + " }\n" + "}\n" } + + """ + The clients fixtures as buildable modules: both initialized in memory from the + default template, their committed sources and config overlaid, the + fixture-wide skip marker removed so generation runs, and the clients workspace + config at the root. The root is where the engine reads the generator rollup + it scopes a local dependency's generation to, so it has to be the same config + the SDK's own module list comes from; the other fixtures' config is left out + so find-up from the app cannot reach it first. + """ + let clientsWS(ws: Workspace!): Directory! { + let depInit = javaSdk.initModule(ws, name: "dep", path: depModulePath) + let appInit = javaSdk.initModule(ws, name: "app", path: appModulePath) + testWS(ws) + .directory("/", exclude: [fixtureRoot + "/.dagger-java-sdk-skip-generate", fixtureRoot + "/dagger.toml"]) + .withFile("dagger.toml", currentModule.source.file("fixtures/clients/workspace.toml")) + # initModule's changeset is rooted at the workspace, not at the module + .withDirectory(".", depInit.layer) + .withDirectory(".", appInit.layer) + .withDirectory(depModulePath, currentModule.source.directory("fixtures/clients/dep")) + .withDirectory(appModulePath, currentModule.source.directory("fixtures/clients/app")) + } + + let generatedRoot: String! = "sdk/src/generated/java/io/dagger" + + let mavenImage: String! = "maven:3.9.9-eclipse-temurin-21-alpine@sha256:4cbb8bf76c46b97e028998f2486ed014759a8e932480431039bdb93dffe6813e" + + """ + A tree with uniform permissions, so two of them can be compared on their bytes. + + Directory.digest covers permissions, and a changeset layer does not carry the + ones generation produced: the engine writes a module's generated tree into the + workspace at 0666/0777 while a standalone client's lands at 0644/0755, from + identical 0644 input. Verified by exporting both trees. Normalizing here keeps + the comparison a digest — every byte and the whole shape — without asserting + on a mode the workspace, not this SDK, decides. + """ + let sameModes(tree: Directory!): Directory! { + container + .from(mavenImage) + .withoutEntrypoint + .withDirectory("/tree", tree) + .withExec(["sh", "-c", "find /tree -type d -exec chmod 0755 {} + ; find /tree -type f -exec chmod 0644 {} +"]) + .directory("/tree") + } + + """ + Dependencies become clients, and a module gets one for itself. Generating the + app fixture — which depends on dep — from nothing must produce core, the dep + client and app's own client; with that client in place app can call itself, + and generating again picks the new function up; and a run with no edits must + change nothing. + """ + clientsGenerateCheck(ws: Workspace!): Void @check { + let root1 = clientsWS(ws) + let first = javaSdk.generateAll(root1.asWorkspace(cwd: appModulePath)) + assertAdded(first, generatedRoot + "/core/Container.java") + assertAdded(first, generatedRoot + "/client/dep/Dep.java") + assertAdded(first, generatedRoot + "/client/greeter/Greeter.java") + assertAdded(first, generatedRoot + "/client/app/App.java") + assertAdded(first, "src/generated/java/io/dagger/gen/entrypoint/Entrypoint.java") + assert( + contains(first.addedPaths, generatedRoot + "/core/Host.java") == false, + "Host must stay hidden from module code", + ) + + let dep = first.layer.file(generatedRoot + "/client/dep/Dep.java").contents + assertContains(dep, "public static Dep from(Client dag)", "the dependency client should expose from(Client)") + assertContains(dep, "public static Dep dep(Client dag)", "the dependency client should expose the static-import alias") + assertContains( + dep, + "ModuleBinding.ensureServed(root, \"dep\", \"LOCAL_SOURCE\", \"/" + depModulePath + "\", \"\")", + "the dependency client should serve dep by its workspace path", + ) + assertContains(dep, "import io.dagger.core.Directory;", "a core type returned by the dependency should resolve to io.dagger.core") + + # app declares dep twice, the second time as "greeter": the alias is the + # module's final name, so the aliased client chains and serves that name and + # both clients live on one session. + let greeter = first.layer.file(generatedRoot + "/client/greeter/Greeter.java").contents + assertContains(greeter, "public static Greeter greeter(Client dag)", "the aliased dependency should be named after the alias") + assertContains( + greeter, + "ModuleBinding.ensureServed(root, \"greeter\", \"LOCAL_SOURCE\", \"/" + depModulePath + "\", \"\")", + "the aliased client should serve the alias, from the aliased module's own path", + ) + + let selfClient = first.layer.file(generatedRoot + "/client/app/App.java").contents + assertContains(selfClient, "public static App app(Client dag)", "the module should get a client for itself") + assertContains(selfClient, "greetViaDep(", "the self client should expose the module's functions") + + # With the self client vendored, the module can call itself; the previous + # self client is carried through the first pass so this compiles. The stale + # package stands in for a dependency dropped from dagger-module.toml: it is + # committed, this generation does not produce it, and it has to go. + # A changeset reports a directory that went away as one removed path, not as + # one per file it held. + let stalePath = generatedRoot + "/client/gone/" + let root2 = root1 + .withDirectory(appModulePath, first.layer) + .withNewFile( + appModulePath + "/" + stalePath + "Gone.java", + "package io.dagger.client.gone;\n\npublic class Gone {}\n", + ) + .withFile( + appModulePath + "/src/main/java/io/dagger/modules/app/App.java", + currentModule.source.file("fixtures/clients/app-self/App.java"), + ) + let second = javaSdk.generateAll(root2.asWorkspace(cwd: appModulePath)) + assertContains( + second.layer.file(generatedRoot + "/client/app/App.java").contents, + "greetSelf(", + "the self client should pick up a function added since the last generation", + ) + assert( + contains(second.removedPaths, stalePath), + "a committed client package this generation does not produce should be removed", + ) + + # A changeset removes as well as adds, and withDirectory only merges, so the + # removals are replayed before the layer is applied. + let root3 = second + .removedPaths + .reduce(root2) { dir, removed => dir.withoutDirectory(appModulePath + "/" + removed) } + .withDirectory(appModulePath, second.layer) + let third = javaSdk.generateAll(root3.asWorkspace(cwd: appModulePath)) + assert( + third.addedPaths.length == 0 and third.modifiedPaths.length == 0 and third.removedPaths.length == 0, + "generating an unchanged module again should change nothing", + ) + null + } + + """ + A standalone client for dep is byte-identical to the client app vendors for + it, sees everything a client is allowed to, and builds as a plain Maven + project with a main that uses it. + """ + standaloneClientCheck(ws: Workspace!): Void @check { + let root = clientsWS(ws) + let appChanges = javaSdk.generateAll(root.asWorkspace(cwd: appModulePath)) + let depChanges = javaSdk.generateAll(root.asWorkspace(cwd: depModulePath)) + let withDep = root.withDirectory(depModulePath, depChanges.layer) + + let client = javaSdk.generateClient( + withDep.asWorkspace(cwd: clientsRoot), + module: "/" + depModulePath, + path: clientsRoot + "/dep-client", + ) + assertAdded(client, "dep-client/pom.xml") + assertAdded(client, "dep-client/sdk/src/main/java/io/dagger/sdk/Dagger.java") + assertAdded(client, "dep-client/sdk/src/generated/java/io/dagger/client/dep/Dep.java") + assertAdded(client, "dep-client/sdk/src/generated/java/io/dagger/core/Host.java") + assertContains( + client.layer.file("dep-client/pom.xml").contents, + "dep-client", + "the client pom should be named after its directory", + ) + assert( + sameModes(client.layer.directory("dep-client/" + generatedRoot + "/client/dep")).digest + == sameModes(appChanges.layer.directory(generatedRoot + "/client/dep")).digest, + "the standalone client must be byte-identical to the one app vendors as its dependency", + ) + + let project = client + .layer + .directory("dep-client") + .withFile( + "src/main/java/io/dagger/clients/depclient/Main.java", + currentModule.source.file("fixtures/clients/dep-client/Main.java"), + ) + container + .from(mavenImage) + .withoutEntrypoint + .withMountedCache("/root/.m2", cacheVolume("sdk-java-maven-m2"), sharing: CacheSharingMode.LOCKED) + .withDirectory("/client", project) + .withWorkdir("/client") + .withExec(["mvn", "-q", "package", "-DskipTests", "--no-transfer-progress"]) + .sync + null + } + + """ + A client registered in workspace config is seeded by initClient and + materialized by the generate hook, with the engine resolving the bound module. + """ + registeredClientCheck(ws: Workspace!): Void @check { + let root = clientsWS(ws) + let depChanges = javaSdk.generateAll(root.asWorkspace(cwd: depModulePath)) + let registered = root + .withDirectory(depModulePath, depChanges.layer) + .withNewFile( + "dagger.toml", + currentModule.source.file("fixtures/clients/workspace.toml").contents + + "\n[[modules.java-sdk.as-sdk.clients]]\npath = \"" + + clientsRoot + + "/dep-client\"\nmodule = \"" + + depModulePath + + "\"\n", + ) + + let seeded = javaSdk.initClient(registered.asWorkspace(cwd: clientsRoot), path: clientsRoot + "/dep-client", module: "dep") + assertAdded(seeded, "dep-client/pom.xml") + assert(seeded.addedPaths.length == 1, "initClient should seed the pom and nothing else") + + let withSeed = registered.withDirectory(clientsRoot, seeded.layer) + let all = javaSdk.generateAllClient(withSeed.asWorkspace(cwd: clientsRoot)) + assertAdded(all, "dep-client/sdk/src/generated/java/io/dagger/client/dep/Dep.java") + assertAdded(all, "dep-client/sdk/src/generated/java/io/dagger/core/Client.java") + + let again = javaSdk.generateAllClient( + withSeed.withDirectory(clientsRoot, all.layer).asWorkspace(cwd: clientsRoot), + ) + assert( + again.addedPaths.length == 0 and again.modifiedPaths.length == 0 and again.removedPaths.length == 0, + "regenerating an unchanged registered client should change nothing", + ) + null + } } From 721046912d84d70d06434cc0d269bdea9ee9ebb9 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 08:54:14 +0200 Subject: [PATCH 16/17] packager: lock the shared Maven cache Every generate now runs two installs into the shared ~/.m2 volume, and concurrent installs from parallel checks corrupt maven-metadata-local.xml ("in epilog non whitespace content is not allowed"). Every mount of the volume is CacheSharingMode.LOCKED, so the SDK's containers serialize on it; this is the packager's side of it, the SDK's mounts carry it in their own patches. Signed-off-by: Yves Brissaud --- .dagger/modules/packager/main.dang | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.dagger/modules/packager/main.dang b/.dagger/modules/packager/main.dang index b1f497a..0fbfa59 100644 --- a/.dagger/modules/packager/main.dang +++ b/.dagger/modules/packager/main.dang @@ -30,7 +30,7 @@ type Packager { let codegenPluginRepo(ws: Workspace!): Directory! { mvn .withoutEntrypoint - .withMountedCache("/root/.m2", cacheVolume("sdk-java-maven-m2")) + .withMountedCache("/root/.m2", cacheVolume("sdk-java-maven-m2"), 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"]) @@ -67,7 +67,7 @@ type Packager { .introspectionSchemaJSON mvn .withoutEntrypoint - .withMountedCache("/root/.m2", cacheVolume("sdk-java-maven-m2")) + .withMountedCache("/root/.m2", cacheVolume("sdk-java-maven-m2"), sharing: CacheSharingMode.LOCKED) .withMountedFile("/schema.json", introspectionJSON) .withDirectory("/dagger-io", sdkSource(ws)) .withWorkdir("/dagger-io") From 8e0cc0de0d7039b44457a7d690b7ad0d92bf89aa Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 27 Aug 2026 11:07:23 +0200 Subject: [PATCH 17/17] hack/designs: archive modules-have-clients The feature is implemented and its clients e2e is green in the engine; move the design and plan into done/. Signed-off-by: Yves Brissaud --- hack/designs/{ => done}/2026-08-26-modules-have-clients.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename hack/designs/{ => done}/2026-08-26-modules-have-clients.md (100%) diff --git a/hack/designs/2026-08-26-modules-have-clients.md b/hack/designs/done/2026-08-26-modules-have-clients.md similarity index 100% rename from hack/designs/2026-08-26-modules-have-clients.md rename to hack/designs/done/2026-08-26-modules-have-clients.md