Skip to content

feat: describe reactor contracts for SDK generation - #3259

Open
neelneelneel wants to merge 3 commits into
devfrom
feat/autogenerate-api-in-sdk
Open

neelneelneel wants to merge 3 commits into
devfrom
feat/autogenerate-api-in-sdk

Conversation

@neelneelneel

@neelneelneel neelneelneel commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

The backend now owns the API contract, and the SDK is generated from it. Monolith exports one OpenAPI 3.1 document, built from its REST annotations and from every Semoss reactor. Each reactor's contract is inferred from what it already declares, and annotations refine it where inference can't see. The TypeScript SDK in libs/sdk generates typed REST calls and a types-only Pixel catalog from that document; a Python SDK can later read the same file.

Everything is opt-in. Existing SDK calls keep their behavior and types, and no reactor has to change to be published.

This change spans three repositories:

  • Semoss reads and annotates reactor contracts.
  • Monolith exports the document.
  • semoss-ui (@semoss/sdk) generates the calls and adds typed hooks.

Changes Made

Semoss

  • ReactorContract reads each reactor's contract from what it already declares. It constructs the reactor to do this but never executes it.
    • Inputs: keysToGet, by canonical name.
    • Required inputs: keyRequired. With none, nothing is required, which is how organizeKeys() checks at runtime.
    • Types: getKeyTypeForMCP.
    • Descriptions: getDescriptionForKey and getReactorDescription, with placeholders where they're missing.
  • Annotations in prerna.openapi:
    • @PixelOutput declares a reactor's output.
    • @PixelInput (repeatable) refines one input's type, item type, description or requiredness.
    • @Internal marks a reactor or REST operation as not for general use.
  • ReactorNames now holds the Pixel-name and frame-type rules, and ReactorFactory uses it too. Registration is unchanged.
  • Pilot reactors: Echo, StopPixelExecution and GetCurrentInsightId are annotated so their published types are pinned. An earlier AbstractReactor2 base class from this branch has been rolled back in favor of inference.
  • Adds the swagger-annotations-jakarta dependency and ReactorContractTest.

Monolith

  • OpenApiExporter and scripts/generate-api write target/generator/openapi.json. Nothing is listed by hand:
    • REST: every @Operation method in the servlets' JAX-RS applications is published.
    • Reactors: every general reactor is published under x-semoss-pixels. The ~150 frame-specific versions are left out, since the frame picks the implementation at runtime.
    • Opting out: swagger's @Hidden leaves a REST method or a reactor out.
    • Markers: @Internal adds x-internal (plus x-internal-reason when given). A contract with anything inferred gets x-semoss-inferred.
    • Modules: x-semoss-servlets names each servlet's SDK module.
  • Skipped reactors: a reactor whose contract can't be read is left out and listed with the reason in target/generator/skipped.txt. That covers a failing constructor, a duplicate key, a non-identifier name, or a name two classes share. The list is the same on every run. An annotated reactor that can't be read fails the export instead.
  • Validation: the document is canonical, so repeated runs produce identical bytes. It's checked with swagger-parser and rejected for OpenAPI 3.0 forms, missing descriptions and duplicate operationIds.
  • GET /api/config: described by a ServerConfig record. ServerConfigContractTest keeps the record in step with what the resource puts, and the response itself is unchanged.
  • Build: an openapi Maven profile builds into target/openapi-build, apart from IDE output in target/classes.
  • Adds JUnit 5, OpenApiExporterTest with fixtures (test/openapi/fixtures, test/openapi/badfixtures), and docs/openapi.md.

semoss-ui (@semoss/sdk)

  • Generator (scripts/sdk-generator):
    • pnpm generate --openapi FILE writes src/generated/. REST calls are plain async functions over the existing get/post, with inline types and TSDoc.
    • pnpm generate:check --openapi FILE reports drift without writing anything.
    • The output is deterministic, whatever the document's key order or the machine's locale.
    • Backend markers become TSDoc: @internal for x-internal, and a note on inferred reactors.
  • REST calls: published as @semoss/sdk/api/*, starting with config from @semoss/sdk/api/monolith.
  • Typed Pixels:
    • PixelCatalog is types only, so it adds no bundle size at any number of reactors.
    • buildPixel is checked against it at compile time and escapes every value.
    • readPixelOutput throws PixelError when the backend reported an error.
    • runPixel, usePixel, useIteratorPixel, runPixelAsync and getPixelAsyncResult get typed overloads. Plain strings and explicit type arguments keep the legacy types.
  • Hooks:
    • useAPI(fn, input, options) passes an AbortSignal, compares its input by value, and takes enabled.
    • useIteratorApi can page a generated call directly: it fills in limit and offset, accepts select, and starts again from page 0 when the input changes, with no deps array. The existing (fetchPage, options, deps) form still works.
  • Adds type-level tests (*.test-d.ts with tsconfig.typecheck.json) and @testing-library/react for the hook tests.

How to Test

  1. Install Semoss, run the Monolith tests, then export and regenerate:
    cd Semoss && mvn -q -DskipTests install
    cd ../Monolith && mvn -q -P openapi test
    scripts/generate-api --skip-semoss-install --sdk <path to semoss-ui>/libs/sdk

Add AbstractReactor2<Input, Output>, so a reactor declares its inputs once
as a record. keysToGet, keyRequired, key descriptions, MCP types and the
typed input are all derived from it, and the return value is wrapped in a
NounMetadata. --help, asMcpTool() and positional inputs keep working.

- Move Echo, GetCurrentInsightId and StopPixelExecution onto
  AbstractReactor2, with the same outputs and messages.
- Add @internal (prerna.openapi) to mark a reactor or REST operation as
  not for general use. Monolith publishes it as x-internal.
- Add @DynamicOutput for reactors whose output has no fixed type.
- Add the swagger-annotations-jakarta dependency and AbstractReactor2Test.
@neelneelneel
neelneelneel requested a review from a team as a code owner November 4, 2026 01:50
@snyk-io

snyk-io Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@neelneelneel

Copy link
Copy Markdown
Contributor Author

Depends on SEMOSS/semoss-ui#3850
Depends on #3259
Depends on SEMOSS/Monolith#468

neelneelneel and others added 2 commits November 3, 2026 21:49
Reactors no longer have to change to be published. ReactorContract reads a
contract from what every reactor already declares:
- inputs from keysToGet (canonical names);
- requiredness from keyRequired (none means nothing is required, as
  organizeKeys() checks);
- input types from getKeyTypeForMCP;
- descriptions from getDescriptionForKey and getReactorDescription.

Annotations in prerna.openapi refine what inference cannot see:
@PixelOutput declares a reactor's output, and @PixelInput (repeatable)
refines one input. Naming a key the reactor does not declare is an error.

- Roll back AbstractReactor2, NoInput and DynamicOutput, and restore Echo,
  StopPixelExecution and GetCurrentInsightId, now annotated so their
  published types are unchanged.
- Move ReactorFactory's Pixel-name and frame-type rules into ReactorNames,
  shared with the contract reader. Registration is unchanged.
- Add ReactorContractTest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants