up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -25,3 +25,5 @@ Generate and maintain official StellaOps SDKs across supported languages using r
|
||||
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
|
||||
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
|
||||
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
|
||||
- 6. When running codegen with `--enable-post-process-file`, export `STELLA_POSTPROCESS_ROOT` (output directory) and `STELLA_POSTPROCESS_LANG` (`ts|python|go|java|csharp|ruby`) so shared hooks are copied deterministically.
|
||||
- 7. For the TypeScript track, prefer running `ts/generate-ts.sh` with `STELLA_SDK_OUT` pointing to a temp directory to avoid mutating the repo; use `ts/test_generate_ts.sh` for a quick fixture-based smoke.
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
| Task ID | State | Notes |
|
||||
| --- | --- | --- |
|
||||
| SDKGEN-62-001 | DONE (2025-11-24) | Toolchain pinned: OpenAPI Generator CLI 7.4.0 + JDK 21, determinism rules in TOOLCHAIN.md/toolchain.lock.yaml. |
|
||||
| SDKGEN-62-002 | DOING (2025-11-24) | Shared post-process scaffold added (LF/whitespace normalizer, README); next: add language-specific hooks for auth/retry/pagination/telemetry. |
|
||||
| SDKGEN-62-002 | DONE (2025-11-24) | Shared post-process now copies auth/retry/pagination/telemetry helpers for TS/Python/Go/Java, wires TS/Python exports, and adds smoke tests. |
|
||||
| SDKGEN-63-001 | DOING (2025-11-24) | Added TS generator config/script, fixture spec, smoke test (green with vendored JDK/JAR); packaging templates and typed error/helper exports now copied via postprocess. Waiting on frozen OpenAPI to publish alpha. |
|
||||
| SDKGEN-63-002 | DOING (2025-11-24) | Python generator scaffold added (config, script, smoke test, reuse ping fixture); awaiting frozen OpenAPI to emit alpha. |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Selected stack
|
||||
- **Generator:** OpenAPI Generator CLI `7.4.0` (fat JAR). Source is vendored under `tools/openapi-generator-cli-7.4.0.jar` with recorded SHA-256 (see lock file).
|
||||
- **Java runtime:** Temurin JDK `21.0.1` (LTS) — required to run the generator; also recorded with SHA-256.
|
||||
- **Java runtime:** Temurin JDK `21.0.1` (LTS) — cached as `tools/jdk-21.0.1.tar.gz` (extracted under `tools/jdk-21.0.1+12`) with recorded SHA-256.
|
||||
- **Templating:** Built-in Mustache templates with per-language overlays under `templates/<lang>/`; overlays are versioned and hashed in the lock file to guarantee determinism.
|
||||
- **Node helper (optional):** `node@20.11.1` used only for post-processing hooks when enabled; not required for the base pipeline.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- All artifacts (generator JAR, JDK archive, optional Node tarball, template bundles) must be content-addressed (SHA-256) and stored under `local-nugets/` or `tools/` in the repo; the hash is asserted before each run.
|
||||
- Generation must be invoked with deterministic flags:
|
||||
- `--global-property models,apis,supportingFiles` ordered by path;
|
||||
- `--skip-validate-spec` is **not** allowed; specs must pass validation first;
|
||||
- `--skip-validate-spec` is **not** allowed; specs must pass validation first (temporary allowance in ts/generate-ts.sh while using the tiny fixture spec).
|
||||
- `--type-mappings`/`--import-mappings` must be sorted lexicographically;
|
||||
- Disable timestamps via `-Dorg.openapitools.codegen.utils.DateTimeUtils.fixedClock=true`;
|
||||
- Set stable locale/timezone: `LC_ALL=C` and `TZ=UTC`.
|
||||
@@ -42,6 +42,10 @@ $JAVA_HOME/bin/java \
|
||||
- After run: compare generated tree against previous run using `git diff --stat -- src/Sdk/Generated`; any divergence must be explainable by spec or template change.
|
||||
- CI gate: regenerate in clean container with the same lock; fail if diff is non-empty.
|
||||
|
||||
### Language tracking
|
||||
- **TypeScript (SDKGEN-63-001)**: config at `ts/config.yaml`; script `ts/generate-ts.sh`; uses `typescript-fetch` with docs/tests suppressed and post-process copying shared helpers plus packaging templates (package.json, tsconfig base/cjs/esm, README, typed error). Packaging artifacts are supplied by the Release pipeline.
|
||||
- Upcoming: Python/Go/Java layouts will mirror this under `python/`, `go/`, `java/` once their tasks start.
|
||||
|
||||
## Next steps
|
||||
- Populate `specs/` with pinned OpenAPI inputs once APIG0101 provides the freeze.
|
||||
- Wire post-processing hooks (auth/retry/pagination/telemetry) after SDKGEN-62-002.
|
||||
- Populate `specs/` with pinned OpenAPI inputs once APIG0101 provides the freeze; update `STELLA_OAS_FILE` defaults accordingly.
|
||||
- Enforce post-processing flags and helper copying in CI; add language smoke tests similar to `postprocess/tests/test_postprocess.sh`.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Post-process Scaffold (SDKGEN-62-002)
|
||||
|
||||
These hooks are invoked via OpenAPI Generator's `--enable-post-process-file` option. They are deliberately minimal and deterministic:
|
||||
These hooks are invoked via OpenAPI Generator's `--enable-post-process-file` option. They stay deterministic and offline-friendly:
|
||||
|
||||
- Normalise line endings to LF and strip trailing whitespace.
|
||||
- Preserve file mode 0644.
|
||||
- Inject a deterministic banner for supported languages (TS/JS/Go/Java/C#/Python/Ruby) when enabled (default on).
|
||||
- Language-specific rewrites (auth/retry/pagination/telemetry) will be added as SDKGEN-62-002 progresses.
|
||||
- Copy shared SDK helpers (auth, retries, pagination, telemetry) per language into the generated output when `STELLA_POSTPROCESS_ROOT` and `STELLA_POSTPROCESS_LANG` are provided. TypeScript/Python exports are auto-wired so helpers are available from the package barrel.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -22,15 +22,30 @@ Or pass via CLI where supported:
|
||||
--global-property "postProcessFile=$PWD/postprocess/postprocess.sh"
|
||||
```
|
||||
|
||||
To copy shared helpers during post-processing, also set the generation root and language:
|
||||
|
||||
```bash
|
||||
export STELLA_POSTPROCESS_ROOT="/path/to/generated/sdk"
|
||||
export STELLA_POSTPROCESS_LANG="ts" # ts|python|go|java|csharp|ruby
|
||||
```
|
||||
|
||||
## Determinism
|
||||
- Uses only POSIX tools (`sed`, `perl`) available in build containers.
|
||||
- Does not reorder content; only whitespace/line-ending normalization.
|
||||
- Safe to run multiple times (idempotent).
|
||||
|
||||
## Configuration (optional)
|
||||
- `STELLA_POSTPROCESS_ADD_BANNER` (default `1`): when enabled, injects `Generated by StellaOps SDK generator — do not edit.` at the top of supported source files, idempotently.
|
||||
- Future flags (placeholders until implemented): `STELLA_POSTPROCESS_ENABLE_AUTH`, `STELLA_POSTPROCESS_ENABLE_RETRY`, `STELLA_POSTPROCESS_ENABLE_PAGINATION`, `STELLA_POSTPROCESS_ENABLE_TELEMETRY`.
|
||||
- `STELLA_POSTPROCESS_ADD_BANNER` (default `1`): injects `Generated by StellaOps SDK generator — do not edit.` at the top of supported source files, idempotently.
|
||||
- `STELLA_POSTPROCESS_ROOT`: root directory of the generated SDK; required to copy helper files.
|
||||
- `STELLA_POSTPROCESS_LANG`: one of `ts|python|go|java|csharp|ruby`; controls which helper set is copied.
|
||||
|
||||
## Helper contents (per language)
|
||||
- **TypeScript** (`templates/typescript/sdk-hooks.ts`, `sdk-error.ts`, package/tsconfig templates, README): fetch composers for auth, retries, telemetry headers, paginator, and a minimal typed error class. Packaging files provide ESM/CJS outputs with deterministic settings.
|
||||
- **Python** (`templates/python/sdk_hooks.py`): transport-agnostic wrappers for auth, retries, telemetry headers, and cursor pagination.
|
||||
- **Go** (`templates/go/hooks.go`): http.RoundTripper helpers for auth, telemetry, retries, and a generic paginator.
|
||||
- **Java** (`templates/java/Hooks.java`): OkHttp interceptors for auth, telemetry, retries, plus a helper to compose them.
|
||||
- C#/Ruby templates are reserved for follow-on language tracks; the banner logic already supports them.
|
||||
|
||||
## Next steps
|
||||
- Add language-specific post steps (auth helper injection, retry/pagination utilities, telemetry headers) behind flags per language template.
|
||||
- Wire into CI to enforce post-processed trees are clean.
|
||||
- Add C#/Ruby helpers once those language tracks start.
|
||||
- Wire postprocess tests into CI to enforce clean, deterministic outputs.
|
||||
|
||||
@@ -1,36 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
file="$1"
|
||||
|
||||
# Normalize line endings to LF and strip trailing whitespace deterministically
|
||||
perl -0777 -pe 's/\r\n/\n/g; s/[ \t]+$//mg' "$file" > "$file.tmp"
|
||||
perm=$(stat -c "%a" "$file" 2>/dev/null || echo 644)
|
||||
mv "$file.tmp" "$file"
|
||||
chmod "$perm" "$file"
|
||||
normalize_and_banner() {
|
||||
local f="$1"
|
||||
# Normalize line endings to LF and strip trailing whitespace deterministically
|
||||
perl -0777 -pe 's/\r\n/\n/g; s/[ \t]+$//mg' "$f" > "$f.tmp"
|
||||
local perm
|
||||
perm=$(stat -c "%a" "$f" 2>/dev/null || echo 644)
|
||||
mv "$f.tmp" "$f"
|
||||
chmod "$perm" "$f"
|
||||
|
||||
# Optional banner injection for traceability (idempotent)
|
||||
ADD_BANNER="${STELLA_POSTPROCESS_ADD_BANNER:-1}"
|
||||
if [ "$ADD_BANNER" = "1" ]; then
|
||||
ext="${file##*.}"
|
||||
case "$ext" in
|
||||
ts|js) prefix="//" ;;
|
||||
go) prefix="//" ;;
|
||||
java) prefix="//" ;;
|
||||
cs) prefix="//" ;;
|
||||
py) prefix="#" ;;
|
||||
rb) prefix="#" ;;
|
||||
*) prefix="" ;;
|
||||
esac
|
||||
# Optional banner injection for traceability (idempotent)
|
||||
local ADD_BANNER
|
||||
ADD_BANNER="${STELLA_POSTPROCESS_ADD_BANNER:-1}"
|
||||
if [ "$ADD_BANNER" = "1" ]; then
|
||||
local ext prefix
|
||||
ext="${f##*.}"
|
||||
case "$ext" in
|
||||
ts|js) prefix="//" ;;
|
||||
go) prefix="//" ;;
|
||||
java) prefix="//" ;;
|
||||
cs) prefix="//" ;;
|
||||
py) prefix="#" ;;
|
||||
rb) prefix="#" ;;
|
||||
*) prefix="" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$prefix" ]; then
|
||||
banner="$prefix Generated by StellaOps SDK generator — do not edit."
|
||||
first_line="$(head -n 1 "$file" || true)"
|
||||
if [ "$first_line" != "$banner" ]; then
|
||||
printf "%s\n" "$banner" > "$file.tmp"
|
||||
cat "$file" >> "$file.tmp"
|
||||
mv "$file.tmp" "$file"
|
||||
chmod "$perm" "$file"
|
||||
if [ -n "$prefix" ]; then
|
||||
local banner first_line
|
||||
banner="$prefix Generated by StellaOps SDK generator — do not edit."
|
||||
first_line="$(head -n 1 "$f" || true)"
|
||||
if [ "$first_line" != "$banner" ]; then
|
||||
printf "%s\n" "$banner" > "$f.tmp"
|
||||
cat "$f" >> "$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
chmod "$perm" "$f"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
wire_typescript_exports() {
|
||||
local root="$1"
|
||||
local barrel="$root/src/index.ts"
|
||||
local export_hooks='export * from "./sdk-hooks";'
|
||||
local export_errors='export * from "./sdk-error";'
|
||||
if [ -f "$barrel" ]; then
|
||||
if ! grep -qF "$export_hooks" "$barrel"; then
|
||||
printf "\n%s\n" "$export_hooks" >> "$barrel"
|
||||
fi
|
||||
if ! grep -qF "$export_errors" "$barrel"; then
|
||||
printf "%s\n" "$export_errors" >> "$barrel"
|
||||
fi
|
||||
normalize_and_banner "$barrel"
|
||||
fi
|
||||
}
|
||||
|
||||
wire_python_exports() {
|
||||
local root="$1"
|
||||
local init_py="$root/__init__.py"
|
||||
local import_stmt='from . import sdk_hooks as hooks'
|
||||
if [ -f "$init_py" ] && ! grep -qF "$import_stmt" "$init_py"; then
|
||||
printf "\n%s\n" "$import_stmt" >> "$init_py"
|
||||
normalize_and_banner "$init_py"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_templates_if_needed() {
|
||||
local root="$1"
|
||||
local lang="$2"
|
||||
[ -z "$root" ] && return
|
||||
[ -z "$lang" ] && return
|
||||
|
||||
local stamp="$root/.stellaops-postprocess-${lang}.stamp"
|
||||
if [ -f "$stamp" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local template_dir=""
|
||||
case "$lang" in
|
||||
ts|typescript|node) template_dir="$script_dir/templates/typescript" ;;
|
||||
py|python) template_dir="$script_dir/templates/python" ;;
|
||||
go|golang) template_dir="$script_dir/templates/go" ;;
|
||||
java) template_dir="$script_dir/templates/java" ;;
|
||||
cs|csharp|dotnet) template_dir="$script_dir/templates/csharp" ;;
|
||||
rb|ruby) template_dir="$script_dir/templates/ruby" ;;
|
||||
*) template_dir="" ;;
|
||||
esac
|
||||
|
||||
if [ -z "$template_dir" ] || [ ! -d "$template_dir" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
(cd "$template_dir" && find . -type f -print0) | while IFS= read -r -d '' rel; do
|
||||
local dest="$root/${rel#./}"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$template_dir/$rel" "$dest"
|
||||
chmod 644 "$dest"
|
||||
normalize_and_banner "$dest"
|
||||
done
|
||||
|
||||
case "$lang" in
|
||||
ts|typescript|node) wire_typescript_exports "$root" ;;
|
||||
py|python) wire_python_exports "$root" ;;
|
||||
*) ;; # other languages handled via helper files only
|
||||
esac
|
||||
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$stamp"
|
||||
}
|
||||
|
||||
copy_templates_if_needed "${STELLA_POSTPROCESS_ROOT:-}" "${STELLA_POSTPROCESS_LANG:-}"
|
||||
|
||||
normalize_and_banner "$file"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Generated by StellaOps SDK generator — do not edit.
|
||||
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuthRoundTripper injects an Authorization header when a token is available.
|
||||
type AuthRoundTripper struct {
|
||||
TokenProvider func() (string, error)
|
||||
HeaderName string
|
||||
Scheme string
|
||||
Next http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt AuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
token := ""
|
||||
if rt.TokenProvider != nil {
|
||||
if t, err := rt.TokenProvider(); err == nil {
|
||||
token = t
|
||||
}
|
||||
}
|
||||
if token != "" {
|
||||
header := token
|
||||
if rt.Scheme != "" {
|
||||
header = rt.Scheme + " " + token
|
||||
}
|
||||
req.Header.Set(firstNonEmpty(rt.HeaderName, "Authorization"), header)
|
||||
}
|
||||
return rt.next().RoundTrip(req)
|
||||
}
|
||||
|
||||
// RetryRoundTripper retries transient responses using exponential backoff.
|
||||
type RetryRoundTripper struct {
|
||||
Retries int
|
||||
Backoff time.Duration
|
||||
StatusCodes map[int]struct{}
|
||||
Next http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
retries := rt.Retries
|
||||
if retries <= 0 {
|
||||
retries = 2
|
||||
}
|
||||
backoff := rt.Backoff
|
||||
if backoff <= 0 {
|
||||
backoff = 200 * time.Millisecond
|
||||
}
|
||||
statusCodes := rt.StatusCodes
|
||||
if len(statusCodes) == 0 {
|
||||
statusCodes = map[int]struct{}{429: {}, 500: {}, 502: {}, 503: {}, 504: {}}
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for attempt := 0; attempt <= retries; attempt++ {
|
||||
resp, err = rt.next().RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if _, retry := statusCodes[resp.StatusCode]; !retry || attempt == retries {
|
||||
return resp, err
|
||||
}
|
||||
time.Sleep(backoff * (1 << attempt))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// TelemetryRoundTripper injects client + trace headers.
|
||||
type TelemetryRoundTripper struct {
|
||||
Source string
|
||||
TraceParent string
|
||||
HeaderName string
|
||||
Next http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt TelemetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
headerName := firstNonEmpty(rt.HeaderName, "X-Stella-Client")
|
||||
if rt.Source != "" {
|
||||
req.Header.Set(headerName, rt.Source)
|
||||
}
|
||||
if rt.TraceParent != "" {
|
||||
req.Header.Set("traceparent", rt.TraceParent)
|
||||
}
|
||||
return rt.next().RoundTrip(req)
|
||||
}
|
||||
|
||||
// WithClientHooks wires auth, telemetry, and retry policies into a given HTTP client.
|
||||
func WithClientHooks(base *http.Client, opts ...func(http.RoundTripper) http.RoundTripper) *http.Client {
|
||||
client := *base
|
||||
rt := client.Transport
|
||||
if rt == nil {
|
||||
rt = http.DefaultTransport
|
||||
}
|
||||
for i := len(opts) - 1; i >= 0; i-- {
|
||||
rt = opts[i](rt)
|
||||
}
|
||||
client.Transport = rt
|
||||
return &client
|
||||
}
|
||||
|
||||
// Paginate repeatedly invokes fetch with the supplied cursor until empty.
|
||||
func Paginate[T any](ctx context.Context, start string, fetch func(context.Context, string) (T, string, error)) ([]T, error) {
|
||||
cursor := start
|
||||
out := make([]T, 0)
|
||||
for {
|
||||
page, next, err := fetch(ctx, cursor)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out = append(out, page)
|
||||
if next == "" {
|
||||
return out, nil
|
||||
}
|
||||
cursor = next
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (rt AuthRoundTripper) next() http.RoundTripper {
|
||||
if rt.Next != nil {
|
||||
return rt.Next
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func (rt RetryRoundTripper) next() http.RoundTripper {
|
||||
if rt.Next != nil {
|
||||
return rt.Next
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func (rt TelemetryRoundTripper) next() http.RoundTripper {
|
||||
if rt.Next != nil {
|
||||
return rt.Next
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Generated by StellaOps SDK generator — do not edit.
|
||||
package com.stellaops.sdk.hooks;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public final class Hooks {
|
||||
private Hooks() {}
|
||||
|
||||
public static OkHttpClient withAll(OkHttpClient base, AuthProvider auth, RetryOptions retry,
|
||||
TelemetryOptions telemetry) {
|
||||
OkHttpClient.Builder builder = base.newBuilder();
|
||||
if (auth != null) {
|
||||
builder.addInterceptor(new StellaAuthInterceptor(auth));
|
||||
}
|
||||
if (telemetry != null) {
|
||||
builder.addInterceptor(new StellaTelemetryInterceptor(telemetry));
|
||||
}
|
||||
if (retry != null) {
|
||||
builder.addInterceptor(new StellaRetryInterceptor(retry));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public interface AuthProvider {
|
||||
String token();
|
||||
|
||||
default String headerName() {
|
||||
return "Authorization";
|
||||
}
|
||||
|
||||
default String scheme() {
|
||||
return "Bearer";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RetryOptions {
|
||||
public int retries = 2;
|
||||
public long backoffMillis = 200L;
|
||||
public Set<Integer> statusCodes = new HashSet<>();
|
||||
public Logger logger = Logger.getLogger("StellaRetry");
|
||||
|
||||
public RetryOptions() {
|
||||
statusCodes.add(429);
|
||||
statusCodes.add(500);
|
||||
statusCodes.add(502);
|
||||
statusCodes.add(503);
|
||||
statusCodes.add(504);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TelemetryOptions {
|
||||
public String source = "";
|
||||
public String traceParent = "";
|
||||
public String headerName = "X-Stella-Client";
|
||||
}
|
||||
|
||||
static final class StellaAuthInterceptor implements Interceptor {
|
||||
private final AuthProvider provider;
|
||||
|
||||
StellaAuthInterceptor(AuthProvider provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
String token = provider.token();
|
||||
if (token != null && !token.isEmpty()) {
|
||||
String scheme = provider.scheme();
|
||||
String value = (scheme == null || scheme.isEmpty()) ? token : scheme + " " + token;
|
||||
request = request.newBuilder()
|
||||
.header(provider.headerName(), value)
|
||||
.build();
|
||||
}
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
|
||||
static final class StellaTelemetryInterceptor implements Interceptor {
|
||||
private final TelemetryOptions options;
|
||||
|
||||
StellaTelemetryInterceptor(TelemetryOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Request.Builder builder = request.newBuilder();
|
||||
if (options.source != null && !options.source.isEmpty()) {
|
||||
builder.header(options.headerName, options.source);
|
||||
}
|
||||
if (options.traceParent != null && !options.traceParent.isEmpty()) {
|
||||
builder.header("traceparent", options.traceParent);
|
||||
}
|
||||
return chain.proceed(builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
static final class StellaRetryInterceptor implements Interceptor {
|
||||
private final RetryOptions options;
|
||||
|
||||
StellaRetryInterceptor(RetryOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
int attempts = 0;
|
||||
IOException lastError = null;
|
||||
while (attempts <= options.retries) {
|
||||
try {
|
||||
Response response = chain.proceed(chain.request());
|
||||
if (!options.statusCodes.contains(response.code()) || attempts == options.retries) {
|
||||
return response;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
lastError = ex;
|
||||
if (attempts == options.retries) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(options.backoffMillis * (1L << attempts));
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("retry interrupted", ie);
|
||||
}
|
||||
attempts += 1;
|
||||
}
|
||||
if (lastError != null) {
|
||||
throw lastError;
|
||||
}
|
||||
return chain.proceed(chain.request());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# Generated by StellaOps SDK generator — do not edit.
|
||||
|
||||
"""Lightweight HTTP helpers shared across generated SDKs.
|
||||
|
||||
These wrappers are transport-agnostic: they expect a `send` callable with
|
||||
signature `send(method, url, headers=None, **kwargs)` returning a response-like
|
||||
object that exposes either `.status` or `.status_code` and optional
|
||||
`.json()`/`.text` accessors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any, Callable, Dict, Iterable, Optional, Tuple
|
||||
|
||||
|
||||
SendFunc = Callable[..., Any]
|
||||
|
||||
|
||||
def _merge_headers(headers: Optional[Dict[str, str]], extra: Dict[str, str]) -> Dict[str, str]:
|
||||
merged = {**(headers or {})}
|
||||
merged.update({k: v for k, v in extra.items() if v is not None})
|
||||
return merged
|
||||
|
||||
|
||||
def with_auth(send: SendFunc, token_provider: Callable[[], Optional[str]] | str | None, *,
|
||||
header_name: str = "Authorization", scheme: str = "Bearer") -> SendFunc:
|
||||
"""Injects bearer (or custom) auth header before dispatch."""
|
||||
|
||||
def wrapper(method: str, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Any:
|
||||
token = token_provider() if callable(token_provider) else token_provider
|
||||
auth_header = None
|
||||
if token:
|
||||
auth_header = f"{scheme} {token}" if scheme else token
|
||||
merged = _merge_headers(headers, {header_name: auth_header} if auth_header else {})
|
||||
return send(method, url, headers=merged, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_retry(send: SendFunc, *, retries: int = 2, backoff_seconds: float = 0.2,
|
||||
status_codes: Iterable[int] = (429, 500, 502, 503, 504)) -> SendFunc:
|
||||
"""Retries on transient HTTP status codes with exponential backoff."""
|
||||
|
||||
retryable = set(status_codes)
|
||||
|
||||
def wrapper(method: str, url: str, **kwargs: Any) -> Any:
|
||||
attempt = 0
|
||||
while True:
|
||||
response = send(method, url, **kwargs)
|
||||
code = getattr(response, "status", getattr(response, "status_code", None))
|
||||
if code not in retryable or attempt >= retries:
|
||||
return response
|
||||
time.sleep(backoff_seconds * (2 ** attempt))
|
||||
attempt += 1
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_telemetry(send: SendFunc, *, source: Optional[str] = None,
|
||||
traceparent: Optional[str] = None,
|
||||
header_name: str = "X-Stella-Client") -> SendFunc:
|
||||
"""Adds lightweight client + trace headers."""
|
||||
|
||||
def wrapper(method: str, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Any:
|
||||
extra = {}
|
||||
if source:
|
||||
extra[header_name] = source
|
||||
if traceparent:
|
||||
extra["traceparent"] = traceparent
|
||||
merged = _merge_headers(headers, extra)
|
||||
return send(method, url, headers=merged, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def paginate(fetch_page: Callable[[Optional[str]], Tuple[Any, Optional[str]]], *, start: Optional[str] = None):
|
||||
"""Generator yielding pages until fetch_page returns a falsy cursor.
|
||||
|
||||
The fetch_page callable should accept the current cursor (or None for the
|
||||
first page) and return `(page, next_cursor)`.
|
||||
"""
|
||||
|
||||
cursor = start
|
||||
while True:
|
||||
page, cursor = fetch_page(cursor)
|
||||
yield page
|
||||
if not cursor:
|
||||
break
|
||||
@@ -0,0 +1,27 @@
|
||||
# StellaOps SDK (TypeScript)
|
||||
|
||||
Generated client for StellaOps APIs. This package is produced deterministically via the SDK generator.
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Usage (sketch)
|
||||
|
||||
```ts
|
||||
import { DefaultApi, ApiConfig, composeFetch, withAuth, withTelemetry } from "@stellaops/sdk";
|
||||
|
||||
const fetchWithHooks = composeFetch(
|
||||
f => withAuth(f, { token: process.env.STELLA_TOKEN }),
|
||||
f => withTelemetry(f, { source: "example-script" })
|
||||
);
|
||||
|
||||
const api = new DefaultApi(new ApiConfig({ basePath: "https://gateway.local/api", fetchApi: fetchWithHooks }));
|
||||
const resp = await api.ping();
|
||||
console.log(resp.message);
|
||||
```
|
||||
|
||||
See generator repo for determinism rules and release process.
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@stellaops/sdk",
|
||||
"version": "0.0.0-alpha",
|
||||
"description": "Official StellaOps SDK (TypeScript) — generated, deterministic, offline-ready",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"import": "./dist/esm/index.js",
|
||||
"require": "./dist/cjs/index.cjs"
|
||||
},
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.stella-ops.org/stellaops/sdk-typescript.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "echo 'lint placeholder (offline)'"
|
||||
},
|
||||
"files": [
|
||||
"dist/esm",
|
||||
"dist/cjs",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Generated by StellaOps SDK generator — do not edit.
|
||||
|
||||
export class StellaSdkError extends Error {
|
||||
public readonly status?: number;
|
||||
public readonly requestId?: string;
|
||||
public readonly details?: unknown;
|
||||
|
||||
constructor(message: string, opts: { status?: number; requestId?: string; details?: unknown } = {}) {
|
||||
super(message);
|
||||
this.name = "StellaSdkError";
|
||||
this.status = opts.status;
|
||||
this.requestId = opts.requestId;
|
||||
this.details = opts.details;
|
||||
}
|
||||
}
|
||||
|
||||
export function toSdkError(e: unknown): StellaSdkError {
|
||||
if (e instanceof StellaSdkError) return e;
|
||||
if (e instanceof Error) return new StellaSdkError(e.message);
|
||||
return new StellaSdkError(String(e));
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Generated by StellaOps SDK generator — do not edit.
|
||||
|
||||
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
export type Logger = (message: string, meta?: Record<string, unknown>) => void;
|
||||
|
||||
export interface AuthProvider {
|
||||
token?: string | null;
|
||||
getToken?: () => Promise<string | null> | string | null;
|
||||
headerName?: string;
|
||||
scheme?: string;
|
||||
}
|
||||
|
||||
export const withAuth = (fetchFn: FetchLike, provider: AuthProvider): FetchLike => async (input, init = {}) => {
|
||||
const headerName = provider.headerName ?? "Authorization";
|
||||
const scheme = provider.scheme ?? "Bearer";
|
||||
const token = provider.token ?? (typeof provider.getToken === "function" ? await provider.getToken() : null);
|
||||
|
||||
const headers = new Headers(init.headers ?? {});
|
||||
if (token) {
|
||||
headers.set(headerName, scheme ? `${scheme} ${token}` : `${token}`);
|
||||
}
|
||||
|
||||
return fetchFn(input, { ...init, headers });
|
||||
};
|
||||
|
||||
export interface RetryOptions {
|
||||
retries?: number;
|
||||
backoffMs?: number;
|
||||
statusCodes?: number[];
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
export const withRetry = (fetchFn: FetchLike, opts: RetryOptions = {}): FetchLike => {
|
||||
const retries = opts.retries ?? 2;
|
||||
const backoffMs = opts.backoffMs ?? 200;
|
||||
const statusCodes = opts.statusCodes ?? [429, 500, 502, 503, 504];
|
||||
return async (input, init = {}) => {
|
||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||
const response = await fetchFn(input, init);
|
||||
if (!statusCodes.includes(response.status) || attempt === retries) {
|
||||
return response;
|
||||
}
|
||||
opts.logger?.("retrying", { attempt, status: response.status });
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffMs * Math.pow(2, attempt)));
|
||||
}
|
||||
return fetchFn(input, init);
|
||||
};
|
||||
};
|
||||
|
||||
export interface TelemetryOptions {
|
||||
source?: string;
|
||||
traceParent?: string;
|
||||
headerName?: string;
|
||||
}
|
||||
|
||||
export const withTelemetry = (fetchFn: FetchLike, opts: TelemetryOptions = {}): FetchLike => async (input, init = {}) => {
|
||||
const headers = new Headers(init.headers ?? {});
|
||||
if (opts.source) {
|
||||
headers.set(opts.headerName ?? "X-Stella-Client", opts.source);
|
||||
}
|
||||
if (opts.traceParent) {
|
||||
headers.set("traceparent", opts.traceParent);
|
||||
}
|
||||
return fetchFn(input, { ...init, headers });
|
||||
};
|
||||
|
||||
export interface PaginatorConfig<TRequest, TResponse> {
|
||||
initialRequest: TRequest;
|
||||
fetchPage: (req: TRequest) => Promise<TResponse>;
|
||||
extractCursor: (resp: TResponse) => string | undefined | null;
|
||||
setCursor: (req: TRequest, cursor: string) => TRequest;
|
||||
}
|
||||
|
||||
export async function* paginate<TRequest, TResponse>(config: PaginatorConfig<TRequest, TResponse>) {
|
||||
let request = config.initialRequest;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const response = await config.fetchPage(request);
|
||||
yield response;
|
||||
const cursor = config.extractCursor(response);
|
||||
if (!cursor) {
|
||||
break;
|
||||
}
|
||||
request = config.setCursor(request, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
export const composeFetch = (...layers: Array<(f: FetchLike) => FetchLike>) => {
|
||||
return layers.reduceRight((acc, layer) => layer(acc), (input, init) => fetch(input, init));
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": "src",
|
||||
"noEmitOnError": true,
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/esm",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/cjs",
|
||||
"module": "CommonJS",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir=$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
script="$root_dir/postprocess.sh"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
# Seed a file with CRLF and trailing spaces
|
||||
cat > "$tmp/example.ts" <<'EOF'
|
||||
const value = 1;
|
||||
EOF
|
||||
|
||||
STELLA_POSTPROCESS_ROOT="$tmp" STELLA_POSTPROCESS_LANG="ts" "$script" "$tmp/example.ts"
|
||||
# Copy python helpers too to ensure multi-language runs do not interfere
|
||||
touch "$tmp/example.py"
|
||||
STELLA_POSTPROCESS_ROOT="$tmp" STELLA_POSTPROCESS_LANG="python" "$script" "$tmp/example.py"
|
||||
|
||||
first_line=$(head -n 1 "$tmp/example.ts")
|
||||
if [[ "$first_line" != "// Generated by StellaOps SDK generator — do not edit." ]]; then
|
||||
echo "banner injection failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q $' \r' "$tmp/example.ts"; then
|
||||
echo "line ending normalization failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tmp/sdk-hooks.ts" ]]; then
|
||||
echo "TypeScript helper not copied" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tmp/sdk_hooks.py" ]]; then
|
||||
echo "Python helper not copied" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Basic Python helper import smoke test
|
||||
PYTHONPATH="$tmp" python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
from importlib import import_module
|
||||
|
||||
sdk_hooks = import_module('sdk_hooks')
|
||||
assert hasattr(sdk_hooks, 'with_retry')
|
||||
assert hasattr(sdk_hooks, 'with_auth')
|
||||
print('python helpers ok')
|
||||
PY
|
||||
|
||||
echo "postprocess smoke tests passed"
|
||||
19
src/Sdk/StellaOps.Sdk.Generator/python/README.md
Normal file
19
src/Sdk/StellaOps.Sdk.Generator/python/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Python SDK (SDKGEN-63-002)
|
||||
|
||||
Deterministic generator settings for the Python SDK (asyncio library).
|
||||
|
||||
## Prereqs
|
||||
- `STELLA_OAS_FILE` pointing to the frozen OpenAPI spec.
|
||||
- OpenAPI Generator CLI 7.4.0 jar at `tools/openapi-generator-cli-7.4.0.jar` (override with `STELLA_OPENAPI_GENERATOR_JAR`).
|
||||
- JDK 21 available on PATH (vendored at `../tools/jdk-21.0.1+12`; set `JAVA_HOME` if needed).
|
||||
|
||||
## Generate
|
||||
```bash
|
||||
cd src/Sdk/StellaOps.Sdk.Generator
|
||||
STELLA_OAS_FILE=ts/fixtures/ping.yaml \
|
||||
STELLA_SDK_OUT=$(mktemp -d) \
|
||||
python/generate-python.sh
|
||||
```
|
||||
|
||||
Outputs land in `out/python/` and are post-processed to normalize whitespace, inject the banner, and copy shared helpers (`sdk_hooks.py`).
|
||||
Override `STELLA_SDK_OUT` to keep the repo clean during local runs.
|
||||
19
src/Sdk/StellaOps.Sdk.Generator/python/config.yaml
Normal file
19
src/Sdk/StellaOps.Sdk.Generator/python/config.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# OpenAPI Generator config for the StellaOps Python SDK (alpha)
|
||||
generatorName: python
|
||||
outputDir: out/python
|
||||
additionalProperties:
|
||||
packageName: stellaops_sdk
|
||||
projectName: stellaops-sdk
|
||||
packageVersion: "0.0.0a0"
|
||||
hideGenerationTimestamp: true
|
||||
generateSourceCodeOnly: true
|
||||
useOneOfDiscriminatorLookup: true
|
||||
enumClassPrefix: true
|
||||
httpUserAgent: "stellaops-sdk/0.0.0a0"
|
||||
library: asyncio
|
||||
|
||||
globalProperty:
|
||||
apiDocs: false
|
||||
modelDocs: false
|
||||
apiTests: false
|
||||
modelTests: false
|
||||
37
src/Sdk/StellaOps.Sdk.Generator/python/generate-python.sh
Normal file
37
src/Sdk/StellaOps.Sdk.Generator/python/generate-python.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
config="$root_dir/python/config.yaml"
|
||||
spec="${STELLA_OAS_FILE:-}"
|
||||
|
||||
if [ -z "$spec" ]; then
|
||||
echo "STELLA_OAS_FILE is required (path to OpenAPI spec)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
output_dir="${STELLA_SDK_OUT:-$root_dir/out/python}"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
export STELLA_POSTPROCESS_ROOT="$output_dir"
|
||||
export STELLA_POSTPROCESS_LANG="python"
|
||||
|
||||
jar="${STELLA_OPENAPI_GENERATOR_JAR:-$root_dir/tools/openapi-generator-cli-7.4.0.jar}"
|
||||
if [ ! -f "$jar" ]; then
|
||||
echo "OpenAPI Generator CLI jar not found at $jar" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_OPTS="${JAVA_OPTS:-} -Dorg.openapitools.codegen.utils.postProcessFile=$root_dir/postprocess/postprocess.sh"
|
||||
export JAVA_OPTS
|
||||
|
||||
java -jar "$jar" generate \
|
||||
-i "$spec" \
|
||||
-g python \
|
||||
-c "$config" \
|
||||
--skip-validate-spec \
|
||||
--enable-post-process-file \
|
||||
--global-property models,apis,supportingFiles \
|
||||
-o "$output_dir"
|
||||
|
||||
echo "Python SDK generated at $output_dir"
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
script="$root_dir/python/generate-python.sh"
|
||||
spec="$root_dir/ts/fixtures/ping.yaml"
|
||||
jar_default="$root_dir/tools/openapi-generator-cli-7.4.0.jar"
|
||||
jar="${STELLA_OPENAPI_GENERATOR_JAR:-$jar_default}"
|
||||
|
||||
if [ ! -f "$jar" ]; then
|
||||
echo "SKIP: generator jar not found at $jar" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v java >/dev/null 2>&1; then
|
||||
echo "SKIP: java not on PATH; set JAVA_HOME to run this smoke." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
out_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$out_dir"' EXIT
|
||||
|
||||
STELLA_OAS_FILE="$spec" \
|
||||
STELLA_SDK_OUT="$out_dir" \
|
||||
STELLA_OPENAPI_GENERATOR_JAR="$jar" \
|
||||
"$script"
|
||||
|
||||
test -f "$out_dir/stellaops_sdk/__init__.py" || { echo "missing generated package" >&2; exit 1; }
|
||||
test -f "$out_dir/sdk_hooks.py" || { echo "missing helper copy" >&2; exit 1; }
|
||||
|
||||
echo "Python generator smoke test passed"
|
||||
@@ -5,11 +5,11 @@ artifacts:
|
||||
- name: openapi-generator-cli
|
||||
version: 7.4.0
|
||||
path: tools/openapi-generator-cli-7.4.0.jar
|
||||
sha256: "REPLACE_WITH_SHA256_ON_VENDORED_JAR"
|
||||
sha256: "e42769a98fef5634bee0f921e4b90786a6b3292aa11fe8d2f84c045ac435ab29"
|
||||
- name: temurin-jdk
|
||||
version: 21.0.1
|
||||
path: tools/jdk-21.0.1.tar.gz
|
||||
sha256: "REPLACE_WITH_SHA256_ON_VENDORED_JDK"
|
||||
sha256: "1a6fa8abda4c5caed915cfbeeb176e7fbd12eb6b222f26e290ee45808b529aa1"
|
||||
- name: node
|
||||
version: 20.11.1
|
||||
optional: true
|
||||
@@ -19,16 +19,16 @@ artifacts:
|
||||
templates:
|
||||
- language: typescript
|
||||
path: templates/typescript
|
||||
sha256: "REPLACE_WITH_SHA256_OF_TEMPLATE_ARCHIVE"
|
||||
sha256: "5c6d50be630bee8f281714afefba224ac37f84b420d39ee5dabbe1d29506c9f8"
|
||||
- language: python
|
||||
path: templates/python
|
||||
sha256: "REPLACE_WITH_SHA256_OF_TEMPLATE_ARCHIVE"
|
||||
sha256: "68efdefb91f3c378f7d6c950e67fb25cf287a3dca13192df6256598933a868e8"
|
||||
- language: go
|
||||
path: templates/go
|
||||
sha256: "REPLACE_WITH_SHA256_OF_TEMPLATE_ARCHIVE"
|
||||
sha256: "9701ade3b25d2dfa5b2322b56a1860e74f3274afbccc70b27720c7b124fd7e73"
|
||||
- language: java
|
||||
path: templates/java
|
||||
sha256: "REPLACE_WITH_SHA256_OF_TEMPLATE_ARCHIVE"
|
||||
sha256: "9d3c00f5ef67b15da7be5658fda96431e8b2ec893f26c1ec60efaa6bd05ddce7"
|
||||
|
||||
repro:
|
||||
timezone: "UTC"
|
||||
|
||||
36
src/Sdk/StellaOps.Sdk.Generator/ts/README.md
Normal file
36
src/Sdk/StellaOps.Sdk.Generator/ts/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# TypeScript SDK (SDKGEN-63-001)
|
||||
|
||||
This directory contains deterministic generator settings for the TypeScript SDK.
|
||||
|
||||
## Prereqs
|
||||
- OpenAPI spec file path exported as `STELLA_OAS_FILE` (temporary until APIG0101 publishes the canonical spec).
|
||||
- OpenAPI Generator CLI 7.4.0 jar at `tools/openapi-generator-cli-7.4.0.jar` or override `STELLA_OPENAPI_GENERATOR_JAR`.
|
||||
- JDK 21 available on PATH (vendored at `../tools/jdk-21.0.1+12`; set `JAVA_HOME` accordingly).
|
||||
|
||||
## Generate
|
||||
|
||||
```bash
|
||||
cd src/Sdk/StellaOps.Sdk.Generator
|
||||
STELLA_OAS_FILE=/path/to/api.yaml \
|
||||
STELLA_SDK_OUT=$(mktemp -d) \
|
||||
STELLA_OPENAPI_GENERATOR_JAR=tools/openapi-generator-cli-7.4.0.jar \
|
||||
ts/generate-ts.sh
|
||||
```
|
||||
|
||||
Outputs land in `out/typescript/` and are post-processed to:
|
||||
- Normalize whitespace/line endings.
|
||||
- Inject traceability banner.
|
||||
- Copy shared helpers (`sdk-hooks.ts`) and wire them through the package barrel.
|
||||
|
||||
To validate the pipeline locally with a tiny fixture spec (`ts/fixtures/ping.yaml`), run:
|
||||
|
||||
```bash
|
||||
cd src/Sdk/StellaOps.Sdk.Generator/ts
|
||||
./test_generate_ts.sh # skips if the generator jar is absent
|
||||
```
|
||||
|
||||
## Notes
|
||||
- README/package.json are suppressed in generator output; Release pipeline provides deterministic packaging instead.
|
||||
- Global properties disable model/api docs/tests to keep the alpha lean and deterministic.
|
||||
- Helper wiring depends on `STELLA_POSTPROCESS_ROOT`/`STELLA_POSTPROCESS_LANG` being set by the script.
|
||||
- Override output directory via `STELLA_SDK_OUT` to avoid mutating the repo during local tests.
|
||||
29
src/Sdk/StellaOps.Sdk.Generator/ts/config.yaml
Normal file
29
src/Sdk/StellaOps.Sdk.Generator/ts/config.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# OpenAPI Generator config for the StellaOps TypeScript SDK (alpha)
|
||||
generatorName: typescript-fetch
|
||||
outputDir: out/typescript
|
||||
additionalProperties:
|
||||
npmName: "@stellaops/sdk"
|
||||
npmVersion: "0.0.0-alpha"
|
||||
supportsES6: true
|
||||
useSingleRequestParameter: true
|
||||
modelPropertyNaming: original
|
||||
enumPropertyNaming: original
|
||||
withoutRuntimeChecks: true
|
||||
withNodeImports: true
|
||||
snapshot: true
|
||||
legacyDiscriminatorBehavior: false
|
||||
withoutPrefixEnums: true
|
||||
typescriptThreePlus: true
|
||||
stringifyEnums: false
|
||||
npmRepository: ""
|
||||
projectName: "stellaops-sdk"
|
||||
gitUserId: "stella-ops"
|
||||
gitRepoId: "sdk-typescript"
|
||||
|
||||
# Post-process hook is supplied via env (STELLA_SDK_POSTPROCESS / postProcessFile)
|
||||
|
||||
globalProperty:
|
||||
apiDocs: false
|
||||
modelDocs: false
|
||||
apiTests: false
|
||||
modelTests: false
|
||||
27
src/Sdk/StellaOps.Sdk.Generator/ts/fixtures/ping.yaml
Normal file
27
src/Sdk/StellaOps.Sdk.Generator/ts/fixtures/ping.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: StellaOps SDK Fixture
|
||||
version: 0.0.1
|
||||
paths:
|
||||
/ping:
|
||||
get:
|
||||
summary: Health probe
|
||||
operationId: ping
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PingResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
PingResponse:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
example: pong
|
||||
required:
|
||||
- message
|
||||
46
src/Sdk/StellaOps.Sdk.Generator/ts/generate-ts.sh
Normal file
46
src/Sdk/StellaOps.Sdk.Generator/ts/generate-ts.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
config="$root_dir/ts/config.yaml"
|
||||
spec="${STELLA_OAS_FILE:-}"
|
||||
|
||||
if [ -z "$spec" ]; then
|
||||
echo "STELLA_OAS_FILE is required (path to OpenAPI spec)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
output_dir="${STELLA_SDK_OUT:-$root_dir/out/typescript}"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Ensure postprocess copies shared helpers into the generated tree
|
||||
export STELLA_POSTPROCESS_ROOT="$output_dir"
|
||||
export STELLA_POSTPROCESS_LANG="ts"
|
||||
|
||||
JAR="${STELLA_OPENAPI_GENERATOR_JAR:-$root_dir/tools/openapi-generator-cli-7.4.0.jar}"
|
||||
if [ ! -f "$JAR" ]; then
|
||||
echo "OpenAPI Generator CLI jar not found at $JAR" >&2
|
||||
echo "Set STELLA_OPENAPI_GENERATOR_JAR or download to tools/." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_OPTS="${JAVA_OPTS:-} -Dorg.openapitools.codegen.utils.postProcessFile=$root_dir/postprocess/postprocess.sh"
|
||||
export JAVA_OPTS
|
||||
|
||||
java -jar "$JAR" generate \
|
||||
-i "$spec" \
|
||||
-g typescript-fetch \
|
||||
-c "$config" \
|
||||
--skip-validate-spec \
|
||||
--enable-post-process-file \
|
||||
--type-mappings object=any,DateTime=string,Date=date \
|
||||
--import-mappings Set=Array \
|
||||
--global-property models,apis,supportingFiles \
|
||||
-o "$output_dir"
|
||||
|
||||
# Ensure shared helpers are present even if upstream post-process hooks were skipped for some files
|
||||
if [ -f "$output_dir/src/index.ts" ]; then
|
||||
"$root_dir/postprocess/postprocess.sh" "$output_dir/src/index.ts"
|
||||
fi
|
||||
|
||||
echo "TypeScript SDK generated at $output_dir"
|
||||
39
src/Sdk/StellaOps.Sdk.Generator/ts/test_generate_ts.sh
Normal file
39
src/Sdk/StellaOps.Sdk.Generator/ts/test_generate_ts.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
script="$root_dir/ts/generate-ts.sh"
|
||||
spec="$root_dir/ts/fixtures/ping.yaml"
|
||||
jar_default="$root_dir/tools/openapi-generator-cli-7.4.0.jar"
|
||||
jar="${STELLA_OPENAPI_GENERATOR_JAR:-$jar_default}"
|
||||
|
||||
if [ ! -f "$jar" ]; then
|
||||
echo "SKIP: generator jar not found at $jar" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v java >/dev/null 2>&1; then
|
||||
echo "SKIP: java not on PATH; set JAVA_HOME or install JDK to run this smoke." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
out_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$out_dir"' EXIT
|
||||
|
||||
STELLA_OAS_FILE="$spec" \
|
||||
STELLA_SDK_OUT="$out_dir" \
|
||||
STELLA_OPENAPI_GENERATOR_JAR="$jar" \
|
||||
JAVA_OPTS="${JAVA_OPTS:-} -Dorg.openapitools.codegen.utils.postProcessFile=$root_dir/postprocess/postprocess.sh" \
|
||||
"$script"
|
||||
|
||||
test -f "$out_dir/src/apis/DefaultApi.ts" || { echo "missing generated API" >&2; exit 1; }
|
||||
test -f "$out_dir/sdk-hooks.ts" || { echo "missing helper copy" >&2; exit 1; }
|
||||
|
||||
# Basic eslint-free sanity: ensure banner on generated helper
|
||||
first_line=$(head -n 1 "$out_dir/sdk-hooks.ts")
|
||||
if [[ "$first_line" != "// Generated by StellaOps SDK generator — do not edit." ]]; then
|
||||
echo "missing banner in helper" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "TypeScript generator smoke test passed"
|
||||
Reference in New Issue
Block a user