Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions openspec/changes/add-openai-codegen/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Add OpenAPI → ABAP client codegen (`@abapify/openai-codegen`)

## Why

Consuming modern REST/JSON APIs (OpenAI, Petstore, etc.) from ABAP today requires hand-written, boilerplate-heavy clients. We want a **deterministic TypeScript-based generator** that reads an OpenAPI spec and emits a single zero-dependency ABAP class, 100% typed to the spec, that can be shipped into cloud-first SAP systems (BTP / Steampunk) through the existing `adt-cli` deploy flow — and tested against a live ABAP system in this monorepo's CI.

A prior LLM-driven attempt (`abapify/codygen`) showed that non-deterministic generation is hard to maintain. This change picks the opposite approach: **typed AST + pretty-printer**, like `ts.factory` for ABAP.

## What Changes

New packages:

- **`@abapify/abap-ast`** — zero-dependency typed AST nodes and pretty-printer for ABAP classes, interfaces, types, statements, and expressions. Foundation for deterministic code generation. No parser (yet); emission only.
- **`@abapify/openai-codegen`** — OpenAPI → ABAP class code generator. Uses `@apidevtools/swagger-parser` for `$ref` dereferencing + validation, then drives a typed planner → emitter pipeline that produces a single ABAP class per spec. Supports target profiles (`on-prem-classic`, `s4-onprem-modern`, `s4-cloud`) and output formats (`abapgit`, `gcts`).

New sample:

- **`samples/petstore3-client`** — end-to-end proof project. Vendored Petstore v3 OpenAPI spec, a `generate` script, committed generated ABAP (for both abapGit and gCTS layouts), smoke scripts for `adt abap run`, and an ABAP Unit test class. Deployed to the `TRL` ABAP Cloud system in CI.

Scope of first version (`s4-cloud` target only, the others are designed-for but not emitted in v1):

- Full JSON Schema 2020-12 subset: primitives, objects, arrays, enums, `$ref`, `allOf` (flatten/INCLUDE), `oneOf`/`anyOf` with discriminator, nullable.
- All Operation object fields: parameters (path/query/header/cookie with `style`/`explode`), `requestBody`, `responses` (2xx typed return / 4xx/5xx → generated `ZCX_…_ERROR`), `security`, `servers`, `tags`, `deprecated`.
- Security: `apiKey`, `http bearer`, `http basic`. `oauth2` / `openIdConnect` via overridable `ON_AUTHORIZE` hook.
- Inline JSON runtime (tokenizer + per-type serializer/deserializer) emitted into the generated class — no `/ui2/cl_json` or `xco_cp_json` runtime dependency, satisfying Steampunk's whitelist.

Out of scope for v1 (tracked as follow-up):

- `on-prem-classic` and `s4-onprem-modern` runtime emission (the profile definitions exist but aren't wired yet).
- OpenAPI webhooks, callbacks, and full OAuth2 flows.
- Streaming / SSE responses.
- `.aclass` DSL parser (will live under `@abapify/abap-ast` when it's added).

## Affected packages

- **new** `packages/abap-ast/` — AST + printer.
- **new** `packages/openai-codegen/` — generator + CLI.
- **new** `samples/petstore3-client/` — E2E fixture and deployment target.
- **modified** `adt.config.ts` — no change required (codegen is a bun CLI, not an `adt` subcommand).
- **touched** `AGENTS.md` — add new packages to the dependency graph section.

## Architectural impact

New leaf packages with no upstream dependencies inside the monorepo:

```
@abapify/abap-ast (zero deps)
@abapify/openai-codegen ─► @abapify/abap-ast
samples/petstore3-client ─► @abapify/openai-codegen (build-time only)
```

Cloud profile constraint drives one subtle design decision: the generated ABAP must only reference kernel classes on the Steampunk whitelist. `@apidevtools/swagger-parser` stays as a TS **dev / CLI-time** dependency — it never reaches ABAP output.

## Testing & rollback

- Unit: AST printer snapshots, JSON-Schema → ABAP type-mapping matrix, OpenAPI operation → method emission.
- Fixture: regenerate `samples/petstore3-client/generated/**/*.abap` and assert it matches committed output (`git diff --exit-code`).
- E2E: deploy the generated `ZCL_PETSTORE3_CLIENT` to `TRL` via `adt-cli`, run an `adt abap run` smoke snippet, and execute the generated ABAP Unit test class via `adt aunit`.
- Rollback: pure additive change. Deleting the three new directories restores previous behaviour; nothing else depends on them.
31 changes: 31 additions & 0 deletions openspec/changes/add-openai-codegen/specs/abap-ast/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Delta — `abap-ast` capability (new)

## ADDED Requirements

### Requirement: Typed AST nodes for ABAP

`@abapify/abap-ast` SHALL expose typed factory functions for constructing ABAP program structure (classes, interfaces, types, methods, parameters, data declarations, statements, expressions, comments) as a pure TypeScript value graph.

#### Scenario: Building a class AST

- **WHEN** `classDef({ name: 'ZCL_FOO', isFinal: true, sections: [...] })` is called
- **THEN** the returned value is a typed `ClassDef` node whose fields are statically checked by TypeScript and whose shape is documented by the exported type.

### Requirement: Deterministic pretty-printing

The package SHALL expose a `print(node): string` function whose output is stable across machines and runs: 2-space indent, keywords upper-cased, identifiers preserved, declarations ordered by declaration index (no implicit sorting), and no trailing whitespace.

#### Scenario: Snapshot stability

- **GIVEN** a fixed AST graph constructed from the same factory calls
- **WHEN** `print(ast)` is called on two different machines
- **THEN** both outputs are byte-identical.

### Requirement: Zero runtime dependencies

`@abapify/abap-ast` SHALL have zero runtime dependencies and be consumable from any TypeScript 5 ESM environment.

#### Scenario: Installing standalone

- **WHEN** the package is installed outside this monorepo into a vanilla `bun init` project
- **THEN** it resolves with no `dependencies` entries and exposes working `classDef` / `print` imports.
62 changes: 62 additions & 0 deletions openspec/changes/add-openai-codegen/specs/openai-codegen/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Delta — `openai-codegen` capability (new)

## ADDED Requirements

### Requirement: Deterministic OpenAPI → ABAP class generation

The `@abapify/openai-codegen` CLI SHALL convert an OpenAPI 3.0.x / 3.1.x specification into a single ABAP class whose public interface maps 1:1 onto the spec's operations and schemas, such that regenerating from the same input always produces byte-identical output.

#### Scenario: Generating the Petstore v3 client

- **GIVEN** the vendored `samples/petstore3-client/spec/openapi.json`
- **WHEN** `openai-codegen --input ./spec/openapi.json --out ./generated/abapgit --target s4-cloud --format abapgit --class-name ZCL_PETSTORE3_CLIENT --type-prefix ZPS3_` is executed
- **THEN** an abapGit-layout directory is written containing `ZCL_PETSTORE3_CLIENT.clas.abap`, `ZCL_PETSTORE3_CLIENT.clas.xml`, and package manifest files, and re-running the same command leaves `git diff` empty.

#### Scenario: Every schema becomes a typed ABAP TYPES entry

- **GIVEN** an OpenAPI spec with `components.schemas.Pet` referencing `Category` and `Tag`
- **WHEN** the generator runs
- **THEN** the emitted class contains a `TYPES: BEGIN OF ty_ps3_pet ... END OF ty_ps3_pet.` structure whose fields have ABAP types derived from `Category` and `Tag` (themselves emitted as TYPES), and topological ordering ensures referenced types appear before references.

#### Scenario: Every operation becomes an ABAP method

- **GIVEN** operation `findPetsByStatus` with a `query` parameter and a 200 response returning `array of Pet`
- **WHEN** the generator runs
- **THEN** the emitted class has method `FIND_PETS_BY_STATUS` with `IMPORTING iv_status TYPE string` and `RETURNING VALUE(rt_pets) TYPE <pet table type>`, and the body serializes the query parameter per its `style`/`explode`, sends the HTTP request, and deserializes the JSON response into `rt_pets`.

### Requirement: Zero runtime dependencies in generated ABAP

The generated ABAP class targeting `s4-cloud` SHALL only reference system classes on the Steampunk-cloud whitelist (`cl_web_http_client_manager`, `cl_http_destination_provider`, `cl_http_utility`, `cl_system_uuid`, `cl_abap_char_utilities`), with no dependency on `/ui2/cl_json` or any non-kernel class.

#### Scenario: JSON without /ui2/cl_json

- **WHEN** the generator targets `s4-cloud`
- **THEN** the emitted class contains inline private methods for JSON tokenization and per-type serialization/deserialization, and the class does not reference `/ui2/cl_json` or `xco_cp_json` in its source.

#### Scenario: Whitelist enforcement

- **WHEN** the emitter is asked to reference a class that is not on the active profile's whitelist
- **THEN** the generator fails with a descriptive error naming the class and the active profile.

### Requirement: Output format abstraction

The generator SHALL package the emitted class as either an abapGit directory layout or a gCTS payload, selectable via `--format`.

#### Scenario: abapGit layout

- **WHEN** `--format abapgit` is passed
- **THEN** the output directory contains `<CLASS>.clas.abap`, `<CLASS>.clas.xml`, and a `package.devc.xml` in conventional abapGit structure, suitable for `adt-plugin-abapgit` import.

#### Scenario: gCTS layout

- **WHEN** `--format gcts` is passed
- **THEN** the output directory contains the class artefacts in the gCTS-expected structure, suitable for `adt-plugin-gcts` import.

### Requirement: Target-profile class whitelist

Each target profile SHALL declare the set of kernel classes its emitted ABAP may reference, and the generator SHALL refuse to emit references outside that set.

#### Scenario: Cloud profile whitelist

- **GIVEN** `--target s4-cloud`
- **THEN** the emitter restricts HTTP to `cl_web_http_client_manager` / `cl_http_destination_provider` and rejects any attempt to emit `cl_http_client=>create_by_destination`.
48 changes: 48 additions & 0 deletions openspec/changes/add-openai-codegen/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Tasks — `add-openai-codegen`

Waves reflect subagent parallelism. Each task lists the package(s) it touches so waves don't collide.

## Wave 0 — scaffold (sequential, done by lead)

- [x] Create `packages/abap-ast` skeleton (package.json, tsconfig, tsdown, vitest, eslint, `src/index.ts`).
- [x] Create `packages/openai-codegen` skeleton (same template + CLI entry).
- [x] Create `samples/petstore3-client` with vendored `spec/openapi.json`.
- [x] Install workspace deps (`bun install`), verify `bunx nx build` succeeds on empty packages.
- [x] Create this OpenSpec change document.
- [x] Feature branch `feat/openai-codegen-abap` off `main`.

## Wave 1 — foundations (parallel subagents)

- [ ] **abap-ast #1** — node types + factories: `ClassDef`, `InterfaceDef`, `TypeDef`, `TypeRef`, `Section`, `MethodDef`, `MethodParam`, `DataDecl`, `Statement` (assignment / call / raise / if / loop / try / return), `Expr` (literal / identifier / call / binop), `Comment`. Package `packages/abap-ast/src/nodes/`.
- [ ] **openai-codegen #1** — OAS loader & normalizer: wrap `@apidevtools/swagger-parser`, dereference, hoist path-level parameters into operations, collect 2xx / 4xx-5xx response buckets, normalize media types. `packages/openai-codegen/src/oas/`.
- [ ] **openai-codegen #2** — target profiles & class whitelist for `s4-cloud` only (others are type stubs with `TODO`). `packages/openai-codegen/src/profiles/`.

## Wave 2 — emitters (parallel, depend on Wave 1)

- [ ] **abap-ast #2** — pretty-printer + snapshot tests for every node kind in `tests/printer.test.ts`. Deterministic: 2-space indent, UPPER keywords, stable ordering within sections.
- [ ] **openai-codegen #3** — type planner + emitter: JSON Schema → `TypeDef` AST (primitives, array, object, enum, `$ref`, `allOf` flatten, `oneOf`/`anyOf` with discriminator, nullable-as-flag). Includes stable-id allocator (≤30 chars + hash suffix). Tests: mapping matrix per schema shape.
- [ ] **openai-codegen #4** — inline JSON runtime: ABAP fragments (tokenizer + type-specific ser/de) emitted into the class for `s4-cloud`. Stored as plain `.abap` templates under `src/runtime/s4-cloud/` and assembled by the emitter.

## Wave 3 — integration (parallel)

- [ ] **openai-codegen #5** — operation emitter: operationId → `MethodDef`; parameter serialization per `style`/`explode`; request body; response routing (typed 2xx return, `RAISING ZCX_…_ERROR` for 4xx/5xx); local exception class.
- [ ] **openai-codegen #6** — security schemes: `apiKey`, `http bearer`, `http basic` auto-injection; constructor wiring; OAuth2 `ON_AUTHORIZE` hook stub.
- [ ] **openai-codegen #7** — CLI (`commander`): `openai-codegen --input --out --target --format --class-name --type-prefix`; deterministic stdout; non-zero exit on spec validation errors.
- [ ] **openai-codegen #8** — format plugins: emit abapGit layout (`.clas.abap` + `.clas.xml` + devc manifest) and gCTS layout. No new runtime deps required — only TS emission of package layouts.

## Wave 4 — E2E on live TRL (sequential, single agent)

- [ ] Generate `samples/petstore3-client/generated/{abapgit,gcts}`; commit results.
- [ ] `bunx adt deploy samples/petstore3-client/generated/abapgit --package $TMP --system TRL`.
- [ ] Smoke: `bunx adt abap run samples/petstore3-client/e2e/smoke.abap` — instantiate the generated client, call `GET /pet/findByStatus`, print first pet name.
- [ ] Deploy ABAP Unit test class `ZCL_PETSTORE3_CLIENT_TESTS` (hand-written, hits a live path and asserts shape).
- [ ] `bunx adt aunit zcl_petstore3_client_tests` — must pass with 0 failures.
- [ ] Regression: `bunx nx test abap-ast openai-codegen` green; `git diff samples/petstore3-client/generated/` empty after re-running `bun run generate`.

## Wave 5 — verification & PR

- [ ] `bunx nx run-many -t build,test,typecheck,lint -p abap-ast,openai-codegen`
- [ ] `bunx nx format:write`
- [ ] Update root `AGENTS.md` dependency graph.
- [ ] `AGENTS.md` per package (abap-ast, openai-codegen) with conventions.
- [ ] Commit waves as separate commits; push feature branch; open PR with summary + test plan + generated-artifact diff stats.
3 changes: 3 additions & 0 deletions packages/abap-ast/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import baseConfig from '../../eslint.config.mjs';

export default [...baseConfig];
27 changes: 27 additions & 0 deletions packages/abap-ast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@abapify/abap-ast",
"publishConfig": {
"access": "public"
},
"version": "0.1.0",
"description": "Zero-dependency typed AST and pretty-printer for ABAP source code (classes, interfaces, types, statements)",
"type": "module",
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"dependencies": {},
"files": [
"dist",
"README.md"
],
"keywords": [
"abap",
"ast",
"codegen",
"sap"
],
"author": "abapify",
"license": "MIT"
}
2 changes: 2 additions & 0 deletions packages/abap-ast/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nodes';
export * from './printer';
83 changes: 83 additions & 0 deletions packages/abap-ast/src/nodes/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { AbapAstError } from './errors';

/** All node kinds in the AST. */
export type NodeKind =
// types
| 'BuiltinType'
| 'TableType'
| 'StructureType'
| 'NamedTypeRef'
| 'EnumType'
| 'TypeDef'
// data
| 'DataDecl'
| 'ConstantDecl'
| 'FieldSymbolDecl'
// statements
| 'Assign'
| 'Call'
| 'Raise'
| 'If'
| 'Loop'
| 'Return'
| 'Try'
| 'Append'
| 'Insert'
| 'Read'
| 'Clear'
| 'Exit'
| 'Continue'
| 'Raw'
// expressions
| 'Literal'
| 'IdentifierExpr'
| 'ConstructorExpr'
| 'MethodCallExpr'
| 'BinOp'
| 'StringTemplate'
| 'Cast'
// members
| 'MethodParam'
| 'MethodDef'
| 'MethodImpl'
| 'EventDef'
| 'AttributeDef'
// class / interface
| 'Section'
| 'ClassDef'
| 'LocalClassDef'
| 'InterfaceDef'
// shared
| 'Comment';

/** Base shape for any AST node. */
export interface AbapNode {
readonly kind: NodeKind;
}

/** An ABAP identifier (not validated beyond presence). */
export type Identifier = string;

Check warning on line 59 in packages/abap-ast/src/nodes/base.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant type alias and replace its occurrences with "string".

See more on https://sonarcloud.io/project/issues?id=abapify_adt-cli&issues=AZ2p1dpu2G8HK9BFzB0P&open=AZ2p1dpu2G8HK9BFzB0P&pullRequest=107

/** An ABAP comment. Star comments begin at column 1; line comments use `"`. */
export interface Comment extends AbapNode {
readonly kind: 'Comment';
readonly text: string;
readonly style: 'star' | 'line';
}

export function comment(input: {
text: string;
style?: 'star' | 'line';
}): Comment {
if (typeof input.text !== 'string') {
throw new AbapAstError('Comment: required field "text" is missing');
}
return Object.freeze({
kind: 'Comment' as const,
text: input.text,
style: input.style ?? 'line',
});
}

/** Visibility modifier for class members / sections. */
export type Visibility = 'public' | 'protected' | 'private';
Loading
Loading