Skip to content

experiment: google.api.http annotations on CommandService (Peaceful Studio internal) - #1

Closed
monsieurleberre wants to merge 2 commits into
mainfrom
experiment/google-api-http-command-service
Closed

experiment: google.api.http annotations on CommandService (Peaceful Studio internal)#1
monsieurleberre wants to merge 2 commits into
mainfrom
experiment/google-api-http-command-service

Conversation

@monsieurleberre

@monsieurleberre monsieurleberre commented May 11, 2026

Copy link
Copy Markdown

Side-by-side rendered comparison (SwaggerHub)

Open both, compare the request/response schemas at the bottom of each operation. The "before" spec references JsCommands (which transitively pulls in numbered duplicates, single-key oneOf envelopes, and inline-duplicated enums); the "after" spec references com.daml.ledger.api.v2.Commands directly with proper proto-qualified types throughout.


Summary

Proof-of-concept for the upstream fix outlined as Option 4 of the C# SDK funding proposal: annotate Canton's .proto files with google.api.http so the JSON HTTP surface can be regenerated cleanly from the protos, bypassing the tapir translation layer that introduces the structural defects catalogued in documentation/dev-funding/openApi/why-openapi-is-not-enough.md.

Scope: one service end-to-end (CommandService, 3 RPCs) + go_package options on the 22 v2 protos + value.proto.

Not for upstream submission yet — internal Peaceful Studio review only. See documentation/dev-funding/openApi/upstream-fork-experiment.md for the full write-up, proposed upstream-PR split, CLA notes, and verification details.

What's in this PR

Change Why
command_service.proto: import "google/api/annotations.proto"; + option (google.api.http) = {...} on each of the 3 RPCs The annotation experiment. Matches the existing tapir routes (/v2/commands/submit-and-wait, submit-and-wait-for-transaction, submit-and-wait-for-reassignment).
22 v2 protos + value.proto: option go_package = "github.com/digital-asset/canton/go/ledger/api/v2;ledgerapiv2"; Precondition for every off-the-shelf OpenAPI tool. Canton has no Go target today, so this option was missing. Harmless to existing C#/Java/Scala codegen.

Built and verified with the canton dependency stack as-is. No build-system changes required for the annotation parsing step.

Verification (reproducible end-to-end)

# 1. Fetch a buf binary
brew install bufbuild/buf/buf

# 2. Make the google.api / google.rpc protos available where buf.work.yaml expects them
#    (sbt's PB.unpackDependencies would normally put them here)
cd canton/community/lib/google-common-protos-scala
mkdir -p target/protobuf_external/google/api target/protobuf_external/google/rpc
curl -sS -o target/protobuf_external/google/api/annotations.proto \
  https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto
curl -sS -o target/protobuf_external/google/api/http.proto \
  https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto
curl -sS -o target/protobuf_external/google/rpc/status.proto \
  https://raw.githubusercontent.com/googleapis/googleapis/master/google/rpc/status.proto

# 3. Verify the annotated proto compiles
cd ../../../community/ledger-api-proto/src/main/protobuf
buf build      # exits 0, no output

# 4. Generate a clean OpenAPI from the annotated proto
mkdir -p /tmp/canton-openapi-gen
cat > /tmp/canton-openapi-gen/buf.gen.yaml <<'YAML'
version: v2
plugins:
  - remote: buf.build/grpc-ecosystem/openapiv2:v2.28.0
    out: gen/openapi
    opt:
      - allow_delete_body=true
      - json_names_for_fields=true
      - openapi_naming_strategy=fqn
YAML

cd /tmp/canton-openapi-gen
buf generate \
  --path "<path-to-fork>/community/ledger-api-proto/src/main/protobuf/com/daml/ledger/api/v2/command_service.proto" \
  "<path-to-fork>/community/ledger-api-proto/src/main/protobuf"

# 5. Inspect
less gen/openapi/com/daml/ledger/api/v2/command_service.swagger.json

The generated OpenAPI (1,372 lines) demonstrably fixes all four type-system defects from why-openapi-is-not-enough.md:

  • No numbered duplicates (Empty1Empty10): types are qualified by proto package (com.daml.ledger.api.v2.Commands, com.daml.ledger.api.v2.Record).
  • No single-key oneOf envelopes: oneof branches emit as sibling optional properties.
  • No inline duplicated enums: each enum extracted once (e.g. com.daml.ledger.api.v2.TransactionShape) and referenced everywhere.
  • DAML payloads are typed: createArguments$ref: com.daml.ledger.api.v2.Record, choiceArgument$ref: com.daml.ledger.api.v2.Value.

Out of scope for this PR (next steps)

  • Annotating the remaining 21–25 RPCs across command_submission_service.proto, command_completion_service.proto, state_service.proto, update_service.proto, package_service.proto, etc.
  • Wiring the google-common-protos-scala includeFilter to also generate Scala bindings for annotations.proto / http.proto, then writing a MethodDescriptor → tapir.Endpoint helper so tapir routes are derived from the annotation instead of hand-written. Feasibility analysis in the companion write-up.
  • Splitting into two upstream PRs to digital-asset/canton (PR-1 = go_package cross-language enablement, PR-2 = annotations + tapir derivation). Not now — internal review first.

…e options

Demonstrates Option 4 from the C# SDK proposal appendix: annotate Canton's
.proto files with google.api.http so the JSON HTTP surface can be derived
from the protos directly, bypassing tapir's translation layer.

Scope (one service end-to-end):

- community/ledger-api-proto/.../v2/command_service.proto: imports
  google/api/annotations.proto and adds an option (google.api.http) block
  to each of the three RPCs (SubmitAndWait, SubmitAndWaitForTransaction,
  SubmitAndWaitForReassignment), matching the existing tapir routes.

- All v2 .proto files (22) plus value.proto: add option go_package next to
  the existing csharp_namespace/java_package options. Required by every
  off-the-shelf OpenAPI generator in the ecosystem. Harmless to existing
  C#/Java/Scala codegen, which does not read this option.

Verified with `buf build`: clean compile. Verified end-to-end OpenAPI
generation with buf.build/grpc-ecosystem/openapiv2:v2.28.0: the output
fixes all four type-system defects catalogued in the C# SDK proposal
(numbered duplicate schemas, single-key oneOf wrappers, inline duplicated
enums, untyped DAML payload fields).

See peacefulstudio/documentation/dev-funding/openApi/upstream-fork-experiment.md
for the full write-up.
Adds review-aid artefacts proving:

- go_package additions are inert for existing Canton consumers
  - source diff: one added line per file, sibling to existing
    csharp_namespace / java_package / java_outer_classname options
  - compiled FileDescriptorSet diff (source_code_info stripped):
    one added line `go_package: "..."` inside `options {}`, nothing
    else changes
  - canton-build-targets.txt: every PB.targets in BuildCommon.scala
    routes to scalapb.gen or PB.gens.java; no Go plugin invoked
    anywhere in the build, so go_package cannot be read

- the annotated proto produces a clean OpenAPI
  - buf.gen.yaml + 1,372-line command_service.swagger.json
  - the "after" side of the SwaggerHub comparison linked from
    the PR description

Internal to the Peaceful Studio fork. To be deleted before any
upstream PR to digital-asset/canton.
monsieurleberre added a commit that referenced this pull request Jul 10, 2026
…ranch

Carries over the go_package-harmlessness proof bundle (before/after
descriptor diffs showing the option is inert for existing codegen) and
the CommandService openapi POC from #1, so this branch is self-contained
once #1 closes as superseded.
@monsieurleberre

Copy link
Copy Markdown
Author

Superseded by #2, which now carries the experiment-evidence/ bundle from this branch (fold commit aa5ec09) and extends the annotation coverage to every HTTP-exposed RPC. Closing per SDK ADR 0012 fork-hygiene decision: fold, close, merge into fork main only.

monsieurleberre added a commit that referenced this pull request Jul 26, 2026
…ranch

Carries over the go_package-harmlessness proof bundle (before/after
descriptor diffs showing the option is inert for existing codegen) and
the CommandService openapi POC from #1, so this branch is self-contained
once #1 closes as superseded.
monsieurleberre added a commit that referenced this pull request Jul 26, 2026
…nAPI (#2)

* experiment: google.api.http annotations on CommandService + go_package options

Demonstrates Option 4 from the C# SDK proposal appendix: annotate Canton's
.proto files with google.api.http so the JSON HTTP surface can be derived
from the protos directly, bypassing tapir's translation layer.

Scope (one service end-to-end):

- community/ledger-api-proto/.../v2/command_service.proto: imports
  google/api/annotations.proto and adds an option (google.api.http) block
  to each of the three RPCs (SubmitAndWait, SubmitAndWaitForTransaction,
  SubmitAndWaitForReassignment), matching the existing tapir routes.

- All v2 .proto files (22) plus value.proto: add option go_package next to
  the existing csharp_namespace/java_package options. Required by every
  off-the-shelf OpenAPI generator in the ecosystem. Harmless to existing
  C#/Java/Scala codegen, which does not read this option.

Verified with `buf build`: clean compile. Verified end-to-end OpenAPI
generation with buf.build/grpc-ecosystem/openapiv2:v2.28.0: the output
fixes all four type-system defects catalogued in the C# SDK proposal
(numbered duplicate schemas, single-key oneOf wrappers, inline duplicated
enums, untyped DAML payload fields).

See peacefulstudio/documentation/dev-funding/openApi/upstream-fork-experiment.md
for the full write-up.

* experiment: extend google.api.http coverage to every JSON-API endpoint

Builds on the CommandService experiment by annotating every other v2 RPC
the JSON API exposes over HTTP, plus adding go_package to the admin/,
testing/, and interactive/ sub-packages so off-the-shelf OpenAPI
generators can resolve the full proto closure.

Annotated services (route source: community/ledger/ledger-json-api Js*Service.scala):

v2/
- CommandSubmissionService:      Submit, SubmitReassignment
- CommandCompletionService:      CompletionStream
- ContractService:               GetContract
- EventQueryService:             GetEventsByContractId
- PackageService:                ListPackages, GetPackage,
                                 GetPackageStatus, ListVettedPackages
- StateService:                  GetActiveContracts, GetActiveContractsPage,
                                 GetConnectedSynchronizers, GetLedgerEnd,
                                 GetLatestPrunedOffsets
- UpdateService:                 GetUpdates, GetUpdateByOffset,
                                 GetUpdateById, GetUpdatesPage
- VersionService:                GetLedgerApiVersion

v2/admin/
- IdentityProviderConfigService: 5 RPCs
- PackageManagementService:      UploadDarFile, ValidateDarFile,
                                 UpdateVettedPackages
- PartyManagementService:        7 HTTP-exposed RPCs
- UserManagementService:         9 RPCs

v2/interactive/
- InteractiveSubmissionService:  6 RPCs

Each annotation matches the existing tapir route (method + path + body
mapping) so the proto-derived OpenAPI is comparable to the current
tapir-emitted document.

Scope notes:
- Server-streaming RPCs (CompletionStream, GetActiveContracts,
  GetUpdates) get only the canonical POST/list mapping; the WebSocket
  GET form is a JSON-API-only convenience that has no representation in
  google.api.http and remains in the AsyncAPI doc.
- Legacy JSON-only paths (/v2/updates/flats|trees|transaction-*,
  /v2/authenticated-user, /v2/definitions/*) are not annotated — they
  either lack a 1:1 proto RPC or are non-gRPC custom endpoints.
- Non-HTTP-exposed RPCs (TimeService, CommandInspectionService,
  ParticipantPruningService, PackageManagementService.ListKnownPackages,
  PartyManagementService.UpdatePartyIdentityProviderId) are deliberately
  left unannotated.

Verified locally with `buf build community/ledger-api-proto/src/main/protobuf`:
clean compile.

* experiment: switch GetActiveContractsPage to POST

The JSON API exposes this as GET with a JSON body containing a required
EventFormat message — non-standard REST, and rejected by
grpc-ecosystem/openapiv2 ("must not set request body when http method
is GET"). Switching to POST is the canonical mapping for a request
with a non-trivial body, and is the single intentional deviation from
the existing tapir route for the proto-derived OpenAPI.

* http-annotations-evidence: burden-of-evidence for full-coverage PR

Mirrors the experiment-evidence pattern from the single-service
experiment, scaled to the full HTTP API:

- openapi-3.0/openapi.yaml — OpenAPI 3.0.3 YAML generated via
  buf.build/community/google-gnostic-openapi:v0.7.0; same major version
  as the existing community/ledger/ledger-json-api/.../openapi.yaml, so
  the two can be diffed directly.
- swagger-2.0/canton-ledger-api.swagger.yaml — Swagger 2.0 YAML via
  buf.build/grpc-ecosystem/openapiv2:v2.28.0; same plugin as the
  upstream CommandService experiment, merged across every annotated
  service.
- coverage-comparison/ — sorted path inventories and the diff between
  the JSON API spec and the proto-derived spec, after normalizing
  path-parameter names (proto field names ↔ tapir placeholder names).
  9 paths-only-in-JSON-API (all deliberate skips: 1 custom JSON-only
  endpoint + 8 deprecated legacy aliases), 0 paths-only-in-proto.
- reproduce.sh — one-shot regen of every artefact in this directory.

Internal to the Peaceful Studio fork. To be deleted before any upstream
PR to digital-asset/canton.

* experiment-evidence: fold #1 evidence bundle into the full-coverage branch

Carries over the go_package-harmlessness proof bundle (before/after
descriptor diffs showing the option is inert for existing codegen) and
the CommandService openapi POC from #1, so this branch is self-contained
once #1 closes as superseded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant