From d8c63b76377f6702ee2c67719eaf685d202fd9a9 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 17:19:40 -0500
Subject: [PATCH 01/16] chore(internal): improve build scripts
---
generators/csharp/dynamic-snippets/build.cjs | 68 -
.../csharp/dynamic-snippets/package.json | 2 -
generators/csharp/model/build.mjs | 5 +
generators/csharp/model/package.json | 2 +-
generators/csharp/sdk/build.mjs | 5 +
generators/csharp/sdk/package.json | 2 +-
generators/go-v2/base/package.json | 3 -
generators/go-v2/dynamic-snippets/build.cjs | 80 -
.../go-v2/dynamic-snippets/package.json | 4 -
generators/go-v2/model/build.mjs | 5 +
generators/go-v2/model/package.json | 2 +-
generators/go-v2/sdk/build.mjs | 24 +-
generators/go-v2/sdk/package.json | 2 +-
generators/java-v2/base/package.json | 2 -
generators/java-v2/dynamic-snippets/build.cjs | 77 -
.../java-v2/dynamic-snippets/package.json | 4 -
generators/java-v2/sdk/build.mjs | 3 +
generators/java-v2/sdk/package.json | 2 +-
generators/openapi/build.mjs | 3 +
generators/openapi/package.json | 2 +-
generators/php/base/package.json | 3 -
generators/php/dynamic-snippets/build.cjs | 77 -
generators/php/dynamic-snippets/package.json | 4 -
generators/php/model/build.mjs | 5 +
generators/php/model/package.json | 2 +-
generators/php/sdk/build.mjs | 5 +
generators/php/sdk/package.json | 2 +-
generators/postman/build.mjs | 3 +
generators/postman/package.json | 2 +-
.../python-v2/dynamic-snippets/build.cjs | 77 -
.../python-v2/dynamic-snippets/package.json | 4 -
generators/python-v2/fastapi/build.mjs | 3 +
generators/python-v2/fastapi/package.json | 2 +-
generators/python-v2/pydantic-model/build.cjs | 16 -
generators/python-v2/pydantic-model/build.mjs | 7 +
.../python-v2/pydantic-model/package.json | 2 +-
generators/python-v2/sdk/build.mjs | 3 +
generators/python-v2/sdk/package.json | 2 +-
generators/ruby-v2/dynamic-snippets/build.cjs | 77 -
.../ruby-v2/dynamic-snippets/package.json | 4 -
generators/ruby-v2/model/build.mjs | 3 +
generators/ruby-v2/model/package.json | 2 +-
generators/ruby-v2/sdk/build.mjs | 5 +
generators/ruby-v2/sdk/package.json | 2 +-
generators/ruby/model/build.mjs | 3 +
generators/ruby/model/package.json | 2 +-
generators/ruby/sdk/build.mjs | 3 +
generators/ruby/sdk/package.json | 2 +-
generators/rust/dynamic-snippets/build.cjs | 19 -
generators/rust/model/build.mjs | 5 +
generators/rust/model/package.json | 2 +-
generators/rust/sdk/build.mjs | 45 +-
generators/rust/sdk/package.json | 2 +-
generators/swift/dynamic-snippets/build.cjs | 78 -
.../swift/dynamic-snippets/package.json | 4 -
generators/swift/model/build.mjs | 5 +
generators/swift/model/package.json | 2 +-
generators/swift/sdk/build.mjs | 5 +
generators/swift/sdk/package.json | 2 +-
generators/typescript-mcp/model/build.mjs | 5 +
generators/typescript-mcp/model/package.json | 2 +-
generators/typescript-mcp/server/build.mjs | 5 +
generators/typescript-mcp/server/package.json | 2 +-
.../typescript-v2/dynamic-snippets/build.cjs | 77 -
.../dynamic-snippets/package.json | 4 -
generators/typescript/express/cli/build.mjs | 39 +-
generators/typescript/sdk/cli/build.mjs | 41 +-
package.json | 8 +-
packages/cli/cli/build-utils.mjs | 118 +
packages/cli/cli/build.dev.cjs | 91 -
packages/cli/cli/build.dev.mjs | 29 +
packages/cli/cli/build.local.cjs | 80 -
packages/cli/cli/build.local.mjs | 32 +
packages/cli/cli/build.prod-unminified.cjs | 55 -
packages/cli/cli/build.prod-unminified.mjs | 30 +
packages/cli/cli/build.prod.cjs | 92 -
packages/cli/cli/build.prod.mjs | 29 +
packages/cli/cli/package.json | 8 +-
.../protoc-gen-fern/build.tsconfig.json | 28 -
.../generation/protoc-gen-fern/package.json | 1 -
packages/cli/git-diff-1763508841128.patch | 2985 -----------------
packages/configs/build-utils.mjs | 74 +
packages/configs/package.json | 1 +
packages/seed/CLAUDE.md | 2 +-
packages/seed/{build.cjs => build.mjs} | 19 +-
packages/seed/package.json | 2 +-
packages/snippets/core/build.cjs | 77 -
packages/snippets/core/package.json | 4 -
pnpm-lock.yaml | 414 +--
turbo.json | 166 +-
90 files changed, 573 insertions(+), 4735 deletions(-)
delete mode 100644 generators/csharp/dynamic-snippets/build.cjs
create mode 100644 generators/csharp/model/build.mjs
create mode 100644 generators/csharp/sdk/build.mjs
delete mode 100644 generators/go-v2/dynamic-snippets/build.cjs
create mode 100644 generators/go-v2/model/build.mjs
delete mode 100644 generators/java-v2/dynamic-snippets/build.cjs
create mode 100644 generators/java-v2/sdk/build.mjs
create mode 100644 generators/openapi/build.mjs
delete mode 100644 generators/php/dynamic-snippets/build.cjs
create mode 100644 generators/php/model/build.mjs
create mode 100644 generators/php/sdk/build.mjs
create mode 100644 generators/postman/build.mjs
delete mode 100644 generators/python-v2/dynamic-snippets/build.cjs
create mode 100644 generators/python-v2/fastapi/build.mjs
delete mode 100644 generators/python-v2/pydantic-model/build.cjs
create mode 100644 generators/python-v2/pydantic-model/build.mjs
create mode 100644 generators/python-v2/sdk/build.mjs
delete mode 100644 generators/ruby-v2/dynamic-snippets/build.cjs
create mode 100644 generators/ruby-v2/model/build.mjs
create mode 100644 generators/ruby-v2/sdk/build.mjs
create mode 100644 generators/ruby/model/build.mjs
create mode 100644 generators/ruby/sdk/build.mjs
delete mode 100644 generators/rust/dynamic-snippets/build.cjs
create mode 100644 generators/rust/model/build.mjs
delete mode 100644 generators/swift/dynamic-snippets/build.cjs
create mode 100644 generators/swift/model/build.mjs
create mode 100644 generators/swift/sdk/build.mjs
create mode 100644 generators/typescript-mcp/model/build.mjs
create mode 100644 generators/typescript-mcp/server/build.mjs
delete mode 100644 generators/typescript-v2/dynamic-snippets/build.cjs
create mode 100644 packages/cli/cli/build-utils.mjs
delete mode 100644 packages/cli/cli/build.dev.cjs
create mode 100644 packages/cli/cli/build.dev.mjs
delete mode 100644 packages/cli/cli/build.local.cjs
create mode 100644 packages/cli/cli/build.local.mjs
delete mode 100644 packages/cli/cli/build.prod-unminified.cjs
create mode 100644 packages/cli/cli/build.prod-unminified.mjs
delete mode 100644 packages/cli/cli/build.prod.cjs
create mode 100644 packages/cli/cli/build.prod.mjs
delete mode 100644 packages/cli/generation/protoc-gen-fern/build.tsconfig.json
delete mode 100644 packages/cli/git-diff-1763508841128.patch
create mode 100644 packages/configs/build-utils.mjs
rename packages/seed/{build.cjs => build.mjs} (70%)
delete mode 100644 packages/snippets/core/build.cjs
diff --git a/generators/csharp/dynamic-snippets/build.cjs b/generators/csharp/dynamic-snippets/build.cjs
deleted file mode 100644
index c4d1cc026020..000000000000
--- a/generators/csharp/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,68 +0,0 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- platform: "node",
- minify: true,
- dts: true,
- sourcemap: true,
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
\ No newline at end of file
diff --git a/generators/csharp/dynamic-snippets/package.json b/generators/csharp/dynamic-snippets/package.json
index f8b00f059f20..89199b558a39 100644
--- a/generators/csharp/dynamic-snippets/package.json
+++ b/generators/csharp/dynamic-snippets/package.json
@@ -26,7 +26,6 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
@@ -42,7 +41,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/csharp/model/build.mjs b/generators/csharp/model/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/csharp/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/csharp/model/package.json b/generators/csharp/model/package.json
index 5ea87838079c..399d269496e9 100644
--- a/generators/csharp/model/package.json
+++ b/generators/csharp/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-csharp-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/csharp/sdk/build.mjs b/generators/csharp/sdk/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/csharp/sdk/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/csharp/sdk/package.json b/generators/csharp/sdk/package.json
index f81cde7814e9..edeee688ebe8 100644
--- a/generators/csharp/sdk/package.json
+++ b/generators/csharp/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "rm -rf ./lib && rm -rf ./dist && pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-csharp-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/go-v2/base/package.json b/generators/go-v2/base/package.json
index 7f9c9a75bc49..b96fc66c5073 100644
--- a/generators/go-v2/base/package.json
+++ b/generators/go-v2/base/package.json
@@ -26,12 +26,10 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
@@ -49,7 +47,6 @@
"@types/node": "18.15.3",
"dedent": "^1.5.1",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/go-v2/dynamic-snippets/build.cjs b/generators/go-v2/dynamic-snippets/build.cjs
deleted file mode 100644
index b31dc8034194..000000000000
--- a/generators/go-v2/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,80 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- external: [
- '@fern-api/go-formatter',
- ],
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
\ No newline at end of file
diff --git a/generators/go-v2/dynamic-snippets/package.json b/generators/go-v2/dynamic-snippets/package.json
index fca99fafd755..9e23d25d5cf8 100644
--- a/generators/go-v2/dynamic-snippets/package.json
+++ b/generators/go-v2/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -43,7 +40,6 @@
"@fern-api/path-utils": "workspace:*",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/go-v2/model/build.mjs b/generators/go-v2/model/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/go-v2/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/go-v2/model/package.json b/generators/go-v2/model/package.json
index a0c4da2266e1..2c2e2ba50dce 100644
--- a/generators/go-v2/model/package.json
+++ b/generators/go-v2/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-go-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/go-v2/sdk/build.mjs b/generators/go-v2/sdk/build.mjs
index 0f67845e153b..4126ef44c1f0 100644
--- a/generators/go-v2/sdk/build.mjs
+++ b/generators/go-v2/sdk/build.mjs
@@ -1,21 +1,5 @@
-import { join, dirname } from "path";
-import { cp } from "fs/promises";
-import { fileURLToPath } from "url";
-import tsup from "tsup";
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-main();
-
-async function main() {
- const filesFoldersToCopy = [
- ["../base/src/asIs", "./dist/asIs"],
- ];
- for (const [source, destination] of filesFoldersToCopy) {
- await cp(join(__dirname, source), join(__dirname, destination), {
- recursive: true,
- force: true,
- });
- }
-}
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: { from: '../base/src/asIs', to: './dist/asIs' }
+});
diff --git a/generators/go-v2/sdk/package.json b/generators/go-v2/sdk/package.json
index 538f4cca4257..07fbdfa4ff0c 100644
--- a/generators/go-v2/sdk/package.json
+++ b/generators/go-v2/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap && node build.mjs",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/java-v2/base/package.json b/generators/java-v2/base/package.json
index 694989e489e5..d1e2c3ea6512 100644
--- a/generators/java-v2/base/package.json
+++ b/generators/java-v2/base/package.json
@@ -26,12 +26,10 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
diff --git a/generators/java-v2/dynamic-snippets/build.cjs b/generators/java-v2/dynamic-snippets/build.cjs
deleted file mode 100644
index d4c63fb6103e..000000000000
--- a/generators/java-v2/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
\ No newline at end of file
diff --git a/generators/java-v2/dynamic-snippets/package.json b/generators/java-v2/dynamic-snippets/package.json
index c1694dd29e79..76be0c1731f4 100644
--- a/generators/java-v2/dynamic-snippets/package.json
+++ b/generators/java-v2/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -44,7 +41,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/java-v2/sdk/build.mjs b/generators/java-v2/sdk/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/java-v2/sdk/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/java-v2/sdk/package.json b/generators/java-v2/sdk/package.json
index 39077c03c34b..6213056c6ade 100644
--- a/generators/java-v2/sdk/package.json
+++ b/generators/java-v2/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-java-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/openapi/build.mjs b/generators/openapi/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/openapi/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/openapi/package.json b/generators/openapi/package.json
index 7faa4b2f6390..7d3b31ba70de 100644
--- a/generators/openapi/package.json
+++ b/generators/openapi/package.json
@@ -27,7 +27,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-openapi:latest .",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-openapi:${0} .",
"format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
diff --git a/generators/php/base/package.json b/generators/php/base/package.json
index 0f1adbbde0ec..b0737338ca63 100644
--- a/generators/php/base/package.json
+++ b/generators/php/base/package.json
@@ -26,12 +26,10 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
@@ -48,7 +46,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/php/dynamic-snippets/build.cjs b/generators/php/dynamic-snippets/build.cjs
deleted file mode 100644
index 1a4c018666d9..000000000000
--- a/generators/php/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
diff --git a/generators/php/dynamic-snippets/package.json b/generators/php/dynamic-snippets/package.json
index 6a82e1577b9d..d7ff61a9f675 100644
--- a/generators/php/dynamic-snippets/package.json
+++ b/generators/php/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -44,7 +41,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/php/model/build.mjs b/generators/php/model/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/php/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/php/model/package.json b/generators/php/model/package.json
index d22ca7a6ad23..f340334ee9a0 100644
--- a/generators/php/model/package.json
+++ b/generators/php/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-php-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/php/sdk/build.mjs b/generators/php/sdk/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/php/sdk/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/php/sdk/package.json b/generators/php/sdk/package.json
index 180a6b583fc0..771f1ecd60c6 100644
--- a/generators/php/sdk/package.json
+++ b/generators/php/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-php-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/postman/build.mjs b/generators/postman/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/postman/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/postman/package.json b/generators/postman/package.json
index 819ec7ccd3ca..56106cc2d76e 100644
--- a/generators/postman/package.json
+++ b/generators/postman/package.json
@@ -27,7 +27,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-postman:latest .",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-postman:${0} .",
"format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
diff --git a/generators/python-v2/dynamic-snippets/build.cjs b/generators/python-v2/dynamic-snippets/build.cjs
deleted file mode 100644
index 1a4c018666d9..000000000000
--- a/generators/python-v2/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
diff --git a/generators/python-v2/dynamic-snippets/package.json b/generators/python-v2/dynamic-snippets/package.json
index 34d1bfaeb127..dc3d03336bba 100644
--- a/generators/python-v2/dynamic-snippets/package.json
+++ b/generators/python-v2/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -45,7 +42,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/python-v2/fastapi/build.mjs b/generators/python-v2/fastapi/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/python-v2/fastapi/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/python-v2/fastapi/package.json b/generators/python-v2/fastapi/package.json
index a60530df0ba5..75490023d351 100644
--- a/generators/python-v2/fastapi/package.json
+++ b/generators/python-v2/fastapi/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-fastapi-server-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/python-v2/pydantic-model/build.cjs b/generators/python-v2/pydantic-model/build.cjs
deleted file mode 100644
index 31d7e1d680cf..000000000000
--- a/generators/python-v2/pydantic-model/build.cjs
+++ /dev/null
@@ -1,16 +0,0 @@
-const tsup = require('tsup');
-
-main();
-
-async function main() {
- await tsup.build({
- entry: ['src/**/*.ts', '!src/__test__'],
- format: ['cjs'],
- sourcemap: true,
- clean: true,
- outDir: 'dist',
- external: [
- "@wasm-fmt/ruff_fmt",
- ],
- });
-}
\ No newline at end of file
diff --git a/generators/python-v2/pydantic-model/build.mjs b/generators/python-v2/pydantic-model/build.mjs
new file mode 100644
index 000000000000..486e05fa6fc7
--- /dev/null
+++ b/generators/python-v2/pydantic-model/build.mjs
@@ -0,0 +1,7 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ tsupOptions: {
+ external: ['@wasm-fmt/ruff_fmt']
+ }
+});
diff --git a/generators/python-v2/pydantic-model/package.json b/generators/python-v2/pydantic-model/package.json
index 3491711f6519..e7b3f10a4dbc 100644
--- a/generators/python-v2/pydantic-model/package.json
+++ b/generators/python-v2/pydantic-model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && node build.cjs && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-pydantic-model-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/python-v2/sdk/build.mjs b/generators/python-v2/sdk/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/python-v2/sdk/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/python-v2/sdk/package.json b/generators/python-v2/sdk/package.json
index d70867662551..762b39d65666 100644
--- a/generators/python-v2/sdk/package.json
+++ b/generators/python-v2/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-python-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/ruby-v2/dynamic-snippets/build.cjs b/generators/ruby-v2/dynamic-snippets/build.cjs
deleted file mode 100644
index d4c63fb6103e..000000000000
--- a/generators/ruby-v2/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
\ No newline at end of file
diff --git a/generators/ruby-v2/dynamic-snippets/package.json b/generators/ruby-v2/dynamic-snippets/package.json
index 121e0be16e12..d6d69af9a876 100644
--- a/generators/ruby-v2/dynamic-snippets/package.json
+++ b/generators/ruby-v2/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -44,7 +41,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/ruby-v2/model/build.mjs b/generators/ruby-v2/model/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/ruby-v2/model/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/ruby-v2/model/package.json b/generators/ruby-v2/model/package.json
index 639147c20891..09603aeabd58 100644
--- a/generators/ruby-v2/model/package.json
+++ b/generators/ruby-v2/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/ruby-v2/sdk/build.mjs b/generators/ruby-v2/sdk/build.mjs
new file mode 100644
index 000000000000..4126ef44c1f0
--- /dev/null
+++ b/generators/ruby-v2/sdk/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: { from: '../base/src/asIs', to: './dist/asIs' }
+});
diff --git a/generators/ruby-v2/sdk/package.json b/generators/ruby-v2/sdk/package.json
index 964b08cbe1d9..23335ec5e372 100644
--- a/generators/ruby-v2/sdk/package.json
+++ b/generators/ruby-v2/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs && cp -r ../base/src/asIs ./dist/asIs",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-sdk-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/ruby/model/build.mjs b/generators/ruby/model/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/ruby/model/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/ruby/model/package.json b/generators/ruby/model/package.json
index 3a7d25f37f5c..19f99a261550 100644
--- a/generators/ruby/model/package.json
+++ b/generators/ruby/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:${0} ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/ruby/sdk/build.mjs b/generators/ruby/sdk/build.mjs
new file mode 100644
index 000000000000..f062576559e0
--- /dev/null
+++ b/generators/ruby/sdk/build.mjs
@@ -0,0 +1,3 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url));
diff --git a/generators/ruby/sdk/package.json b/generators/ruby/sdk/package.json
index 40c9716ab2a8..489c68646f2e 100644
--- a/generators/ruby/sdk/package.json
+++ b/generators/ruby/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap",
+ "dist:cli": "node build.mjs",
"env:prod": "env-cmd -r .env-cmdrc.cjs -e prod",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
diff --git a/generators/rust/dynamic-snippets/build.cjs b/generators/rust/dynamic-snippets/build.cjs
deleted file mode 100644
index 80daf19a5bf7..000000000000
--- a/generators/rust/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,19 +0,0 @@
-const { pnpPlugin } = require("@yarnpkg/esbuild-plugin-pnp");
-const { build } = require("esbuild");
-const path = require("path");
-const { chmod } = require("fs/promises");
-
-main();
-
-async function main() {
- await build({
- entryPoints: ["./src/**/*.ts"],
- platform: "node",
- target: "node18",
- outdir: "./lib",
- bundle: false,
- plugins: [pnpPlugin()],
- logLevel: "info",
- tsconfig: "./build.tsconfig.json"
- });
-}
\ No newline at end of file
diff --git a/generators/rust/model/build.mjs b/generators/rust/model/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/rust/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/rust/model/package.json b/generators/rust/model/package.json
index 2ec4041c25d0..7b864e4632c5 100644
--- a/generators/rust/model/package.json
+++ b/generators/rust/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "rm -rf ./lib && rm -rf ./dist && pnpm compile && tsup ./src/cli.ts --format cjs && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-rust-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/rust/sdk/build.mjs b/generators/rust/sdk/build.mjs
index 07ff2e360dd4..7750e47703ad 100644
--- a/generators/rust/sdk/build.mjs
+++ b/generators/rust/sdk/build.mjs
@@ -1,38 +1,11 @@
-import { join, dirname } from "path";
-import { cp, mkdir } from "fs/promises";
-import { fileURLToPath } from "url";
-import tsup from "tsup";
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-main();
-
-async function main() {
- // Build with tsup
- await tsup.build({
- entry: ["src/cli.ts"],
- format: ["cjs"],
+buildGenerator(getDirname(import.meta.url), {
+ tsupOptions: {
noExternal: [/@fern-api\/.*/, /dedent/],
- dts: false,
- splitting: false,
- sourcemap: false,
- clean: true,
- outDir: "dist"
- });
-
- // Copy necessary files
- const filesFoldersToCopy = [
- ["./features.yml", "./dist/assets/features.yml"],
- ["../base/src/asIs", "./dist/asIs"]
- ];
-
- for (const [source, destination] of filesFoldersToCopy) {
- const destDir = dirname(join(__dirname, destination));
- await mkdir(destDir, { recursive: true });
- await cp(join(__dirname, source), join(__dirname, destination), {
- recursive: true,
- force: true,
- });
- }
-}
\ No newline at end of file
+ },
+ copyFrom: [
+ { from: './features.yml', to: './dist/assets/features.yml' },
+ { from: '../base/src/asIs', to: './dist/asIs' },
+ ],
+});
diff --git a/generators/rust/sdk/package.json b/generators/rust/sdk/package.json
index 463ec43e4fbb..fd67e3c2d6c9 100644
--- a/generators/rust/sdk/package.json
+++ b/generators/rust/sdk/package.json
@@ -25,7 +25,7 @@
"clean": "rm -rf ./lib && rm -rf ./dist && tsc --build --clean",
"compile": "tsc --build",
"depcheck": "depcheck",
- "dist:cli": "rm -rf ./lib && rm -rf ./dist && pnpm compile && node build.mjs",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-rust-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
diff --git a/generators/swift/dynamic-snippets/build.cjs b/generators/swift/dynamic-snippets/build.cjs
deleted file mode 100644
index 9041a057af78..000000000000
--- a/generators/swift/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,78 +0,0 @@
-const {
- NodeModulesPolyfillPlugin,
-} = require("@esbuild-plugins/node-modules-polyfill");
-const {
- NodeGlobalsPolyfillPlugin,
-} = require("@esbuild-plugins/node-globals-polyfill");
-const packageJson = require("./package.json");
-const tsup = require("tsup");
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ["src/**/*.ts", "!src/__test__"],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true,
- }),
- ],
- tsconfig: "./build.tsconfig.json",
- };
-
- await tsup.build({
- ...config,
- format: ["cjs"],
- outDir: "dist/cjs",
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ["esm"],
- outDir: "dist/esm",
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- import: {
- types: "./esm/index.d.ts",
- default: "./esm/index.js",
- },
- require: {
- types: "./cjs/index.d.cts",
- default: "./cjs/index.cjs",
- },
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: ["cjs", "esm"],
- },
- undefined,
- 2,
- ),
- );
-}
diff --git a/generators/swift/dynamic-snippets/package.json b/generators/swift/dynamic-snippets/package.json
index c069774dbad3..4059ef9db07f 100644
--- a/generators/swift/dynamic-snippets/package.json
+++ b/generators/swift/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -44,7 +41,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.2",
"vitest": "^4.0.8"
}
diff --git a/generators/swift/model/build.mjs b/generators/swift/model/build.mjs
new file mode 100644
index 000000000000..0db93d3ff435
--- /dev/null
+++ b/generators/swift/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: ['../base/src/asIs', '../base/src/template']
+});
diff --git a/generators/swift/model/package.json b/generators/swift/model/package.json
index eeb345bb6c48..09ebf660d7fc 100644
--- a/generators/swift/model/package.json
+++ b/generators/swift/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --sourcemap && cp -R ../base/src/asIs dist && cp -R ../base/src/template dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
diff --git a/generators/swift/sdk/build.mjs b/generators/swift/sdk/build.mjs
new file mode 100644
index 000000000000..0db93d3ff435
--- /dev/null
+++ b/generators/swift/sdk/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: ['../base/src/asIs', '../base/src/template']
+});
diff --git a/generators/swift/sdk/package.json b/generators/swift/sdk/package.json
index 4b90c360e40e..e31f35a77953 100644
--- a/generators/swift/sdk/package.json
+++ b/generators/swift/sdk/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "rm -rf ./lib && rm -rf ./dist && pnpm compile && tsup ./src/cli.ts --format cjs --sourcemap && cp -R ../base/src/asIs dist && cp -R ../base/src/template dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
diff --git a/generators/typescript-mcp/model/build.mjs b/generators/typescript-mcp/model/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/typescript-mcp/model/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/typescript-mcp/model/package.json b/generators/typescript-mcp/model/package.json
index 6e2531226a42..93176670c7b0 100644
--- a/generators/typescript-mcp/model/package.json
+++ b/generators/typescript-mcp/model/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && npm publish",
diff --git a/generators/typescript-mcp/server/build.mjs b/generators/typescript-mcp/server/build.mjs
new file mode 100644
index 000000000000..9556cd96c667
--- /dev/null
+++ b/generators/typescript-mcp/server/build.mjs
@@ -0,0 +1,5 @@
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: '../base/src/asIs'
+});
diff --git a/generators/typescript-mcp/server/package.json b/generators/typescript-mcp/server/package.json
index 254ddb26150b..3b824f646950 100644
--- a/generators/typescript-mcp/server/package.json
+++ b/generators/typescript-mcp/server/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && tsup ./src/cli.ts --sourcemap && cp -R ../base/src/asIs dist",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && npm publish",
diff --git a/generators/typescript-v2/dynamic-snippets/build.cjs b/generators/typescript-v2/dynamic-snippets/build.cjs
deleted file mode 100644
index 1a4c018666d9..000000000000
--- a/generators/typescript-v2/dynamic-snippets/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
diff --git a/generators/typescript-v2/dynamic-snippets/package.json b/generators/typescript-v2/dynamic-snippets/package.json
index ae04983b41ed..bd45002eaeed 100644
--- a/generators/typescript-v2/dynamic-snippets/package.json
+++ b/generators/typescript-v2/dynamic-snippets/package.json
@@ -26,14 +26,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/browser-compatible-base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
@@ -43,7 +40,6 @@
"@fern-api/typescript-browser-compatible-base": "workspace:*",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/typescript/express/cli/build.mjs b/generators/typescript/express/cli/build.mjs
index 9776f80f1399..6dd17ce89722 100644
--- a/generators/typescript/express/cli/build.mjs
+++ b/generators/typescript/express/cli/build.mjs
@@ -1,31 +1,8 @@
-import { join, dirname } from "path";
-import { cp } from "fs/promises";
-import { fileURLToPath } from "url";
-import tsup from "tsup";
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-main();
-
-async function main() {
- await tsup.build({
- entry: ["src/cli.ts"],
- format: ["cjs"],
- minify: false,
- outDir: "dist",
- sourcemap: true,
- clean: true,
- });
-
- const filesFoldersToCopy = [
- ["../../asIs/", "./dist/assets/asIs"],
- ["../../utils/core-utilities/", "./dist/assets/core-utilities"],
- ];
- for (const [source, destination] of filesFoldersToCopy) {
- await cp(join(__dirname, source), join(__dirname, destination), {
- recursive: true,
- force: true,
- });
- }
-}
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
+
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: [
+ { from: '../../asIs/', to: './dist/assets/asIs' },
+ { from: '../../utils/core-utilities/', to: './dist/assets/core-utilities' },
+ ],
+});
diff --git a/generators/typescript/sdk/cli/build.mjs b/generators/typescript/sdk/cli/build.mjs
index e288b319c6c1..8455ff60203e 100644
--- a/generators/typescript/sdk/cli/build.mjs
+++ b/generators/typescript/sdk/cli/build.mjs
@@ -1,35 +1,10 @@
-import { join, dirname } from "path";
-import { cp } from "fs/promises";
-import { fileURLToPath } from "url";
-import tsup from "tsup";
+import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-main();
-
-async function main() {
- await tsup.build({
- entry: ["src/cli.ts"],
- format: ["cjs"],
- minify: false,
- outDir: "dist",
- sourcemap: true,
- clean: true,
- });
- const filesFoldersToCopy = [
- ["../features.yml", "./dist/assets/features.yml"],
- [
- "../../asIs/readme/binary-response-addendum.md",
- "./dist/assets/readme/binary-response-addendum.md",
+buildGenerator(getDirname(import.meta.url), {
+ copyFrom: [
+ { from: '../features.yml', to: './dist/assets/features.yml' },
+ { from: '../../asIs/readme/binary-response-addendum.md', to: './dist/assets/readme/binary-response-addendum.md' },
+ { from: '../../asIs/', to: './dist/assets/asIs' },
+ { from: '../../utils/core-utilities/', to: './dist/assets/core-utilities' },
],
- ["../../asIs/", "./dist/assets/asIs"],
- ["../../utils/core-utilities/", "./dist/assets/core-utilities"],
- ];
- for (const [source, destination] of filesFoldersToCopy) {
- await cp(join(__dirname, source), join(__dirname, destination), {
- recursive: true,
- force: true,
- });
- }
-}
+});
diff --git a/package.json b/package.json
index 3ad1cbb31979..6f5291993d29 100644
--- a/package.json
+++ b/package.json
@@ -42,12 +42,12 @@
"fern": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern-dev:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/dev/cli.cjs",
- "fern:build": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
+ "fern:build": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter=@fern-api/cli dist:cli:prod && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter @fern-api/cli dist:cli:prod:unminified && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
- "fern-dev:build": "turbo run dist:cli:dev --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
+ "fern-dev:build": "pnpm --filter=@fern-api/cli dist:cli:dev && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
"fern-local:build": "pnpm --filter @fern-api/cli dist:cli:local && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
- "generator-cli:generate": "pnpm fern generate --api generator-cli --local && pnpm turbo --filter=@fern-api/generator-cli compile",
- "seed:build": "turbo run dist:cli --filter=@fern-api/seed-cli && echo 'Run node --enable-source-maps packages/seed/dist/cli.cjs'",
+ "generator-cli:generate": "pnpm fern generate --api generator-cli --local && pnpm --filter=@fern-api/generator-cli compile",
+ "seed:build": "pnpm --filter=@fern-api/seed-cli dist:cli && echo 'Run node --enable-source-maps packages/seed/dist/cli.cjs'",
"publish": "pnpm -r publish --access public --no-git-checks --loglevel=verbose",
"jsonschema": "pnpm definition-yml:jsonschema && pnpm api-yml:jsonschema && pnpm package-yml:jsonschema && pnpm docs-yml:jsonschema && pnpm generators-yml:jsonschema && pnpm versions-yml:jsonschema && pnpm products-yml:jsonschema",
"definition-yml:jsonschema": "pnpm fern jsonschema fern.schema.json --api fern-definition --type file.DefinitionFileSchema && pnpm fern jsonschema packages/cli/workspace/lazy-fern-workspace/src/fern.schema.json --api fern-definition --type file.DefinitionFileSchema",
diff --git a/packages/cli/cli/build-utils.mjs b/packages/cli/cli/build-utils.mjs
new file mode 100644
index 000000000000..c06cee11da66
--- /dev/null
+++ b/packages/cli/cli/build-utils.mjs
@@ -0,0 +1,118 @@
+import packageJson from "./package.json" with { type: "json" };
+import tsup from 'tsup';
+import { writeFile } from "fs/promises";
+import path from "path";
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Get a dependency version from package.json, preferring dependencies over devDependencies.
+ * This ensures we don't miss runtime dependencies regardless of where they're declared.
+ */
+function getDependencyVersion(packageName) {
+ return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName];
+}
+
+/**
+ * Common external dependencies for full builds (dev/prod with extensive externals)
+ */
+export const FULL_EXTERNALS = [
+ '@boundaryml/baml',
+ /^prettier(?:\/.*)?$/,
+ /^prettier2(?:\/.*)?$/,
+ /^vitest(?:\/.*)?$/,
+ /^depcheck(?:\/.*)?$/,
+ /^tsup(?:\/.*)?$/,
+ /^typescript(?:\/.*)?$/,
+ /^@types\/.*$/,
+];
+
+/**
+ * Minimal external dependencies for local/unminified builds
+ */
+export const MINIMAL_EXTERNALS = ['@boundaryml/baml'];
+
+/**
+ * Common tsup overrides for production-like builds with optimization
+ */
+export const PRODUCTION_TSUP_OVERRIDES = {
+ platform: 'node',
+ target: 'node18',
+ external: FULL_EXTERNALS,
+ metafile: true,
+};
+
+/**
+ * Build the Fern CLI with the specified configuration
+ * @param {Object} config - Build configuration
+ * @param {string} config.outDir - Output directory (e.g., 'dist/prod', 'dist/dev')
+ * @param {boolean} config.minify - Whether to minify the output
+ * @param {Object} config.env - Environment variables to inject
+ * @param {string[]} [config.runtimeDependencies] - List of runtime dependencies to include in package.json
+ * @param {Object} [config.packageJsonOverrides] - Overrides for the generated package.json
+ * @param {Object} [config.tsupOverrides] - Additional tsup configuration options
+ */
+export async function buildCli(config) {
+ const {
+ outDir,
+ minify,
+ env,
+ runtimeDependencies = ['@boundaryml/baml'],
+ packageJsonOverrides = {},
+ tsupOverrides = {}
+ } = config;
+
+ // Build with tsup
+ await tsup.build({
+ entry: ['src/cli.ts'],
+ format: ['cjs'],
+ minify,
+ outDir,
+ sourcemap: true,
+ clean: true,
+ esbuildOptions(options) {
+ options.conditions = ['development', 'source', 'import', 'default']
+ },
+ env: {
+ ...env,
+ CLI_VERSION: process.argv[2] || packageJson.version,
+ },
+ ...tsupOverrides
+ });
+
+ // Change to output directory
+ process.chdir(path.join(__dirname, outDir));
+
+ // Collect runtime dependencies
+ const dependencies = {};
+ for (const dep of runtimeDependencies) {
+ const version = getDependencyVersion(dep);
+ if (version) {
+ dependencies[dep] = version;
+ }
+ }
+
+ // Validate that all required dependencies were found
+ const missingDeps = runtimeDependencies.filter(dep => !dependencies[dep]);
+ if (missingDeps.length > 0) {
+ throw new Error(
+ `Missing required runtime dependencies in package.json: ${missingDeps.join(", ")}. ` +
+ `These must be declared in either dependencies or devDependencies.`
+ );
+ }
+
+ // Write package.json
+ const outputPackageJson = {
+ version: process.argv[2] || packageJson.version,
+ repository: packageJson.repository,
+ files: ["cli.cjs"],
+ dependencies,
+ ...packageJsonOverrides
+ };
+
+ await writeFile(
+ "package.json",
+ JSON.stringify(outputPackageJson, undefined, 2)
+ );
+}
diff --git a/packages/cli/cli/build.dev.cjs b/packages/cli/cli/build.dev.cjs
deleted file mode 100644
index d101d075b77f..000000000000
--- a/packages/cli/cli/build.dev.cjs
+++ /dev/null
@@ -1,91 +0,0 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile } = require("fs/promises");
-const path = require("path");
-
-main();
-
-/**
- * Get a dependency version from package.json, preferring dependencies over devDependencies.
- * This ensures we don't miss runtime dependencies regardless of where they're declared.
- */
-function getDependencyVersion(packageName) {
- return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName];
-}
-
-async function main() {
- await tsup.build({
- entry: ['src/cli.ts'],
- format: ['cjs'],
- outDir: 'dist/dev',
- minify: false,
- sourcemap: true,
- platform: 'node',
- target: 'node18',
- external: [
- '@boundaryml/baml',
- /^prettier(?:\/.*)?$/,
- /^prettier2(?:\/.*)?$/,
- /^vitest(?:\/.*)?$/,
- /^depcheck(?:\/.*)?$/,
- /^tsup(?:\/.*)?$/,
- /^typescript(?:\/.*)?$/,
- /^@types\/.*$/,
- ],
- metafile: true,
- env: {
- AUTH0_DOMAIN: "fern-dev.us.auth0.com",
- AUTH0_CLIENT_ID: "4QiMvRvRUYpnycrVDK2M59hhJ6kcHYFQ",
- DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator-dev2.buildwithfern.com",
- DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com",
- DEFAULT_FDR_ORIGIN: "https://registry-dev2.buildwithfern.com",
- DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
- VENUS_AUDIENCE: "venus-dev",
- LOCAL_STORAGE_FOLDER: ".fern-dev",
- POSTHOG_API_KEY: null,
- DOCS_DOMAIN_SUFFIX: "docs.dev.buildwithfern.com",
- DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle2.s3.amazonaws.com/',
- APP_DOCS_TAR_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle4.s3.amazonaws.com/',
- APP_DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle3.s3.amazonaws.com/',
- CLI_NAME: "fern-dev",
- CLI_VERSION: process.argv[2] || packageJson.version,
- CLI_PACKAGE_NAME: "@fern-api/fern-api-dev",
- },
- });
-
- process.chdir(path.join(__dirname, "dist/dev"));
-
- // Collect runtime dependencies that need to be included in the published package
- const runtimeDependencies = {
- "@boundaryml/baml": getDependencyVersion("@boundaryml/baml")
- };
-
- // Validate that all required dependencies were found
- const missingDeps = Object.entries(runtimeDependencies)
- .filter(([_, version]) => !version)
- .map(([name, _]) => name);
-
- if (missingDeps.length > 0) {
- throw new Error(
- `Missing required runtime dependencies in package.json: ${missingDeps.join(", ")}. ` +
- `These must be declared in either dependencies or devDependencies.`
- );
- }
-
- // write cli's package.json
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: "@fern-api/fern-api-dev",
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- files: ["cli.cjs"],
- bin: { "fern-dev": "cli.cjs" },
- dependencies: runtimeDependencies
- },
- undefined,
- 2
- )
- );
-}
diff --git a/packages/cli/cli/build.dev.mjs b/packages/cli/cli/build.dev.mjs
new file mode 100644
index 000000000000..26c23813e42b
--- /dev/null
+++ b/packages/cli/cli/build.dev.mjs
@@ -0,0 +1,29 @@
+import { buildCli, PRODUCTION_TSUP_OVERRIDES } from './build-utils.mjs';
+
+buildCli({
+ outDir: 'dist/dev',
+ minify: false,
+ env: {
+ AUTH0_DOMAIN: "fern-dev.us.auth0.com",
+ AUTH0_CLIENT_ID: "4QiMvRvRUYpnycrVDK2M59hhJ6kcHYFQ",
+ DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator-dev2.buildwithfern.com",
+ DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com",
+ DEFAULT_FDR_ORIGIN: "https://registry-dev2.buildwithfern.com",
+ DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
+ VENUS_AUDIENCE: "venus-dev",
+ LOCAL_STORAGE_FOLDER: ".fern-dev",
+ POSTHOG_API_KEY: null,
+ DOCS_DOMAIN_SUFFIX: "docs.dev.buildwithfern.com",
+ DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle2.s3.amazonaws.com/',
+ APP_DOCS_TAR_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle4.s3.amazonaws.com/',
+ APP_DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle3.s3.amazonaws.com/',
+ CLI_NAME: "fern-dev",
+ CLI_PACKAGE_NAME: "@fern-api/fern-api-dev",
+ },
+ runtimeDependencies: ['@boundaryml/baml'],
+ packageJsonOverrides: {
+ name: "@fern-api/fern-api-dev",
+ bin: { "fern-dev": "cli.cjs" },
+ },
+ tsupOverrides: PRODUCTION_TSUP_OVERRIDES
+});
diff --git a/packages/cli/cli/build.local.cjs b/packages/cli/cli/build.local.cjs
deleted file mode 100644
index de192ac4d86b..000000000000
--- a/packages/cli/cli/build.local.cjs
+++ /dev/null
@@ -1,80 +0,0 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile } = require("fs/promises");
-const path = require("path");
-
-main();
-
-/**
- * Get a dependency version from package.json, preferring dependencies over devDependencies.
- * This ensures we don't miss runtime dependencies regardless of where they're declared.
- */
-function getDependencyVersion(packageName) {
- return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName];
-}
-
-async function main() {
- await tsup.build({
- entry: ['src/cli.ts'],
- format: ['cjs'],
- minify: false,
- outDir: 'dist/local',
- sourcemap: true,
- external: ['@boundaryml/baml'],
- env: {
- AUTH0_DOMAIN: "fern-dev.us.auth0.com",
- AUTH0_CLIENT_ID: "4QiMvRvRUYpnycrVDK2M59hhJ6kcHYFQ",
- DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator-dev2.buildwithfern.com",
- DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com",
- DEFAULT_FDR_ORIGIN: "http://localhost:8080",
- OVERRIDE_FDR_ORIGIN: "http://localhost:8080",
- DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
- VENUS_AUDIENCE: "venus-dev",
- LOCAL_STORAGE_FOLDER: ".fern-local",
- POSTHOG_API_KEY: null,
- DOCS_DOMAIN_SUFFIX: "docs.dev.buildwithfern.com",
- DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle2.s3.amazonaws.com/',
- APP_DOCS_TAR_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle4.s3.amazonaws.com/',
- APP_DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle3.s3.amazonaws.com/',
- CLI_NAME: "fern-local",
- CLI_VERSION: process.argv[2] || packageJson.version,
- CLI_PACKAGE_NAME: "fern-api",
- },
- });
-
- process.chdir(path.join(__dirname, "dist/local"));
-
- // Collect runtime dependencies that need to be included in the published package
- const runtimeDependencies = {
- "@boundaryml/baml": getDependencyVersion("@boundaryml/baml")
- };
-
- // Validate that all required dependencies were found
- const missingDeps = Object.entries(runtimeDependencies)
- .filter(([_, version]) => !version)
- .map(([name, _]) => name);
-
- if (missingDeps.length > 0) {
- throw new Error(
- `Missing required runtime dependencies in package.json: ${missingDeps.join(", ")}. ` +
- `These must be declared in either dependencies or devDependencies.`
- );
- }
-
- // write cli's package.json
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: "fern-api",
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- files: ["cli.cjs"],
- bin: { fern: "cli.cjs" },
- dependencies: runtimeDependencies
- },
- undefined,
- 2
- )
- );
-}
diff --git a/packages/cli/cli/build.local.mjs b/packages/cli/cli/build.local.mjs
new file mode 100644
index 000000000000..d473b6fabd9f
--- /dev/null
+++ b/packages/cli/cli/build.local.mjs
@@ -0,0 +1,32 @@
+import { buildCli, MINIMAL_EXTERNALS } from './build-utils.mjs';
+
+buildCli({
+ outDir: 'dist/local',
+ minify: false,
+ env: {
+ AUTH0_DOMAIN: "fern-dev.us.auth0.com",
+ AUTH0_CLIENT_ID: "4QiMvRvRUYpnycrVDK2M59hhJ6kcHYFQ",
+ DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator-dev2.buildwithfern.com",
+ DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com",
+ DEFAULT_FDR_ORIGIN: "http://localhost:8080",
+ OVERRIDE_FDR_ORIGIN: "http://localhost:8080",
+ DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
+ VENUS_AUDIENCE: "venus-dev",
+ LOCAL_STORAGE_FOLDER: ".fern-local",
+ POSTHOG_API_KEY: null,
+ DOCS_DOMAIN_SUFFIX: "docs.dev.buildwithfern.com",
+ DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle2.s3.amazonaws.com/',
+ APP_DOCS_TAR_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle4.s3.amazonaws.com/',
+ APP_DOCS_PREVIEW_BUCKET: 'https://dev2-local-preview-bundle3.s3.amazonaws.com/',
+ CLI_NAME: "fern-local",
+ CLI_PACKAGE_NAME: "fern-api",
+ },
+ runtimeDependencies: ['@boundaryml/baml'],
+ packageJsonOverrides: {
+ name: "fern-api",
+ bin: { fern: "cli.cjs" },
+ },
+ tsupOverrides: {
+ external: MINIMAL_EXTERNALS,
+ }
+});
diff --git a/packages/cli/cli/build.prod-unminified.cjs b/packages/cli/cli/build.prod-unminified.cjs
deleted file mode 100644
index b32db331a036..000000000000
--- a/packages/cli/cli/build.prod-unminified.cjs
+++ /dev/null
@@ -1,55 +0,0 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- await tsup.build({
- entry: ['src/cli.ts'],
- format: ['cjs'],
- minify: false,
- outDir: 'dist/prod',
- sourcemap: true,
- external: ['@boundaryml/baml'],
- env: {
- AUTH0_DOMAIN: "fern-prod.us.auth0.com",
- AUTH0_CLIENT_ID: "syaWnk6SjNoo5xBf1omfvziU3q7085lh",
- DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
- DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
- DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
- VENUS_AUDIENCE: "venus-prod",
- LOCAL_STORAGE_FOLDER: ".fern",
- POSTHOG_API_KEY: process.env.POSTHOG_API_KEY ?? "",
- DOCS_DOMAIN_SUFFIX: "docs.buildwithfern.com",
- DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle2.s3.amazonaws.com/',
- APP_DOCS_TAR_PREVIEW_BUCKET: 'https://prod-local-preview-bundle4.s3.amazonaws.com/',
- APP_DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle3.s3.amazonaws.com/',
- CLI_NAME: "fern",
- CLI_VERSION: process.argv[2] || packageJson.version,
- CLI_PACKAGE_NAME: "fern-api",
- },
- });
-
- process.chdir(path.join(__dirname, "dist/prod"));
-
- // write cli's package.json
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: "fern-api",
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- files: ["cli.cjs"],
- bin: { fern: "cli.cjs" },
- dependencies: {
- "@boundaryml/baml": packageJson.devDependencies["@boundaryml/baml"]
- }
- },
- undefined,
- 2
- )
- );
-}
diff --git a/packages/cli/cli/build.prod-unminified.mjs b/packages/cli/cli/build.prod-unminified.mjs
new file mode 100644
index 000000000000..2d4b01426428
--- /dev/null
+++ b/packages/cli/cli/build.prod-unminified.mjs
@@ -0,0 +1,30 @@
+import { buildCli, MINIMAL_EXTERNALS } from './build-utils.mjs';
+
+buildCli({
+ outDir: 'dist/prod',
+ minify: false,
+ env: {
+ AUTH0_DOMAIN: "fern-prod.us.auth0.com",
+ AUTH0_CLIENT_ID: "syaWnk6SjNoo5xBf1omfvziU3q7085lh",
+ DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
+ DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
+ DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
+ VENUS_AUDIENCE: "venus-prod",
+ LOCAL_STORAGE_FOLDER: ".fern",
+ POSTHOG_API_KEY: process.env.POSTHOG_API_KEY ?? "",
+ DOCS_DOMAIN_SUFFIX: "docs.buildwithfern.com",
+ DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle2.s3.amazonaws.com/',
+ APP_DOCS_TAR_PREVIEW_BUCKET: 'https://prod-local-preview-bundle4.s3.amazonaws.com/',
+ APP_DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle3.s3.amazonaws.com/',
+ CLI_NAME: "fern",
+ CLI_PACKAGE_NAME: "fern-api",
+ },
+ runtimeDependencies: ['@boundaryml/baml'],
+ packageJsonOverrides: {
+ name: "fern-api",
+ bin: { fern: "cli.cjs" },
+ },
+ tsupOverrides: {
+ external: MINIMAL_EXTERNALS,
+ }
+});
diff --git a/packages/cli/cli/build.prod.cjs b/packages/cli/cli/build.prod.cjs
deleted file mode 100644
index 08c9e893aa80..000000000000
--- a/packages/cli/cli/build.prod.cjs
+++ /dev/null
@@ -1,92 +0,0 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile } = require("fs/promises");
-const path = require("path");
-
-main();
-
-/**
- * Get a dependency version from package.json, preferring dependencies over devDependencies.
- * This ensures we don't miss runtime dependencies regardless of where they're declared.
- */
-function getDependencyVersion(packageName) {
- return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName];
-}
-
-async function main() {
- await tsup.build({
- entry: ['src/cli.ts'],
- format: ['cjs'],
- minify: true,
- outDir: 'dist/prod',
- sourcemap: true,
- platform: 'node',
- target: 'node18',
- external: [
- '@boundaryml/baml',
- /^prettier(?:\/.*)?$/,
- /^prettier2(?:\/.*)?$/,
- /^vitest(?:\/.*)?$/,
- /^depcheck(?:\/.*)?$/,
- /^tsup(?:\/.*)?$/,
- /^typescript(?:\/.*)?$/,
- /^@types\/.*$/,
- ],
- metafile: true,
- env: {
- AUTH0_DOMAIN: "fern-prod.us.auth0.com",
- AUTH0_CLIENT_ID: "syaWnk6SjNoo5xBf1omfvziU3q7085lh",
- DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
- DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
- DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
- DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
- VENUS_AUDIENCE: "venus-prod",
- LOCAL_STORAGE_FOLDER: ".fern",
- POSTHOG_API_KEY: process.env.POSTHOG_API_KEY ?? "",
- DOCS_DOMAIN_SUFFIX: "docs.buildwithfern.com",
- DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle2.s3.amazonaws.com/',
- APP_DOCS_TAR_PREVIEW_BUCKET: 'https://prod-local-preview-bundle4.s3.amazonaws.com/',
- APP_DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle3.s3.amazonaws.com/',
- CLI_NAME: "fern",
- CLI_VERSION: process.argv[2] || packageJson.version,
- CLI_PACKAGE_NAME: "fern-api",
- },
- });
-
- process.chdir(path.join(__dirname, "dist/prod"));
-
- // Collect runtime dependencies that need to be included in the published package
- const runtimeDependencies = {
- "@boundaryml/baml": getDependencyVersion("@boundaryml/baml"),
- "cli-progress": getDependencyVersion("cli-progress")
- };
-
- // Validate that all required dependencies were found
- const missingDeps = Object.entries(runtimeDependencies)
- .filter(([_, version]) => !version)
- .map(([name, _]) => name);
-
- if (missingDeps.length > 0) {
- throw new Error(
- `Missing required runtime dependencies in package.json: ${missingDeps.join(", ")}. ` +
- `These must be declared in either dependencies or devDependencies.`
- );
- }
-
- // write cli's package.json
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: "fern-api",
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- files: ["cli.cjs"],
- bin: { fern: "cli.cjs" },
- dependencies: runtimeDependencies
- },
- undefined,
- 2
- )
- );
-}
diff --git a/packages/cli/cli/build.prod.mjs b/packages/cli/cli/build.prod.mjs
new file mode 100644
index 000000000000..2c80dd120c55
--- /dev/null
+++ b/packages/cli/cli/build.prod.mjs
@@ -0,0 +1,29 @@
+import { buildCli, PRODUCTION_TSUP_OVERRIDES } from './build-utils.mjs';
+
+buildCli({
+ outDir: 'dist/prod',
+ minify: true,
+ env: {
+ AUTH0_DOMAIN: "fern-prod.us.auth0.com",
+ AUTH0_CLIENT_ID: "syaWnk6SjNoo5xBf1omfvziU3q7085lh",
+ DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
+ DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
+ DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
+ DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
+ VENUS_AUDIENCE: "venus-prod",
+ LOCAL_STORAGE_FOLDER: ".fern",
+ POSTHOG_API_KEY: process.env.POSTHOG_API_KEY ?? "",
+ DOCS_DOMAIN_SUFFIX: "docs.buildwithfern.com",
+ DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle2.s3.amazonaws.com/',
+ APP_DOCS_TAR_PREVIEW_BUCKET: 'https://prod-local-preview-bundle4.s3.amazonaws.com/',
+ APP_DOCS_PREVIEW_BUCKET: 'https://prod-local-preview-bundle3.s3.amazonaws.com/',
+ CLI_NAME: "fern",
+ CLI_PACKAGE_NAME: "fern-api",
+ },
+ runtimeDependencies: ['@boundaryml/baml', 'cli-progress'],
+ packageJsonOverrides: {
+ name: "fern-api",
+ bin: { fern: "cli.cjs" },
+ },
+ tsupOverrides: PRODUCTION_TSUP_OVERRIDES
+});
diff --git a/packages/cli/cli/package.json b/packages/cli/cli/package.json
index 0a0de4664f59..ccd9a03ebb76 100644
--- a/packages/cli/cli/package.json
+++ b/packages/cli/cli/package.json
@@ -32,10 +32,10 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli:dev": "pnpm compile && node build.dev.cjs",
- "dist:cli:local": "pnpm compile && node build.local.cjs",
- "dist:cli:prod": "pnpm compile && node build.prod.cjs",
- "dist:cli:prod:unminified": "pnpm compile && node build.prod-unminified.cjs",
+ "dist:cli:dev": "node build.dev.mjs",
+ "dist:cli:local": "node build.local.mjs",
+ "dist:cli:prod": "node build.prod.mjs",
+ "dist:cli:prod:unminified": "node build.prod-unminified.mjs",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
diff --git a/packages/cli/generation/protoc-gen-fern/build.tsconfig.json b/packages/cli/generation/protoc-gen-fern/build.tsconfig.json
deleted file mode 100644
index a3bf8730a45d..000000000000
--- a/packages/cli/generation/protoc-gen-fern/build.tsconfig.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "extends": "@fern-api/configs/tsconfig/main.json",
- "compilerOptions": {
- "verbatimModuleSyntax": false,
- "module": "CommonJS"
- },
- "include": ["./package.json", "./src/**/*"],
- "references": [
- {
- "path": "../../../commons/casings-generator"
- },
- {
- "path": "../../../commons/core-utils"
- },
- {
- "path": "../../../commons/ir-utils"
- },
- {
- "path": "../../../ir-sdk"
- },
- {
- "path": "../../api-importers/v3-importer-commons"
- },
- {
- "path": "../../logger"
- }
- ]
-}
diff --git a/packages/cli/generation/protoc-gen-fern/package.json b/packages/cli/generation/protoc-gen-fern/package.json
index c05e3d09845c..d882b3fcf386 100644
--- a/packages/cli/generation/protoc-gen-fern/package.json
+++ b/packages/cli/generation/protoc-gen-fern/package.json
@@ -30,7 +30,6 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "tsc --project build.tsconfig.json --outDir ./dist/cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
diff --git a/packages/cli/git-diff-1763508841128.patch b/packages/cli/git-diff-1763508841128.patch
deleted file mode 100644
index 5ce479f43246..000000000000
--- a/packages/cli/git-diff-1763508841128.patch
+++ /dev/null
@@ -1,2985 +0,0 @@
-diff --git a/.fern/metadata.json b/.fern/metadata.json
-deleted file mode 100644
-index ed7073e..0000000
---- a/.fern/metadata.json
-+++ /dev/null
-@@ -1,5 +0,0 @@
--{
-- "cliVersion": "0.110.1",
-- "generatorName": "fernapi/fern-typescript-sdk",
-- "generatorVersion": "3.28.4"
--}
-diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
-index 5834b38..92f2abb 100644
---- a/.github/workflows/ci.yml
-+++ b/.github/workflows/ci.yml
-@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v4
-
- - name: Set up node
-- uses: actions/setup-node@v4
-+ uses: actions/setup-node@v3
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-@@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v4
-
- - name: Set up node
-- uses: actions/setup-node@v4
-+ uses: actions/setup-node@v3
-
- - name: Install pnpm
- uses: pnpm/action-setup@v4
-diff --git a/.npmignore b/.npmignore
-index c0c40ac..b7e5ad3 100644
---- a/.npmignore
-+++ b/.npmignore
-@@ -4,7 +4,6 @@ tests
- .gitignore
- .github
- .fernignore
--.prettierrc.yml
- biome.json
- tsconfig.json
- yarn.lock
-diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
-deleted file mode 100644
-index fe5bc2f..0000000
---- a/CONTRIBUTING.md
-+++ /dev/null
-@@ -1,133 +0,0 @@
--# Contributing
--
--Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project.
--
--## Getting Started
--
--### Prerequisites
--
--- Node.js 20 or higher
--- pnpm package manager
--
--### Installation
--
--Install the project dependencies:
--
--```bash
--pnpm install
--```
--
--### Building
--
--Build the project:
--
--```bash
--pnpm build
--```
--
--### Testing
--
--Run the test suite:
--
--```bash
--pnpm test
--```
--
--Run specific test types:
--- `pnpm test:unit` - Run unit tests
--- `pnpm test:wire` - Run wire/integration tests
--
--### Linting and Formatting
--
--Check code style:
--
--```bash
--pnpm run lint
--pnpm run format:check
--```
--
--Fix code style issues:
--
--```bash
--pnpm run lint:fix
--pnpm run format:fix
--```
--
--Or use the combined check command:
--
--```bash
--pnpm run check:fix
--```
--
--## About Generated Code
--
--**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated.
--
--### Generated Files
--
--The following directories contain generated code:
--- `src/api/` - API client classes and types
--- `src/serialization/` - Serialization/deserialization logic
--- Most TypeScript files in `src/`
--
--### How to Customize
--
--If you need to customize the SDK, you have two options:
--
--#### Option 1: Use `.fernignore`
--
--For custom code that should persist across SDK regenerations:
--
--1. Create a `.fernignore` file in the project root
--2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax)
--3. Add your custom code to those files
--
--Files listed in `.fernignore` will not be overwritten when the SDK is regenerated.
--
--For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code).
--
--#### Option 2: Contribute to the Generator
--
--If you want to change how code is generated for all users of this SDK:
--
--1. The TypeScript SDK generator lives in the [Fern repository](https://github.com/fern-api/fern)
--2. Generator code is located at `generators/typescript/sdk/`
--3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md)
--4. Submit a pull request with your changes to the generator
--
--This approach is best for:
--- Bug fixes in generated code
--- New features that would benefit all users
--- Improvements to code generation patterns
--
--## Making Changes
--
--### Workflow
--
--1. Create a new branch for your changes
--2. Make your modifications
--3. Run tests to ensure nothing breaks: `pnpm test`
--4. Run linting and formatting: `pnpm run check:fix`
--5. Build the project: `pnpm build`
--6. Commit your changes with a clear commit message
--7. Push your branch and create a pull request
--
--### Commit Messages
--
--Write clear, descriptive commit messages that explain what changed and why.
--
--### Code Style
--
--This project uses automated code formatting and linting. Run `pnpm run check:fix` before committing to ensure your code meets the project's style guidelines.
--
--## Questions or Issues?
--
--If you have questions or run into issues:
--
--1. Check the [Fern documentation](https://buildwithfern.com)
--2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues)
--3. Open a new issue if your question hasn't been addressed
--
--## License
--
--By contributing to this project, you agree that your contributions will be licensed under the same license as the project.
-diff --git a/README.md b/README.md
-index c08d0f9..ed474f3 100644
---- a/README.md
-+++ b/README.md
-@@ -1,10 +1,27 @@
- # Fern TypeScript Library
-
--[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Ffern-demo%2Fauto-version-testing-ts-sdk)
-+[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Fern%2FTypeScript)
- [](https://www.npmjs.com/package/)
-
- The Fern TypeScript library provides convenient access to the Fern APIs from TypeScript.
-
-+## Table of Contents
-+
-+- [Installation](#installation)
-+- [Reference](#reference)
-+- [Usage](#usage)
-+- [Request and Response Types](#request-and-response-types)
-+- [Exception Handling](#exception-handling)
-+- [Advanced](#advanced)
-+ - [Additional Headers](#additional-headers)
-+ - [Additional Query String Parameters](#additional-query-string-parameters)
-+ - [Retries](#retries)
-+ - [Timeouts](#timeouts)
-+ - [Aborting Requests](#aborting-requests)
-+ - [Access Raw Response Data](#access-raw-response-data)
-+ - [Runtime Compatibility](#runtime-compatibility)
-+- [Contributing](#contributing)
-+
- ## Installation
-
- ```sh
-@@ -13,7 +30,7 @@ npm i -s
-
- ## Reference
-
--A full reference for this library is available [here](https://github.com/fern-demo/auto-version-testing-ts-sdk/blob/HEAD/./reference.md).
-+A full reference for this library is available [here](./reference.md).
-
- ## Usage
-
-@@ -31,7 +48,7 @@ await client.plant.addPlant({
- });
- ```
-
--## Request And Response Types
-+## Request and Response Types
-
- The SDK exports all request and response types as TypeScript interfaces. Simply import them with the
- following namespace:
-@@ -144,69 +161,6 @@ console.log(data);
- console.log(rawResponse.headers['X-My-Header']);
- ```
-
--### Logging
--
--The SDK supports logging. You can configure the logger by passing in a `logging` object to the client options.
--
--```typescript
--import { FernApiClient, logging } from "FernApi";
--
--const client = new FernApiClient({
-- ...
-- logging: {
-- level: logging.LogLevel.Debug, // defaults to logging.LogLevel.Info
-- logger: new logging.ConsoleLogger(), // defaults to ConsoleLogger
-- silent: false, // defaults to true, set to false to enable logging
-- }
--});
--```
--The `logging` object can have the following properties:
--- `level`: The log level to use. Defaults to `logging.LogLevel.Info`.
--- `logger`: The logger to use. Defaults to a `logging.ConsoleLogger`.
--- `silent`: Whether to silence the logger. Defaults to `true`.
--
--The `level` property can be one of the following values:
--- `logging.LogLevel.Debug`
--- `logging.LogLevel.Info`
--- `logging.LogLevel.Warn`
--- `logging.LogLevel.Error`
--
--To provide a custom logger, you can pass in an object that implements the `logging.ILogger` interface.
--
--
--Custom logger examples
--
--Here's an example using the popular `winston` logging library.
--```ts
--import winston from 'winston';
--
--const winstonLogger = winston.createLogger({...});
--
--const logger: logging.ILogger = {
-- debug: (msg, ...args) => winstonLogger.debug(msg, ...args),
-- info: (msg, ...args) => winstonLogger.info(msg, ...args),
-- warn: (msg, ...args) => winstonLogger.warn(msg, ...args),
-- error: (msg, ...args) => winstonLogger.error(msg, ...args),
--};
--```
--
--Here's an example using the popular `pino` logging library.
--
--```ts
--import pino from 'pino';
--
--const pinoLogger = pino({...});
--
--const logger: logging.ILogger = {
-- debug: (msg, ...args) => pinoLogger.debug(args, msg),
-- info: (msg, ...args) => pinoLogger.info(args, msg),
-- warn: (msg, ...args) => pinoLogger.warn(args, msg),
-- error: (msg, ...args) => pinoLogger.error(args, msg),
--};
--```
--
--
--
- ### Runtime Compatibility
-
-
-diff --git a/biome.json b/biome.json
-index a777468..b6890df 100644
---- a/biome.json
-+++ b/biome.json
-@@ -1,5 +1,5 @@
- {
-- "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json",
-+ "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
- "root": true,
- "vcs": {
- "enabled": false
-@@ -7,21 +7,16 @@
- "files": {
- "ignoreUnknown": true,
- "includes": [
-- "**",
-- "!!dist",
-- "!!**/dist",
-- "!!lib",
-- "!!**/lib",
-- "!!_tmp_*",
-- "!!**/_tmp_*",
-- "!!*.tmp",
-- "!!**/*.tmp",
-- "!!.tmp/",
-- "!!**/.tmp/",
-- "!!*.log",
-- "!!**/*.log",
-- "!!**/.DS_Store",
-- "!!**/Thumbs.db"
-+ "./**",
-+ "!dist",
-+ "!lib",
-+ "!*.tsbuildinfo",
-+ "!_tmp_*",
-+ "!*.tmp",
-+ "!.tmp/",
-+ "!*.log",
-+ "!.DS_Store",
-+ "!Thumbs.db"
- ]
- },
- "formatter": {
-diff --git a/package.json b/package.json
-index b8841b9..206921f 100644
---- a/package.json
-+++ b/package.json
-@@ -1,6 +1,6 @@
- {
- "name": "",
-- "version": "0.0.1",
-+ "version": "AUTO",
- "private": false,
- "repository": "github:fern-demo/auto-version-testing-ts-sdk",
- "type": "commonjs",
-@@ -30,9 +30,6 @@
- ],
- "scripts": {
- "format": "biome format --write --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
-- "format:check": "biome format --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
-- "lint": "biome lint --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
-- "lint:fix": "biome lint --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
- "check": "biome check --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
- "check:fix": "biome check --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none",
- "build": "pnpm build:cjs && pnpm build:esm",
-@@ -42,15 +39,14 @@
- "test:unit": "vitest --project unit",
- "test:wire": "vitest --project wire"
- },
-- "dependencies": {},
- "devDependencies": {
- "webpack": "^5.97.1",
- "ts-loader": "^9.5.1",
- "vitest": "^3.2.4",
- "msw": "2.11.2",
- "@types/node": "^18.19.70",
-- "typescript": "~5.7.2",
-- "@biomejs/biome": "2.3.1"
-+ "@biomejs/biome": "2.2.5",
-+ "typescript": "~5.7.2"
- },
- "browser": {
- "fs": false,
-@@ -58,7 +54,7 @@
- "path": false,
- "stream": false
- },
-- "packageManager": "pnpm@10.20.0",
-+ "packageManager": "pnpm@10.14.0",
- "engines": {
- "node": ">=18.0.0"
- },
-diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
-index d29aa19..7318843 100644
---- a/pnpm-lock.yaml
-+++ b/pnpm-lock.yaml
-@@ -9,8 +9,8 @@ importers:
- .:
- devDependencies:
- '@biomejs/biome':
-- specifier: 2.3.1
-- version: 2.3.1
-+ specifier: 2.2.5
-+ version: 2.2.5
- '@types/node':
- specifier: ^18.19.70
- version: 18.19.130
-@@ -25,62 +25,62 @@ importers:
- version: 5.7.3
- vitest:
- specifier: ^3.2.4
-- version: 3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.1)
-+ version: 3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.0)
- webpack:
- specifier: ^5.97.1
- version: 5.102.1
-
- packages:
-
-- '@biomejs/biome@2.3.1':
-- resolution: {integrity: sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==}
-+ '@biomejs/biome@2.2.5':
-+ resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==}
- engines: {node: '>=14.21.3'}
- hasBin: true
-
-- '@biomejs/cli-darwin-arm64@2.3.1':
-- resolution: {integrity: sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA==}
-+ '@biomejs/cli-darwin-arm64@2.2.5':
-+ resolution: {integrity: sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==}
- engines: {node: '>=14.21.3'}
- cpu: [arm64]
- os: [darwin]
-
-- '@biomejs/cli-darwin-x64@2.3.1':
-- resolution: {integrity: sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ==}
-+ '@biomejs/cli-darwin-x64@2.2.5':
-+ resolution: {integrity: sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==}
- engines: {node: '>=14.21.3'}
- cpu: [x64]
- os: [darwin]
-
-- '@biomejs/cli-linux-arm64-musl@2.3.1':
-- resolution: {integrity: sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg==}
-+ '@biomejs/cli-linux-arm64-musl@2.2.5':
-+ resolution: {integrity: sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==}
- engines: {node: '>=14.21.3'}
- cpu: [arm64]
- os: [linux]
-
-- '@biomejs/cli-linux-arm64@2.3.1':
-- resolution: {integrity: sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ==}
-+ '@biomejs/cli-linux-arm64@2.2.5':
-+ resolution: {integrity: sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==}
- engines: {node: '>=14.21.3'}
- cpu: [arm64]
- os: [linux]
-
-- '@biomejs/cli-linux-x64-musl@2.3.1':
-- resolution: {integrity: sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ==}
-+ '@biomejs/cli-linux-x64-musl@2.2.5':
-+ resolution: {integrity: sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==}
- engines: {node: '>=14.21.3'}
- cpu: [x64]
- os: [linux]
-
-- '@biomejs/cli-linux-x64@2.3.1':
-- resolution: {integrity: sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q==}
-+ '@biomejs/cli-linux-x64@2.2.5':
-+ resolution: {integrity: sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==}
- engines: {node: '>=14.21.3'}
- cpu: [x64]
- os: [linux]
-
-- '@biomejs/cli-win32-arm64@2.3.1':
-- resolution: {integrity: sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A==}
-+ '@biomejs/cli-win32-arm64@2.2.5':
-+ resolution: {integrity: sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==}
- engines: {node: '>=14.21.3'}
- cpu: [arm64]
- os: [win32]
-
-- '@biomejs/cli-win32-x64@2.3.1':
-- resolution: {integrity: sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==}
-+ '@biomejs/cli-win32-x64@2.2.5':
-+ resolution: {integrity: sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==}
- engines: {node: '>=14.21.3'}
- cpu: [x64]
- os: [win32]
-@@ -91,158 +91,158 @@ packages:
- '@bundled-es-modules/statuses@1.0.1':
- resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==}
-
-- '@esbuild/aix-ppc64@0.25.12':
-- resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
-+ '@esbuild/aix-ppc64@0.25.11':
-+ resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [aix]
-
-- '@esbuild/android-arm64@0.25.12':
-- resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
-+ '@esbuild/android-arm64@0.25.11':
-+ resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [android]
-
-- '@esbuild/android-arm@0.25.12':
-- resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
-+ '@esbuild/android-arm@0.25.11':
-+ resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [android]
-
-- '@esbuild/android-x64@0.25.12':
-- resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
-+ '@esbuild/android-x64@0.25.11':
-+ resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [android]
-
-- '@esbuild/darwin-arm64@0.25.12':
-- resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
-+ '@esbuild/darwin-arm64@0.25.11':
-+ resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [darwin]
-
-- '@esbuild/darwin-x64@0.25.12':
-- resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
-+ '@esbuild/darwin-x64@0.25.11':
-+ resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [darwin]
-
-- '@esbuild/freebsd-arm64@0.25.12':
-- resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
-+ '@esbuild/freebsd-arm64@0.25.11':
-+ resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [freebsd]
-
-- '@esbuild/freebsd-x64@0.25.12':
-- resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
-+ '@esbuild/freebsd-x64@0.25.11':
-+ resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [freebsd]
-
-- '@esbuild/linux-arm64@0.25.12':
-- resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
-+ '@esbuild/linux-arm64@0.25.11':
-+ resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [linux]
-
-- '@esbuild/linux-arm@0.25.12':
-- resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
-+ '@esbuild/linux-arm@0.25.11':
-+ resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [linux]
-
-- '@esbuild/linux-ia32@0.25.12':
-- resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
-+ '@esbuild/linux-ia32@0.25.11':
-+ resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [linux]
-
-- '@esbuild/linux-loong64@0.25.12':
-- resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
-+ '@esbuild/linux-loong64@0.25.11':
-+ resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
- engines: {node: '>=18'}
- cpu: [loong64]
- os: [linux]
-
-- '@esbuild/linux-mips64el@0.25.12':
-- resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
-+ '@esbuild/linux-mips64el@0.25.11':
-+ resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
- engines: {node: '>=18'}
- cpu: [mips64el]
- os: [linux]
-
-- '@esbuild/linux-ppc64@0.25.12':
-- resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
-+ '@esbuild/linux-ppc64@0.25.11':
-+ resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [linux]
-
-- '@esbuild/linux-riscv64@0.25.12':
-- resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
-+ '@esbuild/linux-riscv64@0.25.11':
-+ resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
- engines: {node: '>=18'}
- cpu: [riscv64]
- os: [linux]
-
-- '@esbuild/linux-s390x@0.25.12':
-- resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
-+ '@esbuild/linux-s390x@0.25.11':
-+ resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
- engines: {node: '>=18'}
- cpu: [s390x]
- os: [linux]
-
-- '@esbuild/linux-x64@0.25.12':
-- resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
-+ '@esbuild/linux-x64@0.25.11':
-+ resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [linux]
-
-- '@esbuild/netbsd-arm64@0.25.12':
-- resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
-+ '@esbuild/netbsd-arm64@0.25.11':
-+ resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [netbsd]
-
-- '@esbuild/netbsd-x64@0.25.12':
-- resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
-+ '@esbuild/netbsd-x64@0.25.11':
-+ resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [netbsd]
-
-- '@esbuild/openbsd-arm64@0.25.12':
-- resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
-+ '@esbuild/openbsd-arm64@0.25.11':
-+ resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openbsd]
-
-- '@esbuild/openbsd-x64@0.25.12':
-- resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
-+ '@esbuild/openbsd-x64@0.25.11':
-+ resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [openbsd]
-
-- '@esbuild/openharmony-arm64@0.25.12':
-- resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
-+ '@esbuild/openharmony-arm64@0.25.11':
-+ resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openharmony]
-
-- '@esbuild/sunos-x64@0.25.12':
-- resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
-+ '@esbuild/sunos-x64@0.25.11':
-+ resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [sunos]
-
-- '@esbuild/win32-arm64@0.25.12':
-- resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
-+ '@esbuild/win32-arm64@0.25.11':
-+ resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [win32]
-
-- '@esbuild/win32-ia32@0.25.12':
-- resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
-+ '@esbuild/win32-ia32@0.25.11':
-+ resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [win32]
-
-- '@esbuild/win32-x64@0.25.12':
-- resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
-+ '@esbuild/win32-x64@0.25.11':
-+ resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [win32]
-@@ -311,118 +311,118 @@ packages:
- '@open-draft/until@2.1.0':
- resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
-
-- '@rollup/rollup-android-arm-eabi@4.52.5':
-- resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==}
-+ '@rollup/rollup-android-arm-eabi@4.52.4':
-+ resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
- cpu: [arm]
- os: [android]
-
-- '@rollup/rollup-android-arm64@4.52.5':
-- resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==}
-+ '@rollup/rollup-android-arm64@4.52.4':
-+ resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==}
- cpu: [arm64]
- os: [android]
-
-- '@rollup/rollup-darwin-arm64@4.52.5':
-- resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==}
-+ '@rollup/rollup-darwin-arm64@4.52.4':
-+ resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==}
- cpu: [arm64]
- os: [darwin]
-
-- '@rollup/rollup-darwin-x64@4.52.5':
-- resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==}
-+ '@rollup/rollup-darwin-x64@4.52.4':
-+ resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==}
- cpu: [x64]
- os: [darwin]
-
-- '@rollup/rollup-freebsd-arm64@4.52.5':
-- resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==}
-+ '@rollup/rollup-freebsd-arm64@4.52.4':
-+ resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==}
- cpu: [arm64]
- os: [freebsd]
-
-- '@rollup/rollup-freebsd-x64@4.52.5':
-- resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==}
-+ '@rollup/rollup-freebsd-x64@4.52.4':
-+ resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==}
- cpu: [x64]
- os: [freebsd]
-
-- '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
-- resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
-+ '@rollup/rollup-linux-arm-gnueabihf@4.52.4':
-+ resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==}
- cpu: [arm]
- os: [linux]
-
-- '@rollup/rollup-linux-arm-musleabihf@4.52.5':
-- resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
-+ '@rollup/rollup-linux-arm-musleabihf@4.52.4':
-+ resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==}
- cpu: [arm]
- os: [linux]
-
-- '@rollup/rollup-linux-arm64-gnu@4.52.5':
-- resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
-+ '@rollup/rollup-linux-arm64-gnu@4.52.4':
-+ resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==}
- cpu: [arm64]
- os: [linux]
-
-- '@rollup/rollup-linux-arm64-musl@4.52.5':
-- resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
-+ '@rollup/rollup-linux-arm64-musl@4.52.4':
-+ resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==}
- cpu: [arm64]
- os: [linux]
-
-- '@rollup/rollup-linux-loong64-gnu@4.52.5':
-- resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
-+ '@rollup/rollup-linux-loong64-gnu@4.52.4':
-+ resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==}
- cpu: [loong64]
- os: [linux]
-
-- '@rollup/rollup-linux-ppc64-gnu@4.52.5':
-- resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
-+ '@rollup/rollup-linux-ppc64-gnu@4.52.4':
-+ resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==}
- cpu: [ppc64]
- os: [linux]
-
-- '@rollup/rollup-linux-riscv64-gnu@4.52.5':
-- resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
-+ '@rollup/rollup-linux-riscv64-gnu@4.52.4':
-+ resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==}
- cpu: [riscv64]
- os: [linux]
-
-- '@rollup/rollup-linux-riscv64-musl@4.52.5':
-- resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
-+ '@rollup/rollup-linux-riscv64-musl@4.52.4':
-+ resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==}
- cpu: [riscv64]
- os: [linux]
-
-- '@rollup/rollup-linux-s390x-gnu@4.52.5':
-- resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
-+ '@rollup/rollup-linux-s390x-gnu@4.52.4':
-+ resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==}
- cpu: [s390x]
- os: [linux]
-
-- '@rollup/rollup-linux-x64-gnu@4.52.5':
-- resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
-+ '@rollup/rollup-linux-x64-gnu@4.52.4':
-+ resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==}
- cpu: [x64]
- os: [linux]
-
-- '@rollup/rollup-linux-x64-musl@4.52.5':
-- resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
-+ '@rollup/rollup-linux-x64-musl@4.52.4':
-+ resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==}
- cpu: [x64]
- os: [linux]
-
-- '@rollup/rollup-openharmony-arm64@4.52.5':
-- resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
-+ '@rollup/rollup-openharmony-arm64@4.52.4':
-+ resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==}
- cpu: [arm64]
- os: [openharmony]
-
-- '@rollup/rollup-win32-arm64-msvc@4.52.5':
-- resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==}
-+ '@rollup/rollup-win32-arm64-msvc@4.52.4':
-+ resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==}
- cpu: [arm64]
- os: [win32]
-
-- '@rollup/rollup-win32-ia32-msvc@4.52.5':
-- resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==}
-+ '@rollup/rollup-win32-ia32-msvc@4.52.4':
-+ resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==}
- cpu: [ia32]
- os: [win32]
-
-- '@rollup/rollup-win32-x64-gnu@4.52.5':
-- resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==}
-+ '@rollup/rollup-win32-x64-gnu@4.52.4':
-+ resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==}
- cpu: [x64]
- os: [win32]
-
-- '@rollup/rollup-win32-x64-msvc@4.52.5':
-- resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==}
-+ '@rollup/rollup-win32-x64-msvc@4.52.4':
-+ resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==}
- cpu: [x64]
- os: [win32]
-
-- '@types/chai@5.2.3':
-- resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
-+ '@types/chai@5.2.2':
-+ resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
-
- '@types/cookie@0.6.0':
- resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
-@@ -567,16 +567,16 @@ packages:
- resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
- engines: {node: '>=12'}
-
-- baseline-browser-mapping@2.8.25:
-- resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==}
-+ baseline-browser-mapping@2.8.16:
-+ resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
- hasBin: true
-
- braces@3.0.3:
- resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
- engines: {node: '>=8'}
-
-- browserslist@4.27.0:
-- resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==}
-+ browserslist@4.26.3:
-+ resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
-@@ -587,8 +587,8 @@ packages:
- resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
- engines: {node: '>=8'}
-
-- caniuse-lite@1.0.30001754:
-- resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==}
-+ caniuse-lite@1.0.30001750:
-+ resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==}
-
- chai@5.3.3:
- resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
-@@ -641,8 +641,8 @@ packages:
- resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
- engines: {node: '>=6'}
-
-- electron-to-chromium@1.5.246:
-- resolution: {integrity: sha512-CKp2enkTcw94o8p7P+nb3in3yILO7jAIoERSmkIhGazMuK2eLnPSVUH/dxUveGN8ulJJDjYUv0vV7y2e2AZ0nA==}
-+ electron-to-chromium@1.5.237:
-+ resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==}
-
- emoji-regex@8.0.0:
- resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
-@@ -654,8 +654,8 @@ packages:
- es-module-lexer@1.7.0:
- resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
-
-- esbuild@0.25.12:
-- resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
-+ esbuild@0.25.11:
-+ resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
- engines: {node: '>=18'}
- hasBin: true
-
-@@ -724,8 +724,8 @@ packages:
- graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
-
-- graphql@16.12.0:
-- resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==}
-+ graphql@16.11.0:
-+ resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==}
- engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
-
- has-flag@4.0.0:
-@@ -766,8 +766,8 @@ packages:
- loupe@3.2.1:
- resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
-
-- magic-string@0.30.21:
-- resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
-+ magic-string@0.30.19:
-+ resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
-
- merge-stream@2.0.0:
- resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
-@@ -809,8 +809,8 @@ packages:
- neo-async@2.6.2:
- resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
-
-- node-releases@2.0.27:
-- resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
-+ node-releases@2.0.23:
-+ resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
-
- outvariant@1.4.3:
- resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
-@@ -854,8 +854,8 @@ packages:
- rettime@0.7.0:
- resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==}
-
-- rollup@4.52.5:
-- resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
-+ rollup@4.52.4:
-+ resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
- hasBin: true
-
-@@ -948,8 +948,8 @@ packages:
- uglify-js:
- optional: true
-
-- terser@5.44.1:
-- resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==}
-+ terser@5.44.0:
-+ resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
- engines: {node: '>=10'}
- hasBin: true
-
-@@ -1009,8 +1009,8 @@ packages:
- undici-types@5.26.5:
- resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
-
-- update-browserslist-db@1.1.4:
-- resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
-+ update-browserslist-db@1.1.3:
-+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-@@ -1020,8 +1020,8 @@ packages:
- engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
- hasBin: true
-
-- vite@7.2.1:
-- resolution: {integrity: sha512-qTl3VF7BvOupTR85Zc561sPEgxyUSNSvTQ9fit7DEMP7yPgvvIGm5Zfa1dOM+kOwWGNviK9uFM9ra77+OjK7lQ==}
-+ vite@7.1.10:
-+ resolution: {integrity: sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- hasBin: true
- peerDependencies:
-@@ -1137,39 +1137,39 @@ packages:
-
- snapshots:
-
-- '@biomejs/biome@2.3.1':
-+ '@biomejs/biome@2.2.5':
- optionalDependencies:
-- '@biomejs/cli-darwin-arm64': 2.3.1
-- '@biomejs/cli-darwin-x64': 2.3.1
-- '@biomejs/cli-linux-arm64': 2.3.1
-- '@biomejs/cli-linux-arm64-musl': 2.3.1
-- '@biomejs/cli-linux-x64': 2.3.1
-- '@biomejs/cli-linux-x64-musl': 2.3.1
-- '@biomejs/cli-win32-arm64': 2.3.1
-- '@biomejs/cli-win32-x64': 2.3.1
--
-- '@biomejs/cli-darwin-arm64@2.3.1':
-+ '@biomejs/cli-darwin-arm64': 2.2.5
-+ '@biomejs/cli-darwin-x64': 2.2.5
-+ '@biomejs/cli-linux-arm64': 2.2.5
-+ '@biomejs/cli-linux-arm64-musl': 2.2.5
-+ '@biomejs/cli-linux-x64': 2.2.5
-+ '@biomejs/cli-linux-x64-musl': 2.2.5
-+ '@biomejs/cli-win32-arm64': 2.2.5
-+ '@biomejs/cli-win32-x64': 2.2.5
-+
-+ '@biomejs/cli-darwin-arm64@2.2.5':
- optional: true
-
-- '@biomejs/cli-darwin-x64@2.3.1':
-+ '@biomejs/cli-darwin-x64@2.2.5':
- optional: true
-
-- '@biomejs/cli-linux-arm64-musl@2.3.1':
-+ '@biomejs/cli-linux-arm64-musl@2.2.5':
- optional: true
-
-- '@biomejs/cli-linux-arm64@2.3.1':
-+ '@biomejs/cli-linux-arm64@2.2.5':
- optional: true
-
-- '@biomejs/cli-linux-x64-musl@2.3.1':
-+ '@biomejs/cli-linux-x64-musl@2.2.5':
- optional: true
-
-- '@biomejs/cli-linux-x64@2.3.1':
-+ '@biomejs/cli-linux-x64@2.2.5':
- optional: true
-
-- '@biomejs/cli-win32-arm64@2.3.1':
-+ '@biomejs/cli-win32-arm64@2.2.5':
- optional: true
-
-- '@biomejs/cli-win32-x64@2.3.1':
-+ '@biomejs/cli-win32-x64@2.2.5':
- optional: true
-
- '@bundled-es-modules/cookie@2.0.1':
-@@ -1180,82 +1180,82 @@ snapshots:
- dependencies:
- statuses: 2.0.2
-
-- '@esbuild/aix-ppc64@0.25.12':
-+ '@esbuild/aix-ppc64@0.25.11':
- optional: true
-
-- '@esbuild/android-arm64@0.25.12':
-+ '@esbuild/android-arm64@0.25.11':
- optional: true
-
-- '@esbuild/android-arm@0.25.12':
-+ '@esbuild/android-arm@0.25.11':
- optional: true
-
-- '@esbuild/android-x64@0.25.12':
-+ '@esbuild/android-x64@0.25.11':
- optional: true
-
-- '@esbuild/darwin-arm64@0.25.12':
-+ '@esbuild/darwin-arm64@0.25.11':
- optional: true
-
-- '@esbuild/darwin-x64@0.25.12':
-+ '@esbuild/darwin-x64@0.25.11':
- optional: true
-
-- '@esbuild/freebsd-arm64@0.25.12':
-+ '@esbuild/freebsd-arm64@0.25.11':
- optional: true
-
-- '@esbuild/freebsd-x64@0.25.12':
-+ '@esbuild/freebsd-x64@0.25.11':
- optional: true
-
-- '@esbuild/linux-arm64@0.25.12':
-+ '@esbuild/linux-arm64@0.25.11':
- optional: true
-
-- '@esbuild/linux-arm@0.25.12':
-+ '@esbuild/linux-arm@0.25.11':
- optional: true
-
-- '@esbuild/linux-ia32@0.25.12':
-+ '@esbuild/linux-ia32@0.25.11':
- optional: true
-
-- '@esbuild/linux-loong64@0.25.12':
-+ '@esbuild/linux-loong64@0.25.11':
- optional: true
-
-- '@esbuild/linux-mips64el@0.25.12':
-+ '@esbuild/linux-mips64el@0.25.11':
- optional: true
-
-- '@esbuild/linux-ppc64@0.25.12':
-+ '@esbuild/linux-ppc64@0.25.11':
- optional: true
-
-- '@esbuild/linux-riscv64@0.25.12':
-+ '@esbuild/linux-riscv64@0.25.11':
- optional: true
-
-- '@esbuild/linux-s390x@0.25.12':
-+ '@esbuild/linux-s390x@0.25.11':
- optional: true
-
-- '@esbuild/linux-x64@0.25.12':
-+ '@esbuild/linux-x64@0.25.11':
- optional: true
-
-- '@esbuild/netbsd-arm64@0.25.12':
-+ '@esbuild/netbsd-arm64@0.25.11':
- optional: true
-
-- '@esbuild/netbsd-x64@0.25.12':
-+ '@esbuild/netbsd-x64@0.25.11':
- optional: true
-
-- '@esbuild/openbsd-arm64@0.25.12':
-+ '@esbuild/openbsd-arm64@0.25.11':
- optional: true
-
-- '@esbuild/openbsd-x64@0.25.12':
-+ '@esbuild/openbsd-x64@0.25.11':
- optional: true
-
-- '@esbuild/openharmony-arm64@0.25.12':
-+ '@esbuild/openharmony-arm64@0.25.11':
- optional: true
-
-- '@esbuild/sunos-x64@0.25.12':
-+ '@esbuild/sunos-x64@0.25.11':
- optional: true
-
-- '@esbuild/win32-arm64@0.25.12':
-+ '@esbuild/win32-arm64@0.25.11':
- optional: true
-
-- '@esbuild/win32-ia32@0.25.12':
-+ '@esbuild/win32-ia32@0.25.11':
- optional: true
-
-- '@esbuild/win32-x64@0.25.12':
-+ '@esbuild/win32-x64@0.25.11':
- optional: true
-
- '@inquirer/ansi@1.0.1': {}
-@@ -1323,76 +1323,75 @@ snapshots:
-
- '@open-draft/until@2.1.0': {}
-
-- '@rollup/rollup-android-arm-eabi@4.52.5':
-+ '@rollup/rollup-android-arm-eabi@4.52.4':
- optional: true
-
-- '@rollup/rollup-android-arm64@4.52.5':
-+ '@rollup/rollup-android-arm64@4.52.4':
- optional: true
-
-- '@rollup/rollup-darwin-arm64@4.52.5':
-+ '@rollup/rollup-darwin-arm64@4.52.4':
- optional: true
-
-- '@rollup/rollup-darwin-x64@4.52.5':
-+ '@rollup/rollup-darwin-x64@4.52.4':
- optional: true
-
-- '@rollup/rollup-freebsd-arm64@4.52.5':
-+ '@rollup/rollup-freebsd-arm64@4.52.4':
- optional: true
-
-- '@rollup/rollup-freebsd-x64@4.52.5':
-+ '@rollup/rollup-freebsd-x64@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
-+ '@rollup/rollup-linux-arm-gnueabihf@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-arm-musleabihf@4.52.5':
-+ '@rollup/rollup-linux-arm-musleabihf@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-arm64-gnu@4.52.5':
-+ '@rollup/rollup-linux-arm64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-arm64-musl@4.52.5':
-+ '@rollup/rollup-linux-arm64-musl@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-loong64-gnu@4.52.5':
-+ '@rollup/rollup-linux-loong64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-ppc64-gnu@4.52.5':
-+ '@rollup/rollup-linux-ppc64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-riscv64-gnu@4.52.5':
-+ '@rollup/rollup-linux-riscv64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-riscv64-musl@4.52.5':
-+ '@rollup/rollup-linux-riscv64-musl@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-s390x-gnu@4.52.5':
-+ '@rollup/rollup-linux-s390x-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-x64-gnu@4.52.5':
-+ '@rollup/rollup-linux-x64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-linux-x64-musl@4.52.5':
-+ '@rollup/rollup-linux-x64-musl@4.52.4':
- optional: true
-
-- '@rollup/rollup-openharmony-arm64@4.52.5':
-+ '@rollup/rollup-openharmony-arm64@4.52.4':
- optional: true
-
-- '@rollup/rollup-win32-arm64-msvc@4.52.5':
-+ '@rollup/rollup-win32-arm64-msvc@4.52.4':
- optional: true
-
-- '@rollup/rollup-win32-ia32-msvc@4.52.5':
-+ '@rollup/rollup-win32-ia32-msvc@4.52.4':
- optional: true
-
-- '@rollup/rollup-win32-x64-gnu@4.52.5':
-+ '@rollup/rollup-win32-x64-gnu@4.52.4':
- optional: true
-
-- '@rollup/rollup-win32-x64-msvc@4.52.5':
-+ '@rollup/rollup-win32-x64-msvc@4.52.4':
- optional: true
-
-- '@types/chai@5.2.3':
-+ '@types/chai@5.2.2':
- dependencies:
- '@types/deep-eql': 4.0.2
-- assertion-error: 2.0.1
-
- '@types/cookie@0.6.0': {}
-
-@@ -1420,20 +1419,20 @@ snapshots:
-
- '@vitest/expect@3.2.4':
- dependencies:
-- '@types/chai': 5.2.3
-+ '@types/chai': 5.2.2
- '@vitest/spy': 3.2.4
- '@vitest/utils': 3.2.4
- chai: 5.3.3
- tinyrainbow: 2.0.0
-
-- '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.1(@types/node@18.19.130)(terser@5.44.1))':
-+ '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.10(@types/node@18.19.130)(terser@5.44.0))':
- dependencies:
- '@vitest/spy': 3.2.4
- estree-walker: 3.0.3
-- magic-string: 0.30.21
-+ magic-string: 0.30.19
- optionalDependencies:
- msw: 2.11.2(@types/node@18.19.130)(typescript@5.7.3)
-- vite: 7.2.1(@types/node@18.19.130)(terser@5.44.1)
-+ vite: 7.1.10(@types/node@18.19.130)(terser@5.44.0)
-
- '@vitest/pretty-format@3.2.4':
- dependencies:
-@@ -1448,7 +1447,7 @@ snapshots:
- '@vitest/snapshot@3.2.4':
- dependencies:
- '@vitest/pretty-format': 3.2.4
-- magic-string: 0.30.21
-+ magic-string: 0.30.19
- pathe: 2.0.3
-
- '@vitest/spy@3.2.4':
-@@ -1571,25 +1570,25 @@ snapshots:
-
- assertion-error@2.0.1: {}
-
-- baseline-browser-mapping@2.8.25: {}
-+ baseline-browser-mapping@2.8.16: {}
-
- braces@3.0.3:
- dependencies:
- fill-range: 7.1.1
-
-- browserslist@4.27.0:
-+ browserslist@4.26.3:
- dependencies:
-- baseline-browser-mapping: 2.8.25
-- caniuse-lite: 1.0.30001754
-- electron-to-chromium: 1.5.246
-- node-releases: 2.0.27
-- update-browserslist-db: 1.1.4(browserslist@4.27.0)
-+ baseline-browser-mapping: 2.8.16
-+ caniuse-lite: 1.0.30001750
-+ electron-to-chromium: 1.5.237
-+ node-releases: 2.0.23
-+ update-browserslist-db: 1.1.3(browserslist@4.26.3)
-
- buffer-from@1.1.2: {}
-
- cac@6.7.14: {}
-
-- caniuse-lite@1.0.30001754: {}
-+ caniuse-lite@1.0.30001750: {}
-
- chai@5.3.3:
- dependencies:
-@@ -1632,7 +1631,7 @@ snapshots:
-
- deep-eql@5.0.2: {}
-
-- electron-to-chromium@1.5.246: {}
-+ electron-to-chromium@1.5.237: {}
-
- emoji-regex@8.0.0: {}
-
-@@ -1643,34 +1642,34 @@ snapshots:
-
- es-module-lexer@1.7.0: {}
-
-- esbuild@0.25.12:
-+ esbuild@0.25.11:
- optionalDependencies:
-- '@esbuild/aix-ppc64': 0.25.12
-- '@esbuild/android-arm': 0.25.12
-- '@esbuild/android-arm64': 0.25.12
-- '@esbuild/android-x64': 0.25.12
-- '@esbuild/darwin-arm64': 0.25.12
-- '@esbuild/darwin-x64': 0.25.12
-- '@esbuild/freebsd-arm64': 0.25.12
-- '@esbuild/freebsd-x64': 0.25.12
-- '@esbuild/linux-arm': 0.25.12
-- '@esbuild/linux-arm64': 0.25.12
-- '@esbuild/linux-ia32': 0.25.12
-- '@esbuild/linux-loong64': 0.25.12
-- '@esbuild/linux-mips64el': 0.25.12
-- '@esbuild/linux-ppc64': 0.25.12
-- '@esbuild/linux-riscv64': 0.25.12
-- '@esbuild/linux-s390x': 0.25.12
-- '@esbuild/linux-x64': 0.25.12
-- '@esbuild/netbsd-arm64': 0.25.12
-- '@esbuild/netbsd-x64': 0.25.12
-- '@esbuild/openbsd-arm64': 0.25.12
-- '@esbuild/openbsd-x64': 0.25.12
-- '@esbuild/openharmony-arm64': 0.25.12
-- '@esbuild/sunos-x64': 0.25.12
-- '@esbuild/win32-arm64': 0.25.12
-- '@esbuild/win32-ia32': 0.25.12
-- '@esbuild/win32-x64': 0.25.12
-+ '@esbuild/aix-ppc64': 0.25.11
-+ '@esbuild/android-arm': 0.25.11
-+ '@esbuild/android-arm64': 0.25.11
-+ '@esbuild/android-x64': 0.25.11
-+ '@esbuild/darwin-arm64': 0.25.11
-+ '@esbuild/darwin-x64': 0.25.11
-+ '@esbuild/freebsd-arm64': 0.25.11
-+ '@esbuild/freebsd-x64': 0.25.11
-+ '@esbuild/linux-arm': 0.25.11
-+ '@esbuild/linux-arm64': 0.25.11
-+ '@esbuild/linux-ia32': 0.25.11
-+ '@esbuild/linux-loong64': 0.25.11
-+ '@esbuild/linux-mips64el': 0.25.11
-+ '@esbuild/linux-ppc64': 0.25.11
-+ '@esbuild/linux-riscv64': 0.25.11
-+ '@esbuild/linux-s390x': 0.25.11
-+ '@esbuild/linux-x64': 0.25.11
-+ '@esbuild/netbsd-arm64': 0.25.11
-+ '@esbuild/netbsd-x64': 0.25.11
-+ '@esbuild/openbsd-arm64': 0.25.11
-+ '@esbuild/openbsd-x64': 0.25.11
-+ '@esbuild/openharmony-arm64': 0.25.11
-+ '@esbuild/sunos-x64': 0.25.11
-+ '@esbuild/win32-arm64': 0.25.11
-+ '@esbuild/win32-ia32': 0.25.11
-+ '@esbuild/win32-x64': 0.25.11
-
- escalade@3.2.0: {}
-
-@@ -1716,7 +1715,7 @@ snapshots:
-
- graceful-fs@4.2.11: {}
-
-- graphql@16.12.0: {}
-+ graphql@16.11.0: {}
-
- has-flag@4.0.0: {}
-
-@@ -1744,7 +1743,7 @@ snapshots:
-
- loupe@3.2.1: {}
-
-- magic-string@0.30.21:
-+ magic-string@0.30.19:
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
-
-@@ -1773,7 +1772,7 @@ snapshots:
- '@open-draft/until': 2.1.0
- '@types/cookie': 0.6.0
- '@types/statuses': 2.0.6
-- graphql: 16.12.0
-+ graphql: 16.11.0
- headers-polyfill: 4.0.3
- is-node-process: 1.2.0
- outvariant: 1.4.3
-@@ -1795,7 +1794,7 @@ snapshots:
-
- neo-async@2.6.2: {}
-
-- node-releases@2.0.27: {}
-+ node-releases@2.0.23: {}
-
- outvariant@1.4.3: {}
-
-@@ -1827,32 +1826,32 @@ snapshots:
-
- rettime@0.7.0: {}
-
-- rollup@4.52.5:
-+ rollup@4.52.4:
- dependencies:
- '@types/estree': 1.0.8
- optionalDependencies:
-- '@rollup/rollup-android-arm-eabi': 4.52.5
-- '@rollup/rollup-android-arm64': 4.52.5
-- '@rollup/rollup-darwin-arm64': 4.52.5
-- '@rollup/rollup-darwin-x64': 4.52.5
-- '@rollup/rollup-freebsd-arm64': 4.52.5
-- '@rollup/rollup-freebsd-x64': 4.52.5
-- '@rollup/rollup-linux-arm-gnueabihf': 4.52.5
-- '@rollup/rollup-linux-arm-musleabihf': 4.52.5
-- '@rollup/rollup-linux-arm64-gnu': 4.52.5
-- '@rollup/rollup-linux-arm64-musl': 4.52.5
-- '@rollup/rollup-linux-loong64-gnu': 4.52.5
-- '@rollup/rollup-linux-ppc64-gnu': 4.52.5
-- '@rollup/rollup-linux-riscv64-gnu': 4.52.5
-- '@rollup/rollup-linux-riscv64-musl': 4.52.5
-- '@rollup/rollup-linux-s390x-gnu': 4.52.5
-- '@rollup/rollup-linux-x64-gnu': 4.52.5
-- '@rollup/rollup-linux-x64-musl': 4.52.5
-- '@rollup/rollup-openharmony-arm64': 4.52.5
-- '@rollup/rollup-win32-arm64-msvc': 4.52.5
-- '@rollup/rollup-win32-ia32-msvc': 4.52.5
-- '@rollup/rollup-win32-x64-gnu': 4.52.5
-- '@rollup/rollup-win32-x64-msvc': 4.52.5
-+ '@rollup/rollup-android-arm-eabi': 4.52.4
-+ '@rollup/rollup-android-arm64': 4.52.4
-+ '@rollup/rollup-darwin-arm64': 4.52.4
-+ '@rollup/rollup-darwin-x64': 4.52.4
-+ '@rollup/rollup-freebsd-arm64': 4.52.4
-+ '@rollup/rollup-freebsd-x64': 4.52.4
-+ '@rollup/rollup-linux-arm-gnueabihf': 4.52.4
-+ '@rollup/rollup-linux-arm-musleabihf': 4.52.4
-+ '@rollup/rollup-linux-arm64-gnu': 4.52.4
-+ '@rollup/rollup-linux-arm64-musl': 4.52.4
-+ '@rollup/rollup-linux-loong64-gnu': 4.52.4
-+ '@rollup/rollup-linux-ppc64-gnu': 4.52.4
-+ '@rollup/rollup-linux-riscv64-gnu': 4.52.4
-+ '@rollup/rollup-linux-riscv64-musl': 4.52.4
-+ '@rollup/rollup-linux-s390x-gnu': 4.52.4
-+ '@rollup/rollup-linux-x64-gnu': 4.52.4
-+ '@rollup/rollup-linux-x64-musl': 4.52.4
-+ '@rollup/rollup-openharmony-arm64': 4.52.4
-+ '@rollup/rollup-win32-arm64-msvc': 4.52.4
-+ '@rollup/rollup-win32-ia32-msvc': 4.52.4
-+ '@rollup/rollup-win32-x64-gnu': 4.52.4
-+ '@rollup/rollup-win32-x64-msvc': 4.52.4
- fsevents: 2.3.3
-
- safe-buffer@5.2.1: {}
-@@ -1923,10 +1922,10 @@ snapshots:
- jest-worker: 27.5.1
- schema-utils: 4.3.3
- serialize-javascript: 6.0.2
-- terser: 5.44.1
-+ terser: 5.44.0
- webpack: 5.102.1
-
-- terser@5.44.1:
-+ terser@5.44.0:
- dependencies:
- '@jridgewell/source-map': 0.3.11
- acorn: 8.15.0
-@@ -1978,19 +1977,19 @@ snapshots:
-
- undici-types@5.26.5: {}
-
-- update-browserslist-db@1.1.4(browserslist@4.27.0):
-+ update-browserslist-db@1.1.3(browserslist@4.26.3):
- dependencies:
-- browserslist: 4.27.0
-+ browserslist: 4.26.3
- escalade: 3.2.0
- picocolors: 1.1.1
-
-- vite-node@3.2.4(@types/node@18.19.130)(terser@5.44.1):
-+ vite-node@3.2.4(@types/node@18.19.130)(terser@5.44.0):
- dependencies:
- cac: 6.7.14
- debug: 4.4.3
- es-module-lexer: 1.7.0
- pathe: 2.0.3
-- vite: 7.2.1(@types/node@18.19.130)(terser@5.44.1)
-+ vite: 7.1.10(@types/node@18.19.130)(terser@5.44.0)
- transitivePeerDependencies:
- - '@types/node'
- - jiti
-@@ -2005,24 +2004,24 @@ snapshots:
- - tsx
- - yaml
-
-- vite@7.2.1(@types/node@18.19.130)(terser@5.44.1):
-+ vite@7.1.10(@types/node@18.19.130)(terser@5.44.0):
- dependencies:
-- esbuild: 0.25.12
-+ esbuild: 0.25.11
- fdir: 6.5.0(picomatch@4.0.3)
- picomatch: 4.0.3
- postcss: 8.5.6
-- rollup: 4.52.5
-+ rollup: 4.52.4
- tinyglobby: 0.2.15
- optionalDependencies:
- '@types/node': 18.19.130
- fsevents: 2.3.3
-- terser: 5.44.1
-+ terser: 5.44.0
-
-- vitest@3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.1):
-+ vitest@3.2.4(@types/node@18.19.130)(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(terser@5.44.0):
- dependencies:
-- '@types/chai': 5.2.3
-+ '@types/chai': 5.2.2
- '@vitest/expect': 3.2.4
-- '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.2.1(@types/node@18.19.130)(terser@5.44.1))
-+ '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.10(@types/node@18.19.130)(terser@5.44.0))
- '@vitest/pretty-format': 3.2.4
- '@vitest/runner': 3.2.4
- '@vitest/snapshot': 3.2.4
-@@ -2031,7 +2030,7 @@ snapshots:
- chai: 5.3.3
- debug: 4.4.3
- expect-type: 1.2.2
-- magic-string: 0.30.21
-+ magic-string: 0.30.19
- pathe: 2.0.3
- picomatch: 4.0.3
- std-env: 3.10.0
-@@ -2040,8 +2039,8 @@ snapshots:
- tinyglobby: 0.2.15
- tinypool: 1.1.1
- tinyrainbow: 2.0.0
-- vite: 7.2.1(@types/node@18.19.130)(terser@5.44.1)
-- vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.1)
-+ vite: 7.1.10(@types/node@18.19.130)(terser@5.44.0)
-+ vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.0)
- why-is-node-running: 2.3.0
- optionalDependencies:
- '@types/node': 18.19.130
-@@ -2076,7 +2075,7 @@ snapshots:
- '@webassemblyjs/wasm-parser': 1.14.1
- acorn: 8.15.0
- acorn-import-phases: 1.0.4(acorn@8.15.0)
-- browserslist: 4.27.0
-+ browserslist: 4.26.3
- chrome-trace-event: 1.0.4
- enhanced-resolve: 5.18.3
- es-module-lexer: 1.7.0
-diff --git a/reference.md b/reference.md
-index 9ccece5..2a5f768 100644
---- a/reference.md
-+++ b/reference.md
-@@ -135,9 +135,7 @@ Filter plants based on their current status.
-
-
- ```typescript
--await client.plant.searchPlantsByStatus({
-- status: "available"
--});
-+await client.plant.searchPlantsByStatus();
-
- ```
-
-@@ -236,7 +234,7 @@ await client.plant.searchPlantsByTags();
-
-
-
--client.plant.getPlantById(plantId) -> FernApi.PlantResponse
-+client.plant.getPlantById({ ...params }) -> FernApi.PlantResponse
-
- -
-
-@@ -263,7 +261,9 @@ Retrieve a plant's details by its ID.
-
-
-
- ```typescript
--await client.plant.getPlantById(1);
-+await client.plant.getPlantById({
-+ plantId: 1
-+});
-
- ```
-
-@@ -279,7 +279,7 @@ await client.plant.getPlantById(1);
-
- -
-
--**plantId:** `number` — ID of the plant to retrieve
-+**request:** `FernApi.GetPlantByIdRequest`
-
-
-
-@@ -313,10 +313,7 @@ await client.plant.getPlantById(1);
- -
-
- ```typescript
--await client.user.loginUser({
-- username: "username",
-- password: "password"
--});
-+await client.user.loginUser();
-
- ```
-
-@@ -393,7 +390,7 @@ await client.user.logoutUser();
-
-
-
--client.user.getUserByName(username) -> FernApi.User
-+client.user.getUserByName({ ...params }) -> FernApi.User
-
- -
-
-@@ -420,7 +417,9 @@ Retrieve user details using their username.
-
-
-
- ```typescript
--await client.user.getUserByName("username");
-+await client.user.getUserByName({
-+ username: "username"
-+});
-
- ```
-
-@@ -436,7 +435,7 @@ await client.user.getUserByName("username");
-
- -
-
--**username:** `string` — Username of the user to retrieve
-+**request:** `FernApi.GetUserByNameRequest`
-
-
-
-diff --git a/src/BaseClient.ts b/src/BaseClient.ts
-index dd0a3f8..6960e72 100644
---- a/src/BaseClient.ts
-+++ b/src/BaseClient.ts
-@@ -13,10 +13,6 @@ export interface BaseClientOptions {
- timeoutInSeconds?: number;
- /** The default number of times to retry the request. Defaults to 2. */
- maxRetries?: number;
-- /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */
-- fetch?: typeof fetch;
-- /** Configure logging for the client. */
-- logging?: core.logging.LogConfig | core.logging.Logger;
- }
-
- export interface BaseRequestOptions {
-diff --git a/src/Client.ts b/src/Client.ts
-index 3705e94..cc6f94d 100644
---- a/src/Client.ts
-+++ b/src/Client.ts
-@@ -20,13 +20,11 @@ export class FernApiClient {
- constructor(_options: FernApiClient.Options = {}) {
- this._options = {
- ..._options,
-- logging: core.logging.createLogger(_options?.logging),
- headers: mergeHeaders(
- {
- "X-Fern-Language": "JavaScript",
- "X-Fern-SDK-Name": "",
-- "X-Fern-SDK-Version": "0.0.1",
-- "User-Agent": "/0.0.1",
-+ "X-Fern-SDK-Version": "AUTO",
- "X-Fern-Runtime": core.RUNTIME.type,
- "X-Fern-Runtime-Version": core.RUNTIME.version,
- },
-diff --git a/src/api/resources/plant/client/Client.ts b/src/api/resources/plant/client/Client.ts
-index d09e424..901121f 100644
---- a/src/api/resources/plant/client/Client.ts
-+++ b/src/api/resources/plant/client/Client.ts
-@@ -62,11 +62,9 @@ export class Plant {
- queryParameters: requestOptions?.queryParams,
- requestType: "json",
- body: request,
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.PlantResponse, rawResponse: _response.rawResponse };
-@@ -142,11 +140,9 @@ export class Plant {
- queryParameters: requestOptions?.queryParams,
- requestType: "json",
- body: request,
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.PlantResponse, rawResponse: _response.rawResponse };
-@@ -191,9 +187,7 @@ export class Plant {
- * @param {Plant.RequestOptions} requestOptions - Request-specific configuration.
- *
- * @example
-- * await client.plant.searchPlantsByStatus({
-- * status: "available"
-- * })
-+ * await client.plant.searchPlantsByStatus()
- */
- public searchPlantsByStatus(
- request: FernApi.SearchPlantsByStatusRequest = {},
-@@ -223,11 +217,9 @@ export class Plant {
- method: "GET",
- headers: _headers,
- queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.PlantResponse[], rawResponse: _response.rawResponse };
-@@ -299,11 +291,9 @@ export class Plant {
- method: "GET",
- headers: _headers,
- queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.PlantResponse[], rawResponse: _response.rawResponse };
-@@ -337,39 +327,40 @@ export class Plant {
- /**
- * Retrieve a plant's details by its ID.
- *
-- * @param {number} plantId - ID of the plant to retrieve
-+ * @param {FernApi.GetPlantByIdRequest} request
- * @param {Plant.RequestOptions} requestOptions - Request-specific configuration.
- *
- * @example
-- * await client.plant.getPlantById(1)
-+ * await client.plant.getPlantById({
-+ * plantId: 1
-+ * })
- */
- public getPlantById(
-- plantId: number,
-+ request: FernApi.GetPlantByIdRequest,
- requestOptions?: Plant.RequestOptions,
- ): core.HttpResponsePromise {
-- return core.HttpResponsePromise.fromPromise(this.__getPlantById(plantId, requestOptions));
-+ return core.HttpResponsePromise.fromPromise(this.__getPlantById(request, requestOptions));
- }
-
- private async __getPlantById(
-- plantId: number,
-+ request: FernApi.GetPlantByIdRequest,
- requestOptions?: Plant.RequestOptions,
- ): Promise> {
-+ const { plantId } = request;
- const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers);
- const _response = await core.fetcher({
- url: core.url.join(
- (await core.Supplier.get(this._options.baseUrl)) ??
- (await core.Supplier.get(this._options.environment)) ??
- environments.FernApiEnvironment.Default,
-- `plant/${core.url.encodePathParam(plantId)}`,
-+ `plant/${encodeURIComponent(plantId)}`,
- ),
- method: "GET",
- headers: _headers,
- queryParameters: requestOptions?.queryParams,
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.PlantResponse, rawResponse: _response.rawResponse };
-diff --git a/src/api/resources/plant/client/requests/GetPlantByIdRequest.ts b/src/api/resources/plant/client/requests/GetPlantByIdRequest.ts
-new file mode 100644
-index 0000000..9077726
---- /dev/null
-+++ b/src/api/resources/plant/client/requests/GetPlantByIdRequest.ts
-@@ -0,0 +1,12 @@
-+// This file was auto-generated by Fern from our API Definition.
-+
-+/**
-+ * @example
-+ * {
-+ * plantId: 1
-+ * }
-+ */
-+export interface GetPlantByIdRequest {
-+ /** ID of the plant to retrieve */
-+ plantId: number;
-+}
-diff --git a/src/api/resources/plant/client/requests/SearchPlantsByStatusRequest.ts b/src/api/resources/plant/client/requests/SearchPlantsByStatusRequest.ts
-index aac77bf..1e70095 100644
---- a/src/api/resources/plant/client/requests/SearchPlantsByStatusRequest.ts
-+++ b/src/api/resources/plant/client/requests/SearchPlantsByStatusRequest.ts
-@@ -4,9 +4,7 @@ import type * as FernApi from "../../../../index.js";
-
- /**
- * @example
-- * {
-- * status: "available"
-- * }
-+ * {}
- */
- export interface SearchPlantsByStatusRequest {
- /** The status of plants to search for. */
-diff --git a/src/api/resources/plant/client/requests/index.ts b/src/api/resources/plant/client/requests/index.ts
-index dd7cff7..46c152a 100644
---- a/src/api/resources/plant/client/requests/index.ts
-+++ b/src/api/resources/plant/client/requests/index.ts
-@@ -1,2 +1,3 @@
-+export type { GetPlantByIdRequest } from "./GetPlantByIdRequest.js";
- export type { SearchPlantsByStatusRequest } from "./SearchPlantsByStatusRequest.js";
- export type { SearchPlantsByTagsRequest } from "./SearchPlantsByTagsRequest.js";
-diff --git a/src/api/resources/user/client/Client.ts b/src/api/resources/user/client/Client.ts
-index e3daa2a..4cb5eca 100644
---- a/src/api/resources/user/client/Client.ts
-+++ b/src/api/resources/user/client/Client.ts
-@@ -28,10 +28,7 @@ export class User {
- * @param {User.RequestOptions} requestOptions - Request-specific configuration.
- *
- * @example
-- * await client.user.loginUser({
-- * username: "username",
-- * password: "password"
-- * })
-+ * await client.user.loginUser()
- */
- public loginUser(
- request: FernApi.LoginUserRequest = {},
-@@ -65,11 +62,9 @@ export class User {
- method: "GET",
- headers: _headers,
- queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.UserAuthResponse, rawResponse: _response.rawResponse };
-@@ -122,11 +117,9 @@ export class User {
- method: "GET",
- headers: _headers,
- queryParameters: requestOptions?.queryParams,
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: undefined, rawResponse: _response.rawResponse };
-@@ -160,39 +153,40 @@ export class User {
- /**
- * Retrieve user details using their username.
- *
-- * @param {string} username - Username of the user to retrieve
-+ * @param {FernApi.GetUserByNameRequest} request
- * @param {User.RequestOptions} requestOptions - Request-specific configuration.
- *
- * @example
-- * await client.user.getUserByName("username")
-+ * await client.user.getUserByName({
-+ * username: "username"
-+ * })
- */
- public getUserByName(
-- username: string,
-+ request: FernApi.GetUserByNameRequest,
- requestOptions?: User.RequestOptions,
- ): core.HttpResponsePromise {
-- return core.HttpResponsePromise.fromPromise(this.__getUserByName(username, requestOptions));
-+ return core.HttpResponsePromise.fromPromise(this.__getUserByName(request, requestOptions));
- }
-
- private async __getUserByName(
-- username: string,
-+ request: FernApi.GetUserByNameRequest,
- requestOptions?: User.RequestOptions,
- ): Promise> {
-+ const { username } = request;
- const _headers: core.Fetcher.Args["headers"] = mergeHeaders(this._options?.headers, requestOptions?.headers);
- const _response = await core.fetcher({
- url: core.url.join(
- (await core.Supplier.get(this._options.baseUrl)) ??
- (await core.Supplier.get(this._options.environment)) ??
- environments.FernApiEnvironment.Default,
-- `user/${core.url.encodePathParam(username)}`,
-+ `user/${encodeURIComponent(username)}`,
- ),
- method: "GET",
- headers: _headers,
- queryParameters: requestOptions?.queryParams,
-- timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
-+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 30) * 1000,
- maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
- abortSignal: requestOptions?.abortSignal,
-- fetchFn: this._options?.fetch,
-- logging: this._options.logging,
- });
- if (_response.ok) {
- return { data: _response.body as FernApi.User, rawResponse: _response.rawResponse };
-diff --git a/src/api/resources/user/client/requests/GetUserByNameRequest.ts b/src/api/resources/user/client/requests/GetUserByNameRequest.ts
-new file mode 100644
-index 0000000..7ef0922
---- /dev/null
-+++ b/src/api/resources/user/client/requests/GetUserByNameRequest.ts
-@@ -0,0 +1,12 @@
-+// This file was auto-generated by Fern from our API Definition.
-+
-+/**
-+ * @example
-+ * {
-+ * username: "username"
-+ * }
-+ */
-+export interface GetUserByNameRequest {
-+ /** Username of the user to retrieve */
-+ username: string;
-+}
-diff --git a/src/api/resources/user/client/requests/LoginUserRequest.ts b/src/api/resources/user/client/requests/LoginUserRequest.ts
-index 3e9cf4e..7ef5be2 100644
---- a/src/api/resources/user/client/requests/LoginUserRequest.ts
-+++ b/src/api/resources/user/client/requests/LoginUserRequest.ts
-@@ -2,10 +2,7 @@
-
- /**
- * @example
-- * {
-- * username: "username",
-- * password: "password"
-- * }
-+ * {}
- */
- export interface LoginUserRequest {
- /** The username for login */
-diff --git a/src/api/resources/user/client/requests/index.ts b/src/api/resources/user/client/requests/index.ts
-index 82b9cff..c8e418b 100644
---- a/src/api/resources/user/client/requests/index.ts
-+++ b/src/api/resources/user/client/requests/index.ts
-@@ -1 +1,2 @@
-+export type { GetUserByNameRequest } from "./GetUserByNameRequest.js";
- export type { LoginUserRequest } from "./LoginUserRequest.js";
-diff --git a/src/api/types/Plant.ts b/src/api/types/Plant.ts
-index 5fcba5b..4aa95d3 100644
---- a/src/api/types/Plant.ts
-+++ b/src/api/types/Plant.ts
-@@ -1,7 +1,8 @@
- // This file was auto-generated by Fern from our API Definition.
-
- export interface Plant {
-- name?: string;
-+ name: string;
-+ nickName?: string;
- category?: string;
- tags?: string[];
- status?: Plant.Status;
-diff --git a/src/core/exports.ts b/src/core/exports.ts
-deleted file mode 100644
-index 69296d7..0000000
---- a/src/core/exports.ts
-+++ /dev/null
-@@ -1 +0,0 @@
--export * from "./logging/exports.js";
-diff --git a/src/core/fetcher/Fetcher.ts b/src/core/fetcher/Fetcher.ts
-index ef020d4..202e134 100644
---- a/src/core/fetcher/Fetcher.ts
-+++ b/src/core/fetcher/Fetcher.ts
-@@ -1,5 +1,4 @@
- import { toJson } from "../json.js";
--import { createLogger, type LogConfig, type Logger } from "../logging/logger.js";
- import type { APIResponse } from "./APIResponse.js";
- import { createRequestUrl } from "./createRequestUrl.js";
- import type { EndpointMetadata } from "./EndpointMetadata.js";
-@@ -26,12 +25,10 @@ export declare namespace Fetcher {
- maxRetries?: number;
- withCredentials?: boolean;
- abortSignal?: AbortSignal;
-- requestType?: "json" | "file" | "bytes" | "form" | "other";
-+ requestType?: "json" | "file" | "bytes";
- responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response";
- duplex?: "half";
- endpointMetadata?: EndpointMetadata;
-- fetchFn?: typeof fetch;
-- logging?: LogConfig | Logger;
- }
-
- export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError;
-@@ -58,141 +55,6 @@ export declare namespace Fetcher {
- }
- }
-
--const SENSITIVE_HEADERS = new Set([
-- "authorization",
-- "x-api-key",
-- "api-key",
-- "x-auth-token",
-- "cookie",
-- "set-cookie",
-- "proxy-authorization",
-- "x-csrf-token",
-- "x-xsrf-token",
--]);
--
--function redactHeaders(headers: Record): Record {
-- const filtered: Record = {};
-- for (const [key, value] of Object.entries(headers)) {
-- if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
-- filtered[key] = "[REDACTED]";
-- } else {
-- filtered[key] = value;
-- }
-- }
-- return filtered;
--}
--
--const SENSITIVE_QUERY_PARAMS = new Set([
-- "api_key",
-- "api-key",
-- "apikey",
-- "token",
-- "access_token",
-- "access-token",
-- "auth_token",
-- "auth-token",
-- "password",
-- "passwd",
-- "secret",
-- "api_secret",
-- "api-secret",
-- "apisecret",
-- "key",
-- "session",
-- "session_id",
-- "session-id",
--]);
--
--function redactQueryParameters(queryParameters?: Record): Record | undefined {
-- if (queryParameters == null) {
-- return queryParameters;
-- }
-- const redacted: Record = {};
-- for (const [key, value] of Object.entries(queryParameters)) {
-- if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
-- redacted[key] = "[REDACTED]";
-- } else {
-- redacted[key] = value;
-- }
-- }
-- return redacted;
--}
--
--function redactUrl(url: string): string {
-- const protocolIndex = url.indexOf("://");
-- if (protocolIndex === -1) return url;
--
-- const afterProtocol = protocolIndex + 3;
-- const atIndex = url.indexOf("@", afterProtocol);
--
-- if (atIndex !== -1) {
-- const pathStart = url.indexOf("/", afterProtocol);
-- const queryStart = url.indexOf("?", afterProtocol);
-- const fragmentStart = url.indexOf("#", afterProtocol);
--
-- const firstDelimiter = Math.min(
-- pathStart === -1 ? url.length : pathStart,
-- queryStart === -1 ? url.length : queryStart,
-- fragmentStart === -1 ? url.length : fragmentStart,
-- );
--
-- if (atIndex < firstDelimiter) {
-- url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`;
-- }
-- }
--
-- const queryStart = url.indexOf("?");
-- if (queryStart === -1) return url;
--
-- const fragmentStart = url.indexOf("#", queryStart);
-- const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length;
-- const queryString = url.slice(queryStart + 1, queryEnd);
--
-- if (queryString.length === 0) return url;
--
-- // FAST PATH: Quick check if any sensitive keywords present
-- // Using indexOf is faster than regex for simple substring matching
-- const lower = queryString.toLowerCase();
-- const hasSensitive =
-- lower.includes("token") || // catches token, access_token, auth_token, etc.
-- lower.includes("key") || // catches key, api_key, apikey, api-key, etc.
-- lower.includes("password") || // catches password
-- lower.includes("passwd") || // catches passwd
-- lower.includes("secret") || // catches secret, api_secret, etc.
-- lower.includes("session") || // catches session, session_id, session-id
-- lower.includes("auth"); // catches auth_token, auth-token, etc.
--
-- if (!hasSensitive) {
-- return url; // Early exit - no sensitive params
-- }
--
-- // SLOW PATH: Parse and redact
-- const redactedParams: string[] = [];
-- const params = queryString.split("&");
--
-- for (const param of params) {
-- const equalIndex = param.indexOf("=");
-- if (equalIndex === -1) {
-- redactedParams.push(param);
-- continue;
-- }
--
-- const key = param.slice(0, equalIndex);
-- let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase());
--
-- if (!shouldRedact && key.includes("%")) {
-- try {
-- const decodedKey = decodeURIComponent(key);
-- shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase());
-- } catch {}
-- }
--
-- redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param);
-- }
--
-- return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd);
--}
--
- async function getHeaders(args: Fetcher.Args): Promise> {
- const newHeaders: Record = {};
- if (args.body !== undefined && args.contentType != null) {
-@@ -221,22 +83,9 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise= 200 && response.status < 400) {
-- if (logger.isDebug()) {
-- const metadata = {
-- method: args.method,
-- url: redactUrl(url),
-- statusCode: response.status,
-- };
-- logger.debug("HTTP request succeeded", metadata);
-- }
- return {
- ok: true,
- body: (await getResponseBody(response, args.responseType)) as R,
-@@ -271,14 +112,6 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise {
-- if (type === "form") {
-- return toQueryString(body, { arrayFormat: "repeat", encode: true });
-- }
- if (type.includes("json")) {
- return toJson(body);
- } else {
-diff --git a/src/core/headers.ts b/src/core/headers.ts
-index 78ed8b5..a723d22 100644
---- a/src/core/headers.ts
-+++ b/src/core/headers.ts
-@@ -6,11 +6,10 @@ export function mergeHeaders(
- for (const [key, value] of headersArray
- .filter((headers) => headers != null)
- .flatMap((headers) => Object.entries(headers))) {
-- const insensitiveKey = key.toLowerCase();
- if (value != null) {
-- result[insensitiveKey] = value;
-- } else if (insensitiveKey in result) {
-- delete result[insensitiveKey];
-+ result[key] = value;
-+ } else if (key in result) {
-+ delete result[key];
- }
- }
-
-@@ -25,9 +24,8 @@ export function mergeOnlyDefinedHeaders(
- for (const [key, value] of headersArray
- .filter((headers) => headers != null)
- .flatMap((headers) => Object.entries(headers))) {
-- const insensitiveKey = key.toLowerCase();
- if (value != null) {
-- result[insensitiveKey] = value;
-+ result[key] = value;
- }
- }
-
-diff --git a/src/core/index.ts b/src/core/index.ts
-index afa8351..bbb640d 100644
---- a/src/core/index.ts
-+++ b/src/core/index.ts
-@@ -1,4 +1,3 @@
- export * from "./fetcher/index.js";
--export * as logging from "./logging/index.js";
- export * from "./runtime/index.js";
- export * as url from "./url/index.js";
-diff --git a/src/core/logging/exports.ts b/src/core/logging/exports.ts
-deleted file mode 100644
-index 88f6c00..0000000
---- a/src/core/logging/exports.ts
-+++ /dev/null
-@@ -1,19 +0,0 @@
--import * as logger from "./logger.js";
--
--export namespace logging {
-- /**
-- * Configuration for logger instances.
-- */
-- export type LogConfig = logger.LogConfig;
-- export type LogLevel = logger.LogLevel;
-- export const LogLevel: typeof logger.LogLevel = logger.LogLevel;
-- export type ILogger = logger.ILogger;
-- /**
-- * Console logger implementation that outputs to the console.
-- */
-- export type ConsoleLogger = logger.ConsoleLogger;
-- /**
-- * Console logger implementation that outputs to the console.
-- */
-- export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger;
--}
-diff --git a/src/core/logging/index.ts b/src/core/logging/index.ts
-deleted file mode 100644
-index d81cc32..0000000
---- a/src/core/logging/index.ts
-+++ /dev/null
-@@ -1 +0,0 @@
--export * from "./logger.js";
-diff --git a/src/core/logging/logger.ts b/src/core/logging/logger.ts
-deleted file mode 100644
-index a2bdef4..0000000
---- a/src/core/logging/logger.ts
-+++ /dev/null
-@@ -1,203 +0,0 @@
--export const LogLevel = {
-- Debug: "debug",
-- Info: "info",
-- Warn: "warn",
-- Error: "error",
--} as const;
--export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
--const logLevelMap: Record = {
-- [LogLevel.Debug]: 1,
-- [LogLevel.Info]: 2,
-- [LogLevel.Warn]: 3,
-- [LogLevel.Error]: 4,
--};
--
--export interface ILogger {
-- /**
-- * Logs a debug message.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- debug(message: string, ...args: unknown[]): void;
-- /**
-- * Logs an info message.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- info(message: string, ...args: unknown[]): void;
-- /**
-- * Logs a warning message.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- warn(message: string, ...args: unknown[]): void;
-- /**
-- * Logs an error message.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- error(message: string, ...args: unknown[]): void;
--}
--
--/**
-- * Configuration for logger initialization.
-- */
--export interface LogConfig {
-- /**
-- * Minimum log level to output.
-- * @default LogLevel.Info
-- */
-- level?: LogLevel;
-- /**
-- * Logger implementation to use.
-- * @default new ConsoleLogger()
-- */
-- logger?: ILogger;
-- /**
-- * Whether logging should be silenced.
-- * @default true
-- */
-- silent?: boolean;
--}
--
--/**
-- * Default console-based logger implementation.
-- */
--export class ConsoleLogger implements ILogger {
-- debug(message: string, ...args: unknown[]): void {
-- console.debug(message, ...args);
-- }
-- info(message: string, ...args: unknown[]): void {
-- console.info(message, ...args);
-- }
-- warn(message: string, ...args: unknown[]): void {
-- console.warn(message, ...args);
-- }
-- error(message: string, ...args: unknown[]): void {
-- console.error(message, ...args);
-- }
--}
--
--/**
-- * Logger class that provides level-based logging functionality.
-- */
--export class Logger {
-- private readonly level: number;
-- private readonly logger: ILogger;
-- private readonly silent: boolean;
--
-- /**
-- * Creates a new logger instance.
-- * @param config - Logger configuration
-- */
-- constructor(config: Required) {
-- this.level = logLevelMap[config.level];
-- this.logger = config.logger;
-- this.silent = config.silent;
-- }
--
-- /**
-- * Checks if a log level should be output based on configuration.
-- * @param level - The log level to check
-- * @returns True if the level should be logged
-- */
-- public shouldLog(level: LogLevel): boolean {
-- return !this.silent && this.level >= logLevelMap[level];
-- }
--
-- /**
-- * Checks if debug logging is enabled.
-- * @returns True if debug logs should be output
-- */
-- public isDebug(): boolean {
-- return this.shouldLog(LogLevel.Debug);
-- }
--
-- /**
-- * Logs a debug message if debug logging is enabled.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- public debug(message: string, ...args: unknown[]): void {
-- if (this.isDebug()) {
-- this.logger.debug(message, ...args);
-- }
-- }
--
-- /**
-- * Checks if info logging is enabled.
-- * @returns True if info logs should be output
-- */
-- public isInfo(): boolean {
-- return this.shouldLog(LogLevel.Info);
-- }
--
-- /**
-- * Logs an info message if info logging is enabled.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- public info(message: string, ...args: unknown[]): void {
-- if (this.isInfo()) {
-- this.logger.info(message, ...args);
-- }
-- }
--
-- /**
-- * Checks if warning logging is enabled.
-- * @returns True if warning logs should be output
-- */
-- public isWarn(): boolean {
-- return this.shouldLog(LogLevel.Warn);
-- }
--
-- /**
-- * Logs a warning message if warning logging is enabled.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- public warn(message: string, ...args: unknown[]): void {
-- if (this.isWarn()) {
-- this.logger.warn(message, ...args);
-- }
-- }
--
-- /**
-- * Checks if error logging is enabled.
-- * @returns True if error logs should be output
-- */
-- public isError(): boolean {
-- return this.shouldLog(LogLevel.Error);
-- }
--
-- /**
-- * Logs an error message if error logging is enabled.
-- * @param message - The message to log
-- * @param args - Additional arguments to log
-- */
-- public error(message: string, ...args: unknown[]): void {
-- if (this.isError()) {
-- this.logger.error(message, ...args);
-- }
-- }
--}
--
--export function createLogger(config?: LogConfig | Logger): Logger {
-- if (config == null) {
-- return defaultLogger;
-- }
-- if (config instanceof Logger) {
-- return config;
-- }
-- config = config ?? {};
-- config.level ??= LogLevel.Info;
-- config.logger ??= new ConsoleLogger();
-- config.silent ??= true;
-- return new Logger(config as Required);
--}
--
--const defaultLogger: Logger = new Logger({
-- level: LogLevel.Info,
-- logger: new ConsoleLogger(),
-- silent: true,
--});
-diff --git a/src/core/url/encodePathParam.ts b/src/core/url/encodePathParam.ts
-deleted file mode 100644
-index 19b9012..0000000
---- a/src/core/url/encodePathParam.ts
-+++ /dev/null
-@@ -1,18 +0,0 @@
--export function encodePathParam(param: unknown): string {
-- if (param === null) {
-- return "null";
-- }
-- const typeofParam = typeof param;
-- switch (typeofParam) {
-- case "undefined":
-- return "undefined";
-- case "string":
-- case "number":
-- case "boolean":
-- break;
-- default:
-- param = String(param);
-- break;
-- }
-- return encodeURIComponent(param as string | number | boolean);
--}
-diff --git a/src/core/url/index.ts b/src/core/url/index.ts
-index f2e0fa2..ed5aa0f 100644
---- a/src/core/url/index.ts
-+++ b/src/core/url/index.ts
-@@ -1,3 +1,2 @@
--export { encodePathParam } from "./encodePathParam.js";
- export { join } from "./join.js";
- export { toQueryString } from "./qs.js";
-diff --git a/src/exports.ts b/src/exports.ts
-deleted file mode 100644
-index 7b70ee1..0000000
---- a/src/exports.ts
-+++ /dev/null
-@@ -1 +0,0 @@
--export * from "./core/exports.js";
-diff --git a/src/index.ts b/src/index.ts
-index e9c07eb..002f149 100644
---- a/src/index.ts
-+++ b/src/index.ts
-@@ -3,4 +3,3 @@ export type { BaseClientOptions, BaseRequestOptions } from "./BaseClient.js";
- export { FernApiClient } from "./Client.js";
- export { FernApiEnvironment } from "./environments.js";
- export { FernApiError, FernApiTimeoutError } from "./errors/index.js";
--export * from "./exports.js";
-diff --git a/src/version.ts b/src/version.ts
-index b643a3e..4e39e59 100644
---- a/src/version.ts
-+++ b/src/version.ts
-@@ -1 +1 @@
--export const SDK_VERSION = "0.0.1";
-+export const SDK_VERSION = "AUTO";
-diff --git a/tests/mock-server/mockEndpointBuilder.ts b/tests/mock-server/mockEndpointBuilder.ts
-index 1b0e510..18557ec 100644
---- a/tests/mock-server/mockEndpointBuilder.ts
-+++ b/tests/mock-server/mockEndpointBuilder.ts
-@@ -2,7 +2,6 @@ import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponse
-
- import { url } from "../../src/core";
- import { toJson } from "../../src/core/json";
--import { withFormUrlEncoded } from "./withFormUrlEncoded";
- import { withHeaders } from "./withHeaders";
- import { withJson } from "./withJson";
-
-@@ -27,7 +26,6 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage {
-
- interface RequestBodyStage extends ResponseStage {
- jsonBody(body: unknown): ResponseStage;
-- formUrlEncodedBody(body: unknown): ResponseStage;
- }
-
- interface ResponseStage {
-@@ -137,16 +135,6 @@ class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodySta
- return this;
- }
-
-- formUrlEncodedBody(body: unknown): ResponseStage {
-- if (body === undefined) {
-- throw new Error(
-- "Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.",
-- );
-- }
-- this.predicates.push((resolver) => withFormUrlEncoded(body, resolver));
-- return this;
-- }
--
- respondWith(): ResponseStatusStage {
- return new ResponseBuilder(this.method, this.buildUrl(), this.predicates, this.handlerOptions);
- }
-diff --git a/tests/mock-server/withFormUrlEncoded.ts b/tests/mock-server/withFormUrlEncoded.ts
-deleted file mode 100644
-index e9e6ff2..0000000
---- a/tests/mock-server/withFormUrlEncoded.ts
-+++ /dev/null
-@@ -1,80 +0,0 @@
--import { type HttpResponseResolver, passthrough } from "msw";
--
--import { toJson } from "../../src/core/json";
--
--/**
-- * Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object
-- * @param expectedBody - The exact body object to match against
-- * @param resolver - Response resolver to execute if body matches
-- */
--export function withFormUrlEncoded(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver {
-- return async (args) => {
-- const { request } = args;
--
-- let clonedRequest: Request;
-- let bodyText: string | undefined;
-- let actualBody: Record;
-- try {
-- clonedRequest = request.clone();
-- bodyText = await clonedRequest.text();
-- if (bodyText === "") {
-- console.error("Request body is empty, expected a form-urlencoded body.");
-- return passthrough();
-- }
-- const params = new URLSearchParams(bodyText);
-- actualBody = {};
-- for (const [key, value] of params.entries()) {
-- actualBody[key] = value;
-- }
-- } catch (error) {
-- console.error(`Error processing form-urlencoded request body:\n\tError: ${error}\n\tBody: ${bodyText}`);
-- return passthrough();
-- }
--
-- const mismatches = findMismatches(actualBody, expectedBody);
-- if (Object.keys(mismatches).length > 0) {
-- console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2));
-- return passthrough();
-- }
--
-- return resolver(args);
-- };
--}
--
--function findMismatches(actual: any, expected: any): Record {
-- const mismatches: Record = {};
--
-- if (typeof actual !== typeof expected) {
-- return { value: { actual, expected } };
-- }
--
-- if (typeof actual !== "object" || actual === null || expected === null) {
-- if (actual !== expected) {
-- return { value: { actual, expected } };
-- }
-- return {};
-- }
--
-- const actualKeys = Object.keys(actual);
-- const expectedKeys = Object.keys(expected);
--
-- const allKeys = new Set([...actualKeys, ...expectedKeys]);
--
-- for (const key of allKeys) {
-- if (!expectedKeys.includes(key)) {
-- if (actual[key] === undefined) {
-- continue;
-- }
-- mismatches[key] = { actual: actual[key], expected: undefined };
-- } else if (!actualKeys.includes(key)) {
-- if (expected[key] === undefined) {
-- continue;
-- }
-- mismatches[key] = { actual: undefined, expected: expected[key] };
-- } else if (actual[key] !== expected[key]) {
-- mismatches[key] = { actual: actual[key], expected: expected[key] };
-- }
-- }
--
-- return mismatches;
--}
-diff --git a/tests/unit/fetcher/getRequestBody.test.ts b/tests/unit/fetcher/getRequestBody.test.ts
-index e3da10c..e864c8b 100644
---- a/tests/unit/fetcher/getRequestBody.test.ts
-+++ b/tests/unit/fetcher/getRequestBody.test.ts
-@@ -45,65 +45,7 @@ describe("Test getRequestBody", () => {
- expect(result).toBe(input);
- });
-
-- it("should serialize objects for form-urlencoded content type", async () => {
-- const input = { username: "johndoe", email: "john@example.com" };
-- const result = await getRequestBody({
-- body: input,
-- type: "form",
-- });
-- expect(result).toBe("username=johndoe&email=john%40example.com");
-- });
--
-- it("should serialize complex nested objects and arrays for form-urlencoded content type", async () => {
-- const input = {
-- user: {
-- profile: {
-- name: "John Doe",
-- settings: {
-- theme: "dark",
-- notifications: true,
-- },
-- },
-- tags: ["admin", "user"],
-- contacts: [
-- { type: "email", value: "john@example.com" },
-- { type: "phone", value: "+1234567890" },
-- ],
-- },
-- filters: {
-- status: ["active", "pending"],
-- metadata: {
-- created: "2024-01-01",
-- categories: ["electronics", "books"],
-- },
-- },
-- preferences: ["notifications", "updates"],
-- };
-- const result = await getRequestBody({
-- body: input,
-- type: "form",
-- });
-- expect(result).toBe(
-- "user%5Bprofile%5D%5Bname%5D=John%20Doe&" +
-- "user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark&" +
-- "user%5Bprofile%5D%5Bsettings%5D%5Bnotifications%5D=true&" +
-- "user%5Btags%5D=admin&" +
-- "user%5Btags%5D=user&" +
-- "user%5Bcontacts%5D%5Btype%5D=email&" +
-- "user%5Bcontacts%5D%5Bvalue%5D=john%40example.com&" +
-- "user%5Bcontacts%5D%5Btype%5D=phone&" +
-- "user%5Bcontacts%5D%5Bvalue%5D=%2B1234567890&" +
-- "filters%5Bstatus%5D=active&" +
-- "filters%5Bstatus%5D=pending&" +
-- "filters%5Bmetadata%5D%5Bcreated%5D=2024-01-01&" +
-- "filters%5Bmetadata%5D%5Bcategories%5D=electronics&" +
-- "filters%5Bmetadata%5D%5Bcategories%5D=books&" +
-- "preferences=notifications&" +
-- "preferences=updates",
-- );
-- });
--
-- it("should return the input for pre-serialized form-urlencoded strings", async () => {
-+ it("should return the input for content-type 'application/x-www-form-urlencoded'", async () => {
- const input = "key=value&another=param";
- const result = await getRequestBody({
- body: input,
-diff --git a/tests/wire/plant.test.ts b/tests/wire/plant.test.ts
-index 9531e5e..13f448b 100644
---- a/tests/wire/plant.test.ts
-+++ b/tests/wire/plant.test.ts
-@@ -36,7 +36,7 @@ describe("Plant", () => {
- test("addPlant (2)", async () => {
- const server = mockServerPool.createServer();
- const client = new FernApiClient({ environment: server.baseUrl });
-- const rawRequestBody = {};
-+ const rawRequestBody = { name: "name" };
- const rawResponseBody = { key: "value" };
- server
- .mockEndpoint()
-@@ -48,7 +48,9 @@ describe("Plant", () => {
- .build();
-
- await expect(async () => {
-- return await client.plant.addPlant({});
-+ return await client.plant.addPlant({
-+ name: "name",
-+ });
- }).rejects.toThrow(FernApi.MethodNotAllowedError);
- });
-
-@@ -83,7 +85,7 @@ describe("Plant", () => {
- test("updatePlant (2)", async () => {
- const server = mockServerPool.createServer();
- const client = new FernApiClient({ environment: server.baseUrl });
-- const rawRequestBody = {};
-+ const rawRequestBody = { name: "name" };
- const rawResponseBody = { key: "value" };
- server
- .mockEndpoint()
-@@ -95,14 +97,16 @@ describe("Plant", () => {
- .build();
-
- await expect(async () => {
-- return await client.plant.updatePlant({});
-+ return await client.plant.updatePlant({
-+ name: "name",
-+ });
- }).rejects.toThrow(FernApi.BadRequestError);
- });
-
- test("updatePlant (3)", async () => {
- const server = mockServerPool.createServer();
- const client = new FernApiClient({ environment: server.baseUrl });
-- const rawRequestBody = {};
-+ const rawRequestBody = { name: "name" };
- const rawResponseBody = { key: "value" };
- server
- .mockEndpoint()
-@@ -114,7 +118,9 @@ describe("Plant", () => {
- .build();
-
- await expect(async () => {
-- return await client.plant.updatePlant({});
-+ return await client.plant.updatePlant({
-+ name: "name",
-+ });
- }).rejects.toThrow(FernApi.NotFoundError);
- });
-
-@@ -134,9 +140,7 @@ describe("Plant", () => {
- .jsonBody(rawResponseBody)
- .build();
-
-- const response = await client.plant.searchPlantsByStatus({
-- status: "available",
-- });
-+ const response = await client.plant.searchPlantsByStatus();
- expect(response).toEqual([
- {
- id: 101,
-@@ -187,7 +191,9 @@ describe("Plant", () => {
- const rawResponseBody = { id: 101, name: "Fern", status: "available", tags: ["green", "leafy"] };
- server.mockEndpoint().get("/plant/1").respondWith().statusCode(200).jsonBody(rawResponseBody).build();
-
-- const response = await client.plant.getPlantById(1);
-+ const response = await client.plant.getPlantById({
-+ plantId: 1,
-+ });
- expect(response).toEqual({
- id: 101,
- name: "Fern",
-diff --git a/tests/wire/user.test.ts b/tests/wire/user.test.ts
-index 3755bb7..595c6ba 100644
---- a/tests/wire/user.test.ts
-+++ b/tests/wire/user.test.ts
-@@ -11,10 +11,7 @@ describe("User", () => {
- const rawResponseBody = { token: "abc123token", expiresIn: 3600 };
- server.mockEndpoint().get("/user/auth/login").respondWith().statusCode(200).jsonBody(rawResponseBody).build();
-
-- const response = await client.user.loginUser({
-- username: "username",
-- password: "password",
-- });
-+ const response = await client.user.loginUser();
- expect(response).toEqual({
- token: "abc123token",
- expiresIn: 3600,
-@@ -38,7 +35,9 @@ describe("User", () => {
- const rawResponseBody = { id: 1, username: "john_doe", email: "john@example.com" };
- server.mockEndpoint().get("/user/username").respondWith().statusCode(200).jsonBody(rawResponseBody).build();
-
-- const response = await client.user.getUserByName("username");
-+ const response = await client.user.getUserByName({
-+ username: "username",
-+ });
- expect(response).toEqual({
- id: 1,
- username: "john_doe",
-diff --git a/vitest.config.mts b/vitest.config.ts
-similarity index 100%
-rename from vitest.config.mts
-rename to vitest.config.ts
diff --git a/packages/configs/build-utils.mjs b/packages/configs/build-utils.mjs
new file mode 100644
index 000000000000..e9893f15ab21
--- /dev/null
+++ b/packages/configs/build-utils.mjs
@@ -0,0 +1,74 @@
+import tsup from 'tsup';
+import { cp } from 'fs/promises';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+/**
+ * Standard build function for Fern generators
+ * @param {string} dirname - The __dirname of the calling build.mjs file
+ * @param {Object} options - Build options
+ * @param {string} [options.entry='src/cli.ts'] - Entry point for tsup
+ * @param {Object} [options.tsupOptions={}] - Additional tsup configuration options to merge
+ * @param {string|string[]|Object|Object[]|null} [options.copyFrom=null] - Files/folders to copy after build
+ * Can be:
+ * - string: '../base/src/asIs' - copies to dist/
+ * - array of strings: ['../base/src/asIs', '../base/src/template'] - copies each to dist/
+ * - object: { from: '../base/src/asIs', to: 'dist/asIs' } - custom destination
+ * - array of objects: [{ from: '...', to: '...' }, ...]
+ */
+export async function buildGenerator(dirname, options = {}) {
+ const {
+ entry = 'src/cli.ts',
+ tsupOptions = {},
+ copyFrom = null
+ } = options;
+
+ // Build with tsup (merge default options with custom ones)
+ const defaultTsupOptions = {
+ entry: [entry],
+ format: ['cjs'],
+ sourcemap: true,
+ clean: true,
+ outDir: 'dist',
+ esbuildOptions(options) {
+ options.conditions = ['development', 'source', 'import', 'default']
+ },
+ };
+
+ await tsup.build({
+ ...defaultTsupOptions,
+ ...tsupOptions,
+ });
+
+ // Copy additional files if needed
+ if (copyFrom) {
+ const copyOperations = Array.isArray(copyFrom) ? copyFrom : [copyFrom];
+
+ for (const copyOp of copyOperations) {
+ if (typeof copyOp === 'string') {
+ // Simple string: copy to dist/
+ await cp(
+ path.join(dirname, copyOp),
+ path.join(dirname, 'dist'),
+ { recursive: true }
+ );
+ } else if (typeof copyOp === 'object' && copyOp.from) {
+ // Object with from/to: custom destination
+ await cp(
+ path.join(dirname, copyOp.from),
+ path.join(dirname, copyOp.to),
+ { recursive: true, force: true }
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Helper to get __dirname in ESM modules
+ * @param {string} importMetaUrl - import.meta.url from the calling module
+ * @returns {string} The directory name
+ */
+export function getDirname(importMetaUrl) {
+ return path.dirname(fileURLToPath(importMetaUrl));
+}
diff --git a/packages/configs/package.json b/packages/configs/package.json
index eb4b6a0f7822..9b1aaab5ff48 100644
--- a/packages/configs/package.json
+++ b/packages/configs/package.json
@@ -4,6 +4,7 @@
"private": true,
"license": "MIT",
"devDependencies": {
+ "tsup": "^8.5.0",
"vitest": "^4.0.8"
}
}
diff --git a/packages/seed/CLAUDE.md b/packages/seed/CLAUDE.md
index 64ebab5f0644..fb47dcde887d 100644
--- a/packages/seed/CLAUDE.md
+++ b/packages/seed/CLAUDE.md
@@ -22,7 +22,7 @@ Seed is Fern's **comprehensive generator testing framework** that validates gene
- `src/config/` - Configuration loading and validation
- `src/utils/` - Utilities for workspace management, logging, etc.
- `fern/` - Seed's own Fern API definitions
-- `build.cjs` - CLI distribution build script
+- `build.mjs` - CLI distribution build script
### Command Structure
- `test/` - Run predefined fixtures against generators
diff --git a/packages/seed/build.cjs b/packages/seed/build.mjs
similarity index 70%
rename from packages/seed/build.cjs
rename to packages/seed/build.mjs
index d80eed6bbecc..2227bd0d7a58 100644
--- a/packages/seed/build.cjs
+++ b/packages/seed/build.mjs
@@ -1,7 +1,10 @@
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile } = require("fs/promises");
-const path = require("path");
+import packageJson from "./package.json" with { type: "json" };
+import tsup from 'tsup';
+import { writeFile } from "fs/promises";
+import path from "path";
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
main();
@@ -12,6 +15,10 @@ async function main() {
minify: false,
outDir: 'dist',
sourcemap: true,
+ clean: true,
+ esbuildOptions(options) {
+ options.conditions = ['development', 'source', 'import', 'default']
+ },
env: {
CLI_NAME: "seed",
CLI_PACKAGE_NAME: "seed-cli",
@@ -20,7 +27,7 @@ async function main() {
external: [
'@fern-api/go-formatter',
'@boundaryml/baml',
- ],
+ ],
});
process.chdir(path.join(__dirname, "dist"));
@@ -42,4 +49,4 @@ async function main() {
2
)
);
-}
\ No newline at end of file
+}
diff --git a/packages/seed/package.json b/packages/seed/package.json
index ec701f2f6953..2920f15ace0b 100644
--- a/packages/seed/package.json
+++ b/packages/seed/package.json
@@ -27,7 +27,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm compile && node build.cjs",
+ "dist:cli": "node build.mjs",
"env:prod": "env-cmd -r .env-cmdrc.cjs -e prod",
"generate": "fern generate --local",
"test": "vitest --run",
diff --git a/packages/snippets/core/build.cjs b/packages/snippets/core/build.cjs
deleted file mode 100644
index d4c63fb6103e..000000000000
--- a/packages/snippets/core/build.cjs
+++ /dev/null
@@ -1,77 +0,0 @@
-const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
-const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill');
-const packageJson = require("./package.json");
-const tsup = require('tsup');
-const { writeFile, mkdir } = require("fs/promises");
-const path = require("path");
-
-main();
-
-async function main() {
- const config = {
- entry: ['src/**/*.ts', '!src/__test__'],
- target: "es2017",
- minify: true,
- dts: true,
- sourcemap: true,
- esbuildPlugins: [
- NodeModulesPolyfillPlugin(),
- NodeGlobalsPolyfillPlugin({
- process: true,
- buffer: true,
- util: true
- })
- ],
- tsconfig: "./build.tsconfig.json"
- };
-
- await tsup.build({
- ...config,
- format: ['cjs'],
- outDir: 'dist/cjs',
- clean: true,
- });
-
- await tsup.build({
- ...config,
- format: ['esm'],
- outDir: 'dist/esm',
- clean: false,
- });
-
- await mkdir(path.join(__dirname, "dist"), { recursive: true });
- process.chdir(path.join(__dirname, "dist"));
-
- await writeFile(
- "package.json",
- JSON.stringify(
- {
- name: packageJson.name,
- version: process.argv[2] || packageJson.version,
- repository: packageJson.repository,
- type: "module",
- exports: {
- // Conditional exports for ESM and CJS.
- "import": {
- "types": "./esm/index.d.ts",
- "default": "./esm/index.js"
- },
- "require": {
- "types": "./cjs/index.d.cts",
- "default": "./cjs/index.cjs"
- }
- },
- // Fallback for older tooling or direct imports.
- main: "./cjs/index.cjs",
- module: "./esm/index.js",
- types: "./cjs/index.d.cts",
- files: [
- "cjs",
- "esm"
- ]
- },
- undefined,
- 2
- )
- );
-}
\ No newline at end of file
diff --git a/packages/snippets/core/package.json b/packages/snippets/core/package.json
index 9200b4cea8a3..3ffe848ffea5 100644
--- a/packages/snippets/core/package.json
+++ b/packages/snippets/core/package.json
@@ -27,14 +27,11 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist": "pnpm compile && node build.cjs",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
"test:update": "vitest --passWithNoTests --run -u"
},
"devDependencies": {
- "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
- "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@fern-api/api-workspace-commons": "workspace:*",
"@fern-api/browser-compatible-fern-workspace": "workspace:*",
"@fern-api/configs": "workspace:*",
@@ -46,7 +43,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"openapi-types": "^12.1.3",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7271f9bd047e..a5953ff0ea6d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -296,9 +296,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -504,9 +501,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -516,12 +510,6 @@ importers:
generators/go-v2/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -549,9 +537,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -746,12 +731,6 @@ importers:
generators/java-v2/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -782,9 +761,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -938,9 +914,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -984,12 +957,6 @@ importers:
generators/php/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -1020,9 +987,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1276,12 +1240,6 @@ importers:
generators/python-v2/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -1315,9 +1273,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1550,12 +1505,6 @@ importers:
generators/ruby-v2/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -1586,9 +1535,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -2175,12 +2121,6 @@ importers:
generators/swift/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -2211,9 +2151,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.3.3)
typescript:
specifier: 5.9.2
version: 5.9.2
@@ -2540,12 +2477,6 @@ importers:
generators/typescript-v2/dynamic-snippets:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/browser-compatible-base-generator':
specifier: workspace:*
version: link:../../browser-compatible-base
@@ -2573,9 +2504,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -7851,6 +7779,9 @@ importers:
packages/configs:
devDependencies:
+ tsup:
+ specifier: ^8.5.0
+ version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
vitest:
specifier: ^4.0.8
version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jsdom@27.1.0)(msw@2.12.1(@types/node@24.10.0)(typescript@5.9.3))(sass@1.94.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.3.3)
@@ -8114,12 +8045,6 @@ importers:
packages/snippets/core:
devDependencies:
- '@esbuild-plugins/node-globals-polyfill':
- specifier: ^0.2.3
- version: 0.2.3(esbuild@0.27.0)
- '@esbuild-plugins/node-modules-polyfill':
- specifier: ^0.2.2
- version: 0.2.2(esbuild@0.27.0)
'@fern-api/api-workspace-commons':
specifier: workspace:*
version: link:../../cli/workspace/commons
@@ -8153,9 +8078,6 @@ importers:
openapi-types:
specifier: ^12.1.3
version: 12.1.3
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -9219,328 +9141,162 @@ packages:
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
- '@esbuild-plugins/node-globals-polyfill@0.2.3':
- resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
- peerDependencies:
- esbuild: '*'
-
- '@esbuild-plugins/node-modules-polyfill@0.2.2':
- resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==}
- peerDependencies:
- esbuild: '*'
-
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
- '@esbuild/aix-ppc64@0.27.0':
- resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [aix]
-
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
- '@esbuild/android-arm64@0.27.0':
- resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [android]
-
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
- '@esbuild/android-arm@0.27.0':
- resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [android]
-
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
- '@esbuild/android-x64@0.27.0':
- resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [android]
-
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
- '@esbuild/darwin-arm64@0.27.0':
- resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [darwin]
-
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
- '@esbuild/darwin-x64@0.27.0':
- resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [darwin]
-
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
- '@esbuild/freebsd-arm64@0.27.0':
- resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [freebsd]
-
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
- '@esbuild/freebsd-x64@0.27.0':
- resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [freebsd]
-
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
- '@esbuild/linux-arm64@0.27.0':
- resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [linux]
-
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
- '@esbuild/linux-arm@0.27.0':
- resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==}
- engines: {node: '>=18'}
- cpu: [arm]
- os: [linux]
-
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
- '@esbuild/linux-ia32@0.27.0':
- resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [linux]
-
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
- '@esbuild/linux-loong64@0.27.0':
- resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==}
- engines: {node: '>=18'}
- cpu: [loong64]
- os: [linux]
-
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
- '@esbuild/linux-mips64el@0.27.0':
- resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==}
- engines: {node: '>=18'}
- cpu: [mips64el]
- os: [linux]
-
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
- '@esbuild/linux-ppc64@0.27.0':
- resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==}
- engines: {node: '>=18'}
- cpu: [ppc64]
- os: [linux]
-
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
- '@esbuild/linux-riscv64@0.27.0':
- resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==}
- engines: {node: '>=18'}
- cpu: [riscv64]
- os: [linux]
-
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
- '@esbuild/linux-s390x@0.27.0':
- resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==}
- engines: {node: '>=18'}
- cpu: [s390x]
- os: [linux]
-
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
- '@esbuild/linux-x64@0.27.0':
- resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [linux]
-
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
- '@esbuild/netbsd-arm64@0.27.0':
- resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [netbsd]
-
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
- '@esbuild/netbsd-x64@0.27.0':
- resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [netbsd]
-
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
- '@esbuild/openbsd-arm64@0.27.0':
- resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openbsd]
-
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
- '@esbuild/openbsd-x64@0.27.0':
- resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [openbsd]
-
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
- '@esbuild/openharmony-arm64@0.27.0':
- resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [openharmony]
-
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
- '@esbuild/sunos-x64@0.27.0':
- resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [sunos]
-
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
- '@esbuild/win32-arm64@0.27.0':
- resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==}
- engines: {node: '>=18'}
- cpu: [arm64]
- os: [win32]
-
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
- '@esbuild/win32-ia32@0.27.0':
- resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==}
- engines: {node: '>=18'}
- cpu: [ia32]
- os: [win32]
-
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
- '@esbuild/win32-x64@0.27.0':
- resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [win32]
-
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -12220,11 +11976,6 @@ packages:
engines: {node: '>=18'}
hasBin: true
- esbuild@0.27.0:
- resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==}
- engines: {node: '>=18'}
- hasBin: true
-
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -12328,9 +12079,6 @@ packages:
estree-util-visit@2.0.0:
resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
- estree-walker@0.6.1:
- resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
-
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -13570,9 +13318,6 @@ packages:
lru-memoizer@2.3.0:
resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==}
- magic-string@0.25.9:
- resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
-
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -14643,16 +14388,6 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
- rollup-plugin-inject@3.0.2:
- resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
- deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
-
- rollup-plugin-node-polyfills@0.2.1:
- resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==}
-
- rollup-pluginutils@2.8.2:
- resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
-
rollup@4.53.2:
resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -14847,10 +14582,6 @@ packages:
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
- sourcemap-codec@1.4.8:
- resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
- deprecated: Please use @jridgewell/sourcemap-codec instead
-
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
@@ -17067,172 +16798,84 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.27.0)':
- dependencies:
- esbuild: 0.27.0
-
- '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.27.0)':
- dependencies:
- esbuild: 0.27.0
- escape-string-regexp: 4.0.0
- rollup-plugin-node-polyfills: 0.2.1
-
'@esbuild/aix-ppc64@0.25.12':
optional: true
- '@esbuild/aix-ppc64@0.27.0':
- optional: true
-
'@esbuild/android-arm64@0.25.12':
optional: true
- '@esbuild/android-arm64@0.27.0':
- optional: true
-
'@esbuild/android-arm@0.25.12':
optional: true
- '@esbuild/android-arm@0.27.0':
- optional: true
-
'@esbuild/android-x64@0.25.12':
optional: true
- '@esbuild/android-x64@0.27.0':
- optional: true
-
'@esbuild/darwin-arm64@0.25.12':
optional: true
- '@esbuild/darwin-arm64@0.27.0':
- optional: true
-
'@esbuild/darwin-x64@0.25.12':
optional: true
- '@esbuild/darwin-x64@0.27.0':
- optional: true
-
'@esbuild/freebsd-arm64@0.25.12':
optional: true
- '@esbuild/freebsd-arm64@0.27.0':
- optional: true
-
'@esbuild/freebsd-x64@0.25.12':
optional: true
- '@esbuild/freebsd-x64@0.27.0':
- optional: true
-
'@esbuild/linux-arm64@0.25.12':
optional: true
- '@esbuild/linux-arm64@0.27.0':
- optional: true
-
'@esbuild/linux-arm@0.25.12':
optional: true
- '@esbuild/linux-arm@0.27.0':
- optional: true
-
'@esbuild/linux-ia32@0.25.12':
optional: true
- '@esbuild/linux-ia32@0.27.0':
- optional: true
-
'@esbuild/linux-loong64@0.25.12':
optional: true
- '@esbuild/linux-loong64@0.27.0':
- optional: true
-
'@esbuild/linux-mips64el@0.25.12':
optional: true
- '@esbuild/linux-mips64el@0.27.0':
- optional: true
-
'@esbuild/linux-ppc64@0.25.12':
optional: true
- '@esbuild/linux-ppc64@0.27.0':
- optional: true
-
'@esbuild/linux-riscv64@0.25.12':
optional: true
- '@esbuild/linux-riscv64@0.27.0':
- optional: true
-
'@esbuild/linux-s390x@0.25.12':
optional: true
- '@esbuild/linux-s390x@0.27.0':
- optional: true
-
'@esbuild/linux-x64@0.25.12':
optional: true
- '@esbuild/linux-x64@0.27.0':
- optional: true
-
'@esbuild/netbsd-arm64@0.25.12':
optional: true
- '@esbuild/netbsd-arm64@0.27.0':
- optional: true
-
'@esbuild/netbsd-x64@0.25.12':
optional: true
- '@esbuild/netbsd-x64@0.27.0':
- optional: true
-
'@esbuild/openbsd-arm64@0.25.12':
optional: true
- '@esbuild/openbsd-arm64@0.27.0':
- optional: true
-
'@esbuild/openbsd-x64@0.25.12':
optional: true
- '@esbuild/openbsd-x64@0.27.0':
- optional: true
-
'@esbuild/openharmony-arm64@0.25.12':
optional: true
- '@esbuild/openharmony-arm64@0.27.0':
- optional: true
-
'@esbuild/sunos-x64@0.25.12':
optional: true
- '@esbuild/sunos-x64@0.27.0':
- optional: true
-
'@esbuild/win32-arm64@0.25.12':
optional: true
- '@esbuild/win32-arm64@0.27.0':
- optional: true
-
'@esbuild/win32-ia32@0.25.12':
optional: true
- '@esbuild/win32-ia32@0.27.0':
- optional: true
-
'@esbuild/win32-x64@0.25.12':
optional: true
- '@esbuild/win32-x64@0.27.0':
- optional: true
-
'@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
@@ -20502,35 +20145,6 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
- esbuild@0.27.0:
- optionalDependencies:
- '@esbuild/aix-ppc64': 0.27.0
- '@esbuild/android-arm': 0.27.0
- '@esbuild/android-arm64': 0.27.0
- '@esbuild/android-x64': 0.27.0
- '@esbuild/darwin-arm64': 0.27.0
- '@esbuild/darwin-x64': 0.27.0
- '@esbuild/freebsd-arm64': 0.27.0
- '@esbuild/freebsd-x64': 0.27.0
- '@esbuild/linux-arm': 0.27.0
- '@esbuild/linux-arm64': 0.27.0
- '@esbuild/linux-ia32': 0.27.0
- '@esbuild/linux-loong64': 0.27.0
- '@esbuild/linux-mips64el': 0.27.0
- '@esbuild/linux-ppc64': 0.27.0
- '@esbuild/linux-riscv64': 0.27.0
- '@esbuild/linux-s390x': 0.27.0
- '@esbuild/linux-x64': 0.27.0
- '@esbuild/netbsd-arm64': 0.27.0
- '@esbuild/netbsd-x64': 0.27.0
- '@esbuild/openbsd-arm64': 0.27.0
- '@esbuild/openbsd-x64': 0.27.0
- '@esbuild/openharmony-arm64': 0.27.0
- '@esbuild/sunos-x64': 0.27.0
- '@esbuild/win32-arm64': 0.27.0
- '@esbuild/win32-ia32': 0.27.0
- '@esbuild/win32-x64': 0.27.0
-
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -20700,8 +20314,6 @@ snapshots:
'@types/estree-jsx': 1.0.5
'@types/unist': 3.0.3
- estree-walker@0.6.1: {}
-
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -22272,10 +21884,6 @@ snapshots:
lodash.clonedeep: 4.5.0
lru-cache: 6.0.0
- magic-string@0.25.9:
- dependencies:
- sourcemap-codec: 1.4.8
-
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -23884,20 +23492,6 @@ snapshots:
dependencies:
glob: 7.2.3
- rollup-plugin-inject@3.0.2:
- dependencies:
- estree-walker: 0.6.1
- magic-string: 0.25.9
- rollup-pluginutils: 2.8.2
-
- rollup-plugin-node-polyfills@0.2.1:
- dependencies:
- rollup-plugin-inject: 3.0.2
-
- rollup-pluginutils@2.8.2:
- dependencies:
- estree-walker: 0.6.1
-
rollup@4.53.2:
dependencies:
'@types/estree': 1.0.8
@@ -24156,8 +23750,6 @@ snapshots:
dependencies:
whatwg-url: 7.1.0
- sourcemap-codec@1.4.8: {}
-
space-separated-tokens@2.0.2: {}
spdx-correct@3.2.0:
diff --git a/turbo.json b/turbo.json
index c4f356e07fb9..31213bdf25dd 100644
--- a/turbo.json
+++ b/turbo.json
@@ -16,31 +16,72 @@
"cache": false
},
"compile": {
- "dependsOn": ["^compile"],
- "outputs": ["lib/**"],
- "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json"]
+ "dependsOn": [
+ "^compile"
+ ],
+ "outputs": [
+ "lib/**"
+ ],
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "$TURBO_ROOT$/packages/configs/**"
+ ]
},
"compile:debug": {
- "dependsOn": ["^compile:debug"],
- "outputs": ["lib/**"],
- "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json"]
+ "dependsOn": [
+ "^compile:debug"
+ ],
+ "outputs": [
+ "lib/**"
+ ],
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "$TURBO_ROOT$/packages/configs/**"
+ ]
},
"depcheck": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "package.json", ".depcheckrc.json", "$TURBO_ROOT$/.depcheckrc.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ ".depcheckrc.json",
+ "$TURBO_ROOT$/.depcheckrc.json"
+ ]
},
"lint:eslint": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "$TURBO_ROOT$/.eslintrc.js",
+ "$TURBO_ROOT$/.eslintignore",
+ "tsconfig.json"
+ ]
},
"lint:eslint:fix": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "$TURBO_ROOT$/.eslintrc.js",
+ "$TURBO_ROOT$/.eslintignore",
+ "tsconfig.json"
+ ]
},
"test": {
- "dependsOn": ["^compile"],
+ "dependsOn": [
+ "^compile"
+ ],
"outputs": [],
"inputs": [
+ "$TURBO_ROOT$/packages/configs/**",
"$TURBO_ROOT$/shared/vitest.config.ts",
"$TURBO_ROOT$/test-definitions-openapi/**",
"$TURBO_ROOT$/test-definitions/**",
@@ -53,9 +94,12 @@
]
},
"test:debug": {
- "dependsOn": ["^compile:debug"],
+ "dependsOn": [
+ "^compile:debug"
+ ],
"outputs": [],
"inputs": [
+ "$TURBO_ROOT$/packages/configs/**",
"$TURBO_ROOT$/shared/vitest.config.ts",
"$TURBO_ROOT$/test-definitions-openapi/**",
"$TURBO_ROOT$/test-definitions/**",
@@ -68,9 +112,12 @@
]
},
"test:update": {
- "dependsOn": ["^compile"],
+ "dependsOn": [
+ "^compile"
+ ],
"outputs": [],
"inputs": [
+ "$TURBO_ROOT$/packages/configs/**",
"$TURBO_ROOT$/shared/vitest.config.ts",
"$TURBO_ROOT$/test-definitions-openapi/**",
"$TURBO_ROOT$/test-definitions/**",
@@ -83,61 +130,83 @@
]
},
"dist:cli": {
- "dependsOn": ["^compile"],
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
"package.json",
"tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
"$TURBO_ROOT$/shared/.prettierignore",
"$TURBO_ROOT$/shared/stylelintrc.shared.json",
"$TURBO_ROOT$/tsconfig.eslint.json"
]
},
"dist:cli:dev": {
- "dependsOn": ["^compile"],
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
"package.json",
"tsconfig.json",
+ "build-utils.mjs",
+ "build.dev.mjs",
"$TURBO_ROOT$/shared/.prettierignore",
"$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
+ "$TURBO_ROOT$/tsconfig.eslint.json",
+ "$TURBO_ROOT$/packages/configs/**"
]
},
"dist:cli:local": {
- "dependsOn": ["^compile"],
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
"package.json",
"tsconfig.json",
+ "build-utils.mjs",
+ "build.local.mjs",
"$TURBO_ROOT$/shared/.prettierignore",
"$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
+ "$TURBO_ROOT$/tsconfig.eslint.json",
+ "$TURBO_ROOT$/packages/configs/**"
]
},
"dist:cli:prod": {
- "dependsOn": ["^compile"],
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
"package.json",
"tsconfig.json",
+ "build-utils.mjs",
+ "build.prod.mjs",
"$TURBO_ROOT$/shared/.prettierignore",
"$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
+ "$TURBO_ROOT$/tsconfig.eslint.json",
+ "$TURBO_ROOT$/packages/configs/**"
]
},
"dist": {
- "with": ["dist:cli", "dist:cli:dev", "dist:cli:local", "dist:cli:prod"],
+ "with": [
+ "dist:cli",
+ "dist:cli:dev",
+ "dist:cli:local",
+ "dist:cli:prod"
+ ],
"cache": false,
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -145,50 +214,9 @@
"tsconfig.json",
"$TURBO_ROOT$/shared/.prettierignore",
"$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
- ]
- },
- "fern:build": {
- "dependsOn": ["^compile"],
- "outputs": ["packages/cli/cli/dist/prod/**"],
- "inputs": [
- "packages/cli/cli/src/**",
- "packages/cli/cli/tests/**",
- "packages/cli/cli/package.json",
- "packages/cli/cli/tsconfig.json",
- "packages/cli/cli/build.prod.cjs",
- "$TURBO_ROOT$/shared/.prettierignore",
- "$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
- ]
- },
- "fern-dev:build": {
- "dependsOn": ["^compile"],
- "outputs": ["packages/cli/cli/dist/dev/**"],
- "inputs": [
- "packages/cli/cli/src/**",
- "packages/cli/cli/tests/**",
- "packages/cli/cli/package.json",
- "packages/cli/cli/tsconfig.json",
- "packages/cli/cli/build.dev.cjs",
- "$TURBO_ROOT$/shared/.prettierignore",
- "$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
- ]
- },
- "seed:build": {
- "dependsOn": ["^compile"],
- "outputs": ["packages/seed/dist/**"],
- "inputs": [
- "packages/seed/src/**",
- "packages/seed/tests/**",
- "packages/seed/package.json",
- "packages/seed/tsconfig.json",
- "packages/seed/build.cjs",
- "$TURBO_ROOT$/shared/.prettierignore",
- "$TURBO_ROOT$/shared/stylelintrc.shared.json",
- "$TURBO_ROOT$/tsconfig.eslint.json"
+ "$TURBO_ROOT$/tsconfig.eslint.json",
+ "$TURBO_ROOT$/packages/configs/**"
]
}
}
-}
+}
\ No newline at end of file
From 1857ead9711ecd6a29643d2c353352d03738e33f Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:03:41 -0500
Subject: [PATCH 02/16] depcheck
---
generators/csharp/model/package.json | 1 -
generators/csharp/sdk/package.json | 1 -
generators/go-v2/model/package.json | 1 -
generators/go-v2/sdk/package.json | 1 -
generators/java-v2/sdk/package.json | 1 -
generators/openapi/package.json | 1 -
generators/php/model/package.json | 1 -
generators/php/sdk/package.json | 1 -
generators/postman/package.json | 1 -
generators/postman/tsup.config.ts | 7 --
generators/python-v2/fastapi/package.json | 1 -
.../python-v2/pydantic-model/package.json | 1 -
generators/python-v2/sdk/package.json | 1 -
generators/ruby-v2/model/package.json | 1 -
generators/ruby-v2/sdk/package.json | 1 -
generators/ruby/model/package.json | 1 -
generators/ruby/sdk/package.json | 1 -
.../rust/dynamic-snippets/build.tsconfig.json | 7 --
generators/rust/dynamic-snippets/package.json | 2 -
generators/rust/model/package.json | 1 -
generators/rust/sdk/package.json | 1 -
generators/rust/sdk/tsup.config.ts | 12 ---
generators/swift/model/package.json | 1 -
generators/swift/sdk/package.json | 1 -
generators/typescript-mcp/model/package.json | 1 -
generators/typescript-mcp/server/package.json | 1 -
.../typescript/express/cli/.depcheckrc.json | 2 +-
.../typescript/express/cli/package.json | 3 +-
.../typescript/sdk/cli/.depcheckrc.json | 2 +-
generators/typescript/sdk/cli/package.json | 3 +-
pnpm-lock.yaml | 100 ------------------
31 files changed, 4 insertions(+), 156 deletions(-)
delete mode 100644 generators/postman/tsup.config.ts
delete mode 100644 generators/rust/dynamic-snippets/build.tsconfig.json
delete mode 100644 generators/rust/sdk/tsup.config.ts
diff --git a/generators/csharp/model/package.json b/generators/csharp/model/package.json
index 399d269496e9..b45c071483e0 100644
--- a/generators/csharp/model/package.json
+++ b/generators/csharp/model/package.json
@@ -49,7 +49,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/csharp/sdk/package.json b/generators/csharp/sdk/package.json
index edeee688ebe8..628d3ad07fb6 100644
--- a/generators/csharp/sdk/package.json
+++ b/generators/csharp/sdk/package.json
@@ -54,7 +54,6 @@
"@types/node": "18.15.3",
"@types/url-join": "4.0.1",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"url-join": "^5.0.0",
"vitest": "^4.0.8"
diff --git a/generators/go-v2/model/package.json b/generators/go-v2/model/package.json
index 2c2e2ba50dce..62ca8c65ba71 100644
--- a/generators/go-v2/model/package.json
+++ b/generators/go-v2/model/package.json
@@ -48,7 +48,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/go-v2/sdk/package.json b/generators/go-v2/sdk/package.json
index 07fbdfa4ff0c..8e65f39fae75 100644
--- a/generators/go-v2/sdk/package.json
+++ b/generators/go-v2/sdk/package.json
@@ -55,7 +55,6 @@
"@types/node": "18.15.3",
"dedent": "^1.5.1",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/java-v2/sdk/package.json b/generators/java-v2/sdk/package.json
index 6213056c6ade..db418bb0bce5 100644
--- a/generators/java-v2/sdk/package.json
+++ b/generators/java-v2/sdk/package.json
@@ -54,7 +54,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/openapi/package.json b/generators/openapi/package.json
index 7d3b31ba70de..f8a7dfb2a3a1 100644
--- a/generators/openapi/package.json
+++ b/generators/openapi/package.json
@@ -55,7 +55,6 @@
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.21",
"openapi-types": "^12.1.3",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"url-join": "^5.0.0",
"vitest": "^4.0.8"
diff --git a/generators/php/model/package.json b/generators/php/model/package.json
index f340334ee9a0..25630ad0981d 100644
--- a/generators/php/model/package.json
+++ b/generators/php/model/package.json
@@ -47,7 +47,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/php/sdk/package.json b/generators/php/sdk/package.json
index 771f1ecd60c6..9296f5418e82 100644
--- a/generators/php/sdk/package.json
+++ b/generators/php/sdk/package.json
@@ -55,7 +55,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/postman/package.json b/generators/postman/package.json
index 56106cc2d76e..b58ac48e01d1 100644
--- a/generators/postman/package.json
+++ b/generators/postman/package.json
@@ -53,7 +53,6 @@
"depcheck": "^1.4.7",
"endent": "^2.1.0",
"lodash": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/postman/tsup.config.ts b/generators/postman/tsup.config.ts
deleted file mode 100644
index b948a27a8128..000000000000
--- a/generators/postman/tsup.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { defineConfig } from "tsup";
-
-export default defineConfig({
- entry: ["src/cli.ts"],
- format: "cjs",
- sourcemap: true
-});
diff --git a/generators/python-v2/fastapi/package.json b/generators/python-v2/fastapi/package.json
index 75490023d351..ef9d1610bc8d 100644
--- a/generators/python-v2/fastapi/package.json
+++ b/generators/python-v2/fastapi/package.json
@@ -42,7 +42,6 @@
"@fern-api/configs": "workspace:*",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/python-v2/pydantic-model/package.json b/generators/python-v2/pydantic-model/package.json
index e7b3f10a4dbc..2585cdebfcdf 100644
--- a/generators/python-v2/pydantic-model/package.json
+++ b/generators/python-v2/pydantic-model/package.json
@@ -49,7 +49,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.4"
diff --git a/generators/python-v2/sdk/package.json b/generators/python-v2/sdk/package.json
index f14802611850..9296411a5c53 100644
--- a/generators/python-v2/sdk/package.json
+++ b/generators/python-v2/sdk/package.json
@@ -54,7 +54,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.4"
diff --git a/generators/ruby-v2/model/package.json b/generators/ruby-v2/model/package.json
index 09603aeabd58..2bcb8990f98f 100644
--- a/generators/ruby-v2/model/package.json
+++ b/generators/ruby-v2/model/package.json
@@ -48,7 +48,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/ruby-v2/sdk/package.json b/generators/ruby-v2/sdk/package.json
index 23335ec5e372..0aeb76824d6d 100644
--- a/generators/ruby-v2/sdk/package.json
+++ b/generators/ruby-v2/sdk/package.json
@@ -59,7 +59,6 @@
"dedent": "^1.5.1",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/ruby/model/package.json b/generators/ruby/model/package.json
index 19f99a261550..e565cd420ce5 100644
--- a/generators/ruby/model/package.json
+++ b/generators/ruby/model/package.json
@@ -51,7 +51,6 @@
"@fern-fern/ir-sdk": "^39",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/ruby/sdk/package.json b/generators/ruby/sdk/package.json
index 489c68646f2e..04a56c95252b 100644
--- a/generators/ruby/sdk/package.json
+++ b/generators/ruby/sdk/package.json
@@ -45,7 +45,6 @@
"@fern-fern/ir-sdk": "^39",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/rust/dynamic-snippets/build.tsconfig.json b/generators/rust/dynamic-snippets/build.tsconfig.json
deleted file mode 100644
index 27476223521c..000000000000
--- a/generators/rust/dynamic-snippets/build.tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "composite": false,
- "incremental": false
- }
-}
diff --git a/generators/rust/dynamic-snippets/package.json b/generators/rust/dynamic-snippets/package.json
index f801a4c50b84..5f5decbb534c 100644
--- a/generators/rust/dynamic-snippets/package.json
+++ b/generators/rust/dynamic-snippets/package.json
@@ -38,9 +38,7 @@
"@fern-api/dynamic-ir-sdk": "^59.6.1",
"@fern-api/path-utils": "workspace:*",
"@types/node": "18.15.3",
- "@yarnpkg/esbuild-plugin-pnp": "3.0.0-rc.15",
"depcheck": "^1.4.7",
- "esbuild": "^0.25.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/rust/model/package.json b/generators/rust/model/package.json
index 7b864e4632c5..82e5696d62e5 100644
--- a/generators/rust/model/package.json
+++ b/generators/rust/model/package.json
@@ -50,7 +50,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.4"
diff --git a/generators/rust/sdk/package.json b/generators/rust/sdk/package.json
index fd67e3c2d6c9..9c8a0518cd00 100644
--- a/generators/rust/sdk/package.json
+++ b/generators/rust/sdk/package.json
@@ -55,7 +55,6 @@
"@fern-fern/ir-sdk": "^61.7.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8",
"zod": "^3.22.4"
diff --git a/generators/rust/sdk/tsup.config.ts b/generators/rust/sdk/tsup.config.ts
deleted file mode 100644
index 210098021568..000000000000
--- a/generators/rust/sdk/tsup.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from "tsup";
-
-export default defineConfig({
- entry: ["src/cli.ts"],
- format: ["cjs"],
- // Bundle ALL dependencies including workspace packages
- noExternal: [/@fern-api\/.*/, /dedent/],
- dts: false,
- splitting: false,
- sourcemap: false,
- clean: true
-});
diff --git a/generators/swift/model/package.json b/generators/swift/model/package.json
index 09ebf660d7fc..4f0a489df302 100644
--- a/generators/swift/model/package.json
+++ b/generators/swift/model/package.json
@@ -49,7 +49,6 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.2",
"vitest": "^4.0.8",
"zod": "^3.22.3"
diff --git a/generators/swift/sdk/package.json b/generators/swift/sdk/package.json
index e31f35a77953..fa392739a8b6 100644
--- a/generators/swift/sdk/package.json
+++ b/generators/swift/sdk/package.json
@@ -54,7 +54,6 @@
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.2",
"vitest": "^4.0.8",
"zod": "^3.25.75"
diff --git a/generators/typescript-mcp/model/package.json b/generators/typescript-mcp/model/package.json
index 93176670c7b0..f5faee288b12 100644
--- a/generators/typescript-mcp/model/package.json
+++ b/generators/typescript-mcp/model/package.json
@@ -43,7 +43,6 @@
"@fern-fern/ir-sdk": "^58.2.0",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/typescript-mcp/server/package.json b/generators/typescript-mcp/server/package.json
index 3b824f646950..930bb1583693 100644
--- a/generators/typescript-mcp/server/package.json
+++ b/generators/typescript-mcp/server/package.json
@@ -46,7 +46,6 @@
"@fern-typescript/sdk-generator-cli": "workspace:*",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
diff --git a/generators/typescript/express/cli/.depcheckrc.json b/generators/typescript/express/cli/.depcheckrc.json
index 6b406215dacc..018f9dd71981 100644
--- a/generators/typescript/express/cli/.depcheckrc.json
+++ b/generators/typescript/express/cli/.depcheckrc.json
@@ -1,4 +1,4 @@
{
"ignores": [],
- "ignore-patterns": ["lib", "docker/bundle.js"]
+ "ignore-patterns": ["lib", "dist", "docker/bundle.js"]
}
diff --git a/generators/typescript/express/cli/package.json b/generators/typescript/express/cli/package.json
index 5d5ed01448f6..198fc0d67ed4 100644
--- a/generators/typescript/express/cli/package.json
+++ b/generators/typescript/express/cli/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm clean && pnpm compile && node build.mjs",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../.."
},
@@ -42,7 +42,6 @@
"@types/lodash-es": "^4.17.12",
"depcheck": "^1.4.7",
"lodash-es": "^4.17.21",
- "tsup": "^8.5.0",
"typescript": "5.9.3",
"zod": "^3.22.3"
}
diff --git a/generators/typescript/sdk/cli/.depcheckrc.json b/generators/typescript/sdk/cli/.depcheckrc.json
index 6b406215dacc..d389561009c7 100644
--- a/generators/typescript/sdk/cli/.depcheckrc.json
+++ b/generators/typescript/sdk/cli/.depcheckrc.json
@@ -1,4 +1,4 @@
{
"ignores": [],
- "ignore-patterns": ["lib", "docker/bundle.js"]
+ "ignore-patterns": ["dist", "lib", "docker/bundle.js"]
}
diff --git a/generators/typescript/sdk/cli/package.json b/generators/typescript/sdk/cli/package.json
index 39122a907d9b..91074645e735 100644
--- a/generators/typescript/sdk/cli/package.json
+++ b/generators/typescript/sdk/cli/package.json
@@ -26,7 +26,7 @@
"compile": "tsc --build",
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
- "dist:cli": "pnpm clean && pnpm compile && node build.mjs",
+ "dist:cli": "node build.mjs",
"dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../..",
"podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../.."
},
@@ -44,7 +44,6 @@
"@fern-typescript/sdk-generator": "workspace:*",
"@types/node": "18.15.3",
"depcheck": "^1.4.7",
- "tsup": "^8.5.0",
"typescript": "5.9.3"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f189dc9c1e66..57fb4f3cd2c8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -363,9 +363,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -420,9 +417,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -598,9 +592,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -658,9 +649,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -815,9 +803,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -866,9 +851,6 @@ importers:
openapi-types:
specifier: ^12.1.3
version: 12.1.3
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1020,9 +1002,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1083,9 +1062,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1131,9 +1107,6 @@ importers:
lodash:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1291,9 +1264,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1357,9 +1327,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1417,9 +1384,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1580,9 +1544,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1652,9 +1613,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1774,9 +1732,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1822,9 +1777,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -1978,9 +1930,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -2042,9 +1991,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -2206,9 +2152,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.3.3)
typescript:
specifier: 5.9.2
version: 5.9.2
@@ -2278,9 +2221,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.3.3)
typescript:
specifier: 5.9.2
version: 5.9.2
@@ -2359,9 +2299,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -2404,9 +2341,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -2583,9 +2517,6 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -3195,9 +3126,6 @@ importers:
depcheck:
specifier: ^1.4.7
version: 1.4.7
- tsup:
- specifier: ^8.5.0
- version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3)
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -24317,34 +24245,6 @@ snapshots:
tslib@2.8.1: {}
- tsup@8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.3.3):
- dependencies:
- bundle-require: 5.1.0(esbuild@0.25.12)
- cac: 6.7.14
- chokidar: 4.0.3
- consola: 3.4.2
- debug: 4.4.3
- esbuild: 0.25.12
- fix-dts-default-cjs-exports: 1.0.1
- joycon: 3.1.1
- picocolors: 1.1.1
- postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6)(yaml@2.3.3)
- resolve-from: 5.0.0
- rollup: 4.53.2
- source-map: 0.8.0-beta.0
- sucrase: 3.35.0
- tinyexec: 0.3.2
- tinyglobby: 0.2.15
- tree-kill: 1.2.2
- optionalDependencies:
- postcss: 8.5.6
- typescript: 5.9.2
- transitivePeerDependencies:
- - jiti
- - supports-color
- - tsx
- - yaml
-
tsup@8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.3.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.12)
From 89a4a304985493613006b61725c6e8b81c740dd9 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:05:45 -0500
Subject: [PATCH 03/16] pnpm install
---
pnpm-lock.yaml | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 57fb4f3cd2c8..a431e7be9552 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1879,15 +1879,9 @@ importers:
'@types/node':
specifier: 18.15.3
version: 18.15.3
- '@yarnpkg/esbuild-plugin-pnp':
- specifier: 3.0.0-rc.15
- version: 3.0.0-rc.15(esbuild@0.25.12)
depcheck:
specifier: ^1.4.7
version: 1.4.7
- esbuild:
- specifier: ^0.25.0
- version: 0.25.12
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -10860,12 +10854,6 @@ packages:
'@wasm-fmt/ruff_fmt@0.6.1':
resolution: {integrity: sha512-7O3kVJ3JLYbo2l6+2LQHtPfq+1/Bna7tg8TE9sUft0Wj70qizlu7QJY6hC7KpJwW8LQjvSlLRtGv5W5wu0Pa5w==}
- '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15':
- resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==}
- engines: {node: '>=14.15.0'}
- peerDependencies:
- esbuild: '>=0.10.0'
-
'@yarnpkg/lockfile@1.1.0':
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
@@ -18942,11 +18930,6 @@ snapshots:
'@wasm-fmt/ruff_fmt@0.6.1': {}
- '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.25.12)':
- dependencies:
- esbuild: 0.25.12
- tslib: 2.8.1
-
'@yarnpkg/lockfile@1.1.0': {}
'@yarnpkg/parsers@3.0.2':
From b4497c72aafc159b14a7ff450cc9db29c5944b7b Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:28:22 -0500
Subject: [PATCH 04/16] chore: update build process by removing Turbo CLI seed
build and refining workflow steps
---
.github/actions/publish-generator/action.yaml | 2 +-
.github/workflows/publish-generator-cli.yml | 9 +++++++--
.github/workflows/validate-changelog.yml | 3 ---
generators/rust/dynamic-snippets/package.json | 2 ++
4 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/.github/actions/publish-generator/action.yaml b/.github/actions/publish-generator/action.yaml
index 98ea8d8ea594..ef9faa4f7a22 100644
--- a/.github/actions/publish-generator/action.yaml
+++ b/.github/actions/publish-generator/action.yaml
@@ -45,7 +45,7 @@ runs:
shell: bash
run: |
echo "Building seed CLI once with Turbo caching..."
- pnpm turbo run dist:cli --filter=@fern-api/seed-cli
+ pnpm seed:build
- name: Run publish (auto)
if: inputs.manual-trigger == 'false'
diff --git a/.github/workflows/publish-generator-cli.yml b/.github/workflows/publish-generator-cli.yml
index 37fae18af56a..f69cb46fea23 100644
--- a/.github/workflows/publish-generator-cli.yml
+++ b/.github/workflows/publish-generator-cli.yml
@@ -79,10 +79,15 @@ jobs:
- name: Update npm
run: npm install -g npm@latest
- - name: 🧪 Build and test
+ - name: Compile
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
- run: pnpm turbo compile test --filter=${{ env.PACKAGE_NAME }}
+ run: pnpm turbo compile --filter=${{ env.PACKAGE_NAME }}
+
+ - name: Test
+ env:
+ GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
+ run: pnpm turbo test --filter=${{ env.PACKAGE_NAME }}
- name: Setup Node for npm publish
uses: actions/setup-node@v4
diff --git a/.github/workflows/validate-changelog.yml b/.github/workflows/validate-changelog.yml
index be0383512462..71dfca4f7a5b 100644
--- a/.github/workflows/validate-changelog.yml
+++ b/.github/workflows/validate-changelog.yml
@@ -39,9 +39,6 @@ jobs:
- name: Install
uses: ./.github/actions/install
- - name: Compile
- run: pnpm exec turbo compile '--filter=@fern-api/seed-cli'
-
- name: Seed Build
run: pnpm seed:build
diff --git a/generators/rust/dynamic-snippets/package.json b/generators/rust/dynamic-snippets/package.json
index 5f5decbb534c..0410d3ea295e 100644
--- a/generators/rust/dynamic-snippets/package.json
+++ b/generators/rust/dynamic-snippets/package.json
@@ -9,6 +9,8 @@
},
"exports": {
".": {
+ "development": "./src/index.ts",
+ "source": "./src/index.ts",
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"default": "./lib/index.js"
From 211740abdcf7c11281b4d16f61eb246020e432c5 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:35:12 -0500
Subject: [PATCH 05/16] chore: update package.json exports and remove unused
build.tsconfig.json
---
packages/commons/github/package.json | 8 ++++--
packages/snippets/core/build.tsconfig.json | 32 ----------------------
2 files changed, 6 insertions(+), 34 deletions(-)
delete mode 100644 packages/snippets/core/build.tsconfig.json
diff --git a/packages/commons/github/package.json b/packages/commons/github/package.json
index 13ad484ff2b4..b99e05b5634c 100644
--- a/packages/commons/github/package.json
+++ b/packages/commons/github/package.json
@@ -11,6 +11,8 @@
"type": "module",
"exports": {
".": {
+ "development": "./src/index.ts",
+ "source": "./src/index.ts",
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
@@ -19,7 +21,9 @@
"module": "src/index.ts",
"source": "src/index.ts",
"types": "lib/index.d.ts",
- "files": ["lib"],
+ "files": [
+ "lib"
+ ],
"scripts": {
"clean": "rm -rf ./lib && rm -rf ./dist && tsc --build --clean",
"compile": "tsc --build",
@@ -46,4 +50,4 @@
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
-}
+}
\ No newline at end of file
diff --git a/packages/snippets/core/build.tsconfig.json b/packages/snippets/core/build.tsconfig.json
deleted file mode 100644
index 812fa0c8b9b9..000000000000
--- a/packages/snippets/core/build.tsconfig.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/tsconfig",
- "extends": "@fern-api/configs/tsconfig/main.json",
- "include": ["src/**/*.ts"],
- "exclude": ["src/**/__test__"],
- "compilerOptions": {
- "composite": false
- },
- "references": [
- {
- "path": "../../cli/configuration"
- },
- {
- "path": "../../cli/generation/ir-generator"
- },
- {
- "path": "../../cli/generation/source-resolver"
- },
- {
- "path": "../../cli/task-context"
- },
- {
- "path": "../../cli/workspace/browser-compatible-fern-workspace"
- },
- {
- "path": "../../cli/workspace/commons"
- },
- {
- "path": "../../ir-sdk"
- }
- ]
-}
From 5546b4b3a768a6790f7e9a067218fd1c9c54a8b3 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:50:48 -0500
Subject: [PATCH 06/16] fmt
---
packages/commons/github/package.json | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/packages/commons/github/package.json b/packages/commons/github/package.json
index b99e05b5634c..b59231767e61 100644
--- a/packages/commons/github/package.json
+++ b/packages/commons/github/package.json
@@ -21,9 +21,7 @@
"module": "src/index.ts",
"source": "src/index.ts",
"types": "lib/index.d.ts",
- "files": [
- "lib"
- ],
+ "files": ["lib"],
"scripts": {
"clean": "rm -rf ./lib && rm -rf ./dist && tsc --build --clean",
"compile": "tsc --build",
@@ -50,4 +48,4 @@
"typescript": "5.9.3",
"vitest": "^4.0.8"
}
-}
\ No newline at end of file
+}
From 41ddf3dc641078e9a2b773ecb3da278cb4ad7040 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 20:39:37 -0500
Subject: [PATCH 07/16] chore: update Turbo commands in workflows and scripts
for consistency
---
.github/workflows/publish-generator-cli.yml | 4 +-
package.json | 26 ++---
packages/cli/cli/build-utils.mjs | 3 -
packages/configs/build-utils.mjs | 3 -
turbo.json | 109 ++++++++++++++++----
5 files changed, 105 insertions(+), 40 deletions(-)
diff --git a/.github/workflows/publish-generator-cli.yml b/.github/workflows/publish-generator-cli.yml
index f69cb46fea23..648f50a48325 100644
--- a/.github/workflows/publish-generator-cli.yml
+++ b/.github/workflows/publish-generator-cli.yml
@@ -82,12 +82,12 @@ jobs:
- name: Compile
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
- run: pnpm turbo compile --filter=${{ env.PACKAGE_NAME }}
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: Test
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
- run: pnpm turbo test --filter=${{ env.PACKAGE_NAME }}
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Setup Node for npm publish
uses: actions/setup-node@v4
diff --git a/package.json b/package.json
index 6f5291993d29..5ffd382e45e4 100644
--- a/package.json
+++ b/package.json
@@ -13,14 +13,14 @@
"scripts": {
"bootstrap": "./scripts/bootstrap.sh",
"sparse-checkout": "./scripts/sparse-checkout.sh",
- "clean": "turbo clean",
- "compile": "turbo compile",
- "compile:debug": "turbo compile:debug",
+ "clean": "turbo run clean",
+ "compile": "turbo run compile",
+ "compile:debug": "turbo run compile:debug",
"compile:win": "pnpm -r compile",
- "test": "turbo test --filter=!@fern-api/ete-tests",
- "test:debug": "turbo test:debug --filter=!@fern-api/ete-tests",
- "test:update": "turbo test:update --filter=!@fern-api/ete-tests",
- "test:update:dockerless": "turbo test:update --filter=!@fern-api/ete-tests --filter=!@fern-api/docker-utils",
+ "test": "turbo run test --filter=!@fern-api/ete-tests",
+ "test:debug": "turbo run test:debug --filter=!@fern-api/ete-tests",
+ "test:update": "turbo run test:update --filter=!@fern-api/ete-tests",
+ "test:update:dockerless": "turbo run test:update --filter=!@fern-api/ete-tests --filter=!@fern-api/docker-utils",
"test:ete": "pnpm --filter @fern-api/cli dist:cli:dev && pnpm --filter @fern-api/seed-cli dist:cli && pnpm --filter @fern-api/ete-tests test",
"test:ete:update": "pnpm --filter @fern-api/cli dist:cli:dev && pnpm --filter @fern-api/seed-cli dist:cli && pnpm --filter @fern-api/ete-tests test -- -u",
"lint:biome": "biome lint --error-on-warnings",
@@ -37,17 +37,17 @@
"check:biome": "biome check",
"check": "biome check",
"check:fix": "biome check --write",
- "depcheck": "turbo depcheck --continue=always",
+ "depcheck": "turbo run depcheck --continue=always",
"codegen:local": "pnpm fern:local generate",
"fern": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern-dev:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/dev/cli.cjs",
- "fern:build": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter=@fern-api/cli dist:cli:prod && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
+ "fern:build": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod --filter=@fern-api/cli --graph && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter @fern-api/cli dist:cli:prod:unminified && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
- "fern-dev:build": "pnpm --filter=@fern-api/cli dist:cli:dev && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
- "fern-local:build": "pnpm --filter @fern-api/cli dist:cli:local && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
+ "fern-dev:build": "turbo run dist:cli:dev --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
+ "fern-local:build": "turbo run dist:cli:local --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
"generator-cli:generate": "pnpm fern generate --api generator-cli --local && pnpm --filter=@fern-api/generator-cli compile",
- "seed:build": "pnpm --filter=@fern-api/seed-cli dist:cli && echo 'Run node --enable-source-maps packages/seed/dist/cli.cjs'",
+ "seed:build": "turbo run dist:cli --filter=@fern-api/seed-cli && echo 'Run node --enable-source-maps packages/seed/dist/cli.cjs'",
"publish": "pnpm -r publish --access public --no-git-checks --loglevel=verbose",
"jsonschema": "pnpm definition-yml:jsonschema && pnpm api-yml:jsonschema && pnpm package-yml:jsonschema && pnpm docs-yml:jsonschema && pnpm generators-yml:jsonschema && pnpm versions-yml:jsonschema && pnpm products-yml:jsonschema",
"definition-yml:jsonschema": "pnpm fern jsonschema fern.schema.json --api fern-definition --type file.DefinitionFileSchema && pnpm fern jsonschema packages/cli/workspace/lazy-fern-workspace/src/fern.schema.json --api fern-definition --type file.DefinitionFileSchema",
@@ -143,4 +143,4 @@
"dependencies": {
"js-yaml": "^4.1.1"
}
-}
+}
\ No newline at end of file
diff --git a/packages/cli/cli/build-utils.mjs b/packages/cli/cli/build-utils.mjs
index c06cee11da66..7dbb715d6983 100644
--- a/packages/cli/cli/build-utils.mjs
+++ b/packages/cli/cli/build-utils.mjs
@@ -71,9 +71,6 @@ export async function buildCli(config) {
outDir,
sourcemap: true,
clean: true,
- esbuildOptions(options) {
- options.conditions = ['development', 'source', 'import', 'default']
- },
env: {
...env,
CLI_VERSION: process.argv[2] || packageJson.version,
diff --git a/packages/configs/build-utils.mjs b/packages/configs/build-utils.mjs
index e9893f15ab21..e1f0c38df232 100644
--- a/packages/configs/build-utils.mjs
+++ b/packages/configs/build-utils.mjs
@@ -30,9 +30,6 @@ export async function buildGenerator(dirname, options = {}) {
sourcemap: true,
clean: true,
outDir: 'dist',
- esbuildOptions(options) {
- options.conditions = ['development', 'source', 'import', 'default']
- },
};
await tsup.build({
diff --git a/turbo.json b/turbo.json
index 51344b0e8306..915746ba6537 100644
--- a/turbo.json
+++ b/turbo.json
@@ -16,29 +16,69 @@
"cache": false
},
"compile": {
- "dependsOn": ["^compile"],
- "outputs": ["lib/**"],
- "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json", "$TURBO_ROOT$/packages/configs/**"]
+ "dependsOn": [
+ "^compile"
+ ],
+ "outputs": [
+ "lib/**"
+ ],
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "$TURBO_ROOT$/packages/configs/**"
+ ]
},
"compile:debug": {
- "dependsOn": ["^compile:debug"],
- "outputs": ["lib/**"],
- "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json", "$TURBO_ROOT$/packages/configs/**"]
+ "dependsOn": [
+ "^compile:debug"
+ ],
+ "outputs": [
+ "lib/**"
+ ],
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "$TURBO_ROOT$/packages/configs/**"
+ ]
},
"depcheck": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "package.json", ".depcheckrc.json", "$TURBO_ROOT$/.depcheckrc.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ ".depcheckrc.json",
+ "$TURBO_ROOT$/.depcheckrc.json"
+ ]
},
"lint:eslint": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "$TURBO_ROOT$/.eslintrc.js",
+ "$TURBO_ROOT$/.eslintignore",
+ "tsconfig.json"
+ ]
},
"lint:eslint:fix": {
"outputs": [],
- "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "$TURBO_ROOT$/.eslintrc.js",
+ "$TURBO_ROOT$/.eslintignore",
+ "tsconfig.json"
+ ]
},
"test": {
- "dependsOn": ["^compile"],
+ "dependsOn": [
+ "^compile"
+ ],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -54,7 +94,9 @@
]
},
"test:debug": {
- "dependsOn": ["^compile:debug"],
+ "dependsOn": [
+ "^compile:debug"
+ ],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -70,7 +112,9 @@
]
},
"test:update": {
- "dependsOn": ["^compile"],
+ "dependsOn": [
+ "^compile"
+ ],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -86,7 +130,12 @@
]
},
"dist:cli": {
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
+ "dependsOn": [
+ "^compile"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -100,7 +149,12 @@
]
},
"dist:cli:dev": {
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
+ "dependsOn": [
+ "^compile"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -115,7 +169,12 @@
]
},
"dist:cli:local": {
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
+ "dependsOn": [
+ "^compile"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -130,7 +189,12 @@
]
},
"dist:cli:prod": {
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
+ "dependsOn": [
+ "^compile"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -145,9 +209,16 @@
]
},
"dist": {
- "with": ["dist:cli", "dist:cli:dev", "dist:cli:local", "dist:cli:prod"],
+ "with": [
+ "dist:cli",
+ "dist:cli:dev",
+ "dist:cli:local",
+ "dist:cli:prod"
+ ],
"cache": false,
- "outputs": ["dist/**"],
+ "outputs": [
+ "dist/**"
+ ],
"inputs": [
"src/**",
"tests/**",
@@ -160,4 +231,4 @@
]
}
}
-}
+}
\ No newline at end of file
From b9cb8b83b1553fd6677ec96c2c07a19b084177f8 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 20:40:28 -0500
Subject: [PATCH 08/16] upgrade turbo
---
package.json | 2 +-
pnpm-lock.yaml | 63 +++++++++++++++++++++++++-------------------------
2 files changed, 33 insertions(+), 32 deletions(-)
diff --git a/package.json b/package.json
index 5ffd382e45e4..a128ddea1613 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,7 @@
"stylelint-config-standard-scss": "^5.0.0",
"tsup": "^8.5.0",
"tsx": "^4.20.3",
- "turbo": "^2.5.5",
+ "turbo": "^2.6.1",
"typescript": "5.9.3",
"vitest": "^4.0.8"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a431e7be9552..a9f8c5da7bc2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,8 +90,8 @@ importers:
specifier: ^4.20.3
version: 4.20.6
turbo:
- specifier: ^2.5.5
- version: 2.5.8
+ specifier: ^2.6.1
+ version: 2.6.1
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -7643,7 +7643,7 @@ importers:
version: 29.7.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0)
ts-jest:
specifier: ^29.1.1
- version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0))(typescript@5.9.3)
+ version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0))(typescript@5.9.3)
typescript:
specifier: ^5.2.2
version: 5.9.3
@@ -14971,38 +14971,38 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
- turbo-darwin-64@2.5.8:
- resolution: {integrity: sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ==}
+ turbo-darwin-64@2.6.1:
+ resolution: {integrity: sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ==}
cpu: [x64]
os: [darwin]
- turbo-darwin-arm64@2.5.8:
- resolution: {integrity: sha512-f1H/tQC9px7+hmXn6Kx/w8Jd/FneIUnvLlcI/7RGHunxfOkKJKvsoiNzySkoHQ8uq1pJnhJ0xNGTlYM48ZaJOQ==}
+ turbo-darwin-arm64@2.6.1:
+ resolution: {integrity: sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw==}
cpu: [arm64]
os: [darwin]
- turbo-linux-64@2.5.8:
- resolution: {integrity: sha512-hMyvc7w7yadBlZBGl/bnR6O+dJTx3XkTeyTTH4zEjERO6ChEs0SrN8jTFj1lueNXKIHh1SnALmy6VctKMGnWfw==}
+ turbo-linux-64@2.6.1:
+ resolution: {integrity: sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw==}
cpu: [x64]
os: [linux]
- turbo-linux-arm64@2.5.8:
- resolution: {integrity: sha512-LQELGa7bAqV2f+3rTMRPnj5G/OHAe2U+0N9BwsZvfMvHSUbsQ3bBMWdSQaYNicok7wOZcHjz2TkESn1hYK6xIQ==}
+ turbo-linux-arm64@2.6.1:
+ resolution: {integrity: sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA==}
cpu: [arm64]
os: [linux]
- turbo-windows-64@2.5.8:
- resolution: {integrity: sha512-3YdcaW34TrN1AWwqgYL9gUqmZsMT4T7g8Y5Azz+uwwEJW+4sgcJkIi9pYFyU4ZBSjBvkfuPZkGgfStir5BBDJQ==}
+ turbo-windows-64@2.6.1:
+ resolution: {integrity: sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ==}
cpu: [x64]
os: [win32]
- turbo-windows-arm64@2.5.8:
- resolution: {integrity: sha512-eFC5XzLmgXJfnAK3UMTmVECCwuBcORrWdewoiXBnUm934DY6QN8YowC/srhNnROMpaKaqNeRpoB5FxCww3eteQ==}
+ turbo-windows-arm64@2.6.1:
+ resolution: {integrity: sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q==}
cpu: [arm64]
os: [win32]
- turbo@2.5.8:
- resolution: {integrity: sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w==}
+ turbo@2.6.1:
+ resolution: {integrity: sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA==}
hasBin: true
type-check@0.4.0:
@@ -24189,7 +24189,7 @@ snapshots:
ts-interface-checker@0.1.13: {}
- ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0))(typescript@5.9.3):
+ ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@18.19.130)(babel-plugin-macros@3.1.0))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -24207,6 +24207,7 @@ snapshots:
'@jest/transform': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.28.5)
+ esbuild: 0.25.12
jest-util: 30.2.0
ts-morph@15.1.0:
@@ -24263,32 +24264,32 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
- turbo-darwin-64@2.5.8:
+ turbo-darwin-64@2.6.1:
optional: true
- turbo-darwin-arm64@2.5.8:
+ turbo-darwin-arm64@2.6.1:
optional: true
- turbo-linux-64@2.5.8:
+ turbo-linux-64@2.6.1:
optional: true
- turbo-linux-arm64@2.5.8:
+ turbo-linux-arm64@2.6.1:
optional: true
- turbo-windows-64@2.5.8:
+ turbo-windows-64@2.6.1:
optional: true
- turbo-windows-arm64@2.5.8:
+ turbo-windows-arm64@2.6.1:
optional: true
- turbo@2.5.8:
+ turbo@2.6.1:
optionalDependencies:
- turbo-darwin-64: 2.5.8
- turbo-darwin-arm64: 2.5.8
- turbo-linux-64: 2.5.8
- turbo-linux-arm64: 2.5.8
- turbo-windows-64: 2.5.8
- turbo-windows-arm64: 2.5.8
+ turbo-darwin-64: 2.6.1
+ turbo-darwin-arm64: 2.6.1
+ turbo-linux-64: 2.6.1
+ turbo-linux-arm64: 2.6.1
+ turbo-windows-64: 2.6.1
+ turbo-windows-arm64: 2.6.1
type-check@0.4.0:
dependencies:
From 19ac144ad4acf88c6abc529a44df900159250014 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 20:48:58 -0500
Subject: [PATCH 09/16] fmt
---
package.json | 2 +-
turbo.json | 113 +++++++++++----------------------------------------
2 files changed, 24 insertions(+), 91 deletions(-)
diff --git a/package.json b/package.json
index a128ddea1613..64867acd3289 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
"fern": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern-dev:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/dev/cli.cjs",
- "fern:build": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod --filter=@fern-api/cli --graph && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
+ "fern:build": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter @fern-api/cli dist:cli:prod:unminified && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern-dev:build": "turbo run dist:cli:dev --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
"fern-local:build": "turbo run dist:cli:local --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
diff --git a/turbo.json b/turbo.json
index 915746ba6537..c82625190cbe 100644
--- a/turbo.json
+++ b/turbo.json
@@ -16,69 +16,29 @@
"cache": false
},
"compile": {
- "dependsOn": [
- "^compile"
- ],
- "outputs": [
- "lib/**"
- ],
- "inputs": [
- "src/**",
- "tests/**",
- "package.json",
- "tsconfig.json",
- "$TURBO_ROOT$/packages/configs/**"
- ]
+ "dependsOn": ["^compile"],
+ "outputs": ["lib/**"],
+ "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json", "$TURBO_ROOT$/packages/configs/**"]
},
"compile:debug": {
- "dependsOn": [
- "^compile:debug"
- ],
- "outputs": [
- "lib/**"
- ],
- "inputs": [
- "src/**",
- "tests/**",
- "package.json",
- "tsconfig.json",
- "$TURBO_ROOT$/packages/configs/**"
- ]
+ "dependsOn": ["^compile:debug"],
+ "outputs": ["lib/**"],
+ "inputs": ["src/**", "tests/**", "package.json", "tsconfig.json", "$TURBO_ROOT$/packages/configs/**"]
},
"depcheck": {
"outputs": [],
- "inputs": [
- "src/**",
- "tests/**",
- "package.json",
- ".depcheckrc.json",
- "$TURBO_ROOT$/.depcheckrc.json"
- ]
+ "inputs": ["src/**", "tests/**", "package.json", ".depcheckrc.json", "$TURBO_ROOT$/.depcheckrc.json"]
},
"lint:eslint": {
"outputs": [],
- "inputs": [
- "src/**",
- "tests/**",
- "$TURBO_ROOT$/.eslintrc.js",
- "$TURBO_ROOT$/.eslintignore",
- "tsconfig.json"
- ]
+ "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
},
"lint:eslint:fix": {
"outputs": [],
- "inputs": [
- "src/**",
- "tests/**",
- "$TURBO_ROOT$/.eslintrc.js",
- "$TURBO_ROOT$/.eslintignore",
- "tsconfig.json"
- ]
+ "inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
},
"test": {
- "dependsOn": [
- "^compile"
- ],
+ "dependsOn": ["^compile"],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -94,9 +54,7 @@
]
},
"test:debug": {
- "dependsOn": [
- "^compile:debug"
- ],
+ "dependsOn": ["^compile:debug"],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -112,9 +70,7 @@
]
},
"test:update": {
- "dependsOn": [
- "^compile"
- ],
+ "dependsOn": ["^compile"],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -130,12 +86,8 @@
]
},
"dist:cli": {
- "outputs": [
- "dist/**"
- ],
- "dependsOn": [
- "^compile"
- ],
+ "outputs": ["dist/**"],
+ "dependsOn": ["^compile"],
"inputs": [
"src/**",
"tests/**",
@@ -149,12 +101,8 @@
]
},
"dist:cli:dev": {
- "outputs": [
- "dist/**"
- ],
- "dependsOn": [
- "^compile"
- ],
+ "outputs": ["dist/**"],
+ "dependsOn": ["^compile"],
"inputs": [
"src/**",
"tests/**",
@@ -169,12 +117,8 @@
]
},
"dist:cli:local": {
- "outputs": [
- "dist/**"
- ],
- "dependsOn": [
- "^compile"
- ],
+ "outputs": ["dist/**"],
+ "dependsOn": ["^compile"],
"inputs": [
"src/**",
"tests/**",
@@ -189,12 +133,8 @@
]
},
"dist:cli:prod": {
- "outputs": [
- "dist/**"
- ],
- "dependsOn": [
- "^compile"
- ],
+ "outputs": ["dist/**"],
+ "dependsOn": ["^compile"],
"inputs": [
"src/**",
"tests/**",
@@ -209,16 +149,9 @@
]
},
"dist": {
- "with": [
- "dist:cli",
- "dist:cli:dev",
- "dist:cli:local",
- "dist:cli:prod"
- ],
+ "with": ["dist:cli", "dist:cli:dev", "dist:cli:local", "dist:cli:prod"],
"cache": false,
- "outputs": [
- "dist/**"
- ],
+ "outputs": ["dist/**"],
"inputs": [
"src/**",
"tests/**",
@@ -231,4 +164,4 @@
]
}
}
-}
\ No newline at end of file
+}
From 63f755212800afa8cab50c289011387a4840755d Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 20:49:16 -0500
Subject: [PATCH 10/16] fmt
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 64867acd3289..af36c4f16970 100644
--- a/package.json
+++ b/package.json
@@ -143,4 +143,4 @@
"dependencies": {
"js-yaml": "^4.1.1"
}
-}
\ No newline at end of file
+}
From dbd9ae5ea32cc9099347a10543a8ee2ec03d2d0e Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Tue, 25 Nov 2025 20:58:15 -0500
Subject: [PATCH 11/16] chore: update test:ete and test:ete:update scripts for
consistency
---
package.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index af36c4f16970..0a0fd9a10af9 100644
--- a/package.json
+++ b/package.json
@@ -21,8 +21,8 @@
"test:debug": "turbo run test:debug --filter=!@fern-api/ete-tests",
"test:update": "turbo run test:update --filter=!@fern-api/ete-tests",
"test:update:dockerless": "turbo run test:update --filter=!@fern-api/ete-tests --filter=!@fern-api/docker-utils",
- "test:ete": "pnpm --filter @fern-api/cli dist:cli:dev && pnpm --filter @fern-api/seed-cli dist:cli && pnpm --filter @fern-api/ete-tests test",
- "test:ete:update": "pnpm --filter @fern-api/cli dist:cli:dev && pnpm --filter @fern-api/seed-cli dist:cli && pnpm --filter @fern-api/ete-tests test -- -u",
+ "test:ete": "pnpm fern-dev:build && pnpm seed:build && pnpm --filter @fern-api/ete-tests test",
+ "test:ete:update": "pnpm fern-dev:build && pnpm seed:build && pnpm --filter @fern-api/ete-tests test -- -u",
"lint:biome": "biome lint --error-on-warnings",
"lint:style": "stylelint 'packages/**/src/**/*.scss' --allow-empty-input --max-warnings 0",
"lint:style:fix": "pnpm lint:style --fix",
@@ -143,4 +143,4 @@
"dependencies": {
"js-yaml": "^4.1.1"
}
-}
+}
\ No newline at end of file
From 394fedf7463a29ed25c08ccf065926a0d2007b6e Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Wed, 26 Nov 2025 00:08:57 -0500
Subject: [PATCH 12/16] Refactor build scripts to use turbo
---
generators/csharp/model/build.mjs | 4 +--
generators/csharp/model/package.json | 4 +--
generators/csharp/model/turbo.jsonc | 20 ++++++++++++++
generators/csharp/sdk/build.mjs | 4 +--
generators/csharp/sdk/package.json | 4 +--
generators/csharp/sdk/turbo.jsonc | 20 ++++++++++++++
generators/go-v2/model/build.mjs | 4 +--
generators/go-v2/model/package.json | 4 +--
generators/go-v2/model/turbo.jsonc | 20 ++++++++++++++
generators/go-v2/sdk/build.mjs | 4 +--
generators/go-v2/sdk/package.json | 4 +--
generators/go-v2/sdk/turbo.jsonc | 20 ++++++++++++++
generators/java-v2/sdk/build.mjs | 2 +-
generators/java-v2/sdk/package.json | 4 +--
generators/openapi/build.mjs | 2 +-
generators/openapi/package.json | 4 +--
generators/php/model/build.mjs | 4 +--
generators/php/model/package.json | 4 +--
generators/php/model/turbo.jsonc | 20 ++++++++++++++
generators/php/sdk/build.mjs | 4 +--
generators/php/sdk/package.json | 4 +--
generators/php/sdk/turbo.jsonc | 20 ++++++++++++++
generators/postman/build.mjs | 2 +-
generators/postman/package.json | 4 +--
generators/python-v2/fastapi/build.mjs | 2 +-
generators/python-v2/fastapi/package.json | 4 +--
generators/python-v2/pydantic-model/build.mjs | 2 +-
.../python-v2/pydantic-model/package.json | 4 +--
generators/python-v2/sdk/build.mjs | 2 +-
generators/python-v2/sdk/package.json | 4 +--
generators/ruby-v2/model/Dockerfile | 23 ++++++++++++++--
.../ruby-v2/model/Dockerfile.dockerignore | 26 +++----------------
generators/ruby-v2/model/build.mjs | 2 +-
generators/ruby-v2/model/package.json | 4 +--
generators/ruby-v2/sdk/build.mjs | 4 +--
generators/ruby-v2/sdk/package.json | 4 +--
generators/ruby-v2/sdk/turbo.jsonc | 20 ++++++++++++++
generators/ruby/model/build.mjs | 2 +-
generators/ruby/model/package.json | 4 +--
generators/ruby/sdk/build.mjs | 2 +-
generators/rust/model/build.mjs | 4 +--
generators/rust/model/package.json | 4 +--
generators/rust/model/turbo.jsonc | 20 ++++++++++++++
generators/rust/sdk/build.mjs | 4 +--
generators/rust/sdk/package.json | 4 +--
generators/swift/model/build.mjs | 4 +--
generators/swift/model/package.json | 4 +--
generators/swift/model/turbo.jsonc | 20 ++++++++++++++
generators/swift/sdk/build.mjs | 5 ++--
generators/swift/sdk/package.json | 4 +--
generators/swift/sdk/turbo.jsonc | 20 ++++++++++++++
generators/typescript-mcp/model/build.mjs | 5 ++--
generators/typescript-mcp/model/package.json | 4 +--
generators/typescript-mcp/model/turbo.jsonc | 20 ++++++++++++++
generators/typescript-mcp/server/build.mjs | 4 +--
generators/typescript-mcp/server/package.json | 4 +--
generators/typescript-mcp/server/turbo.jsonc | 20 ++++++++++++++
generators/typescript/express/cli/build.mjs | 4 +--
.../typescript/express/cli/package.json | 4 +--
generators/typescript/sdk/cli/build.mjs | 4 +--
generators/typescript/sdk/cli/package.json | 4 +--
generators/typescript/sdk/cli/turbo.jsonc | 22 ++++++++++++++++
package.json | 2 +-
packages/configs/build-utils.mjs | 8 +++---
seed/csharp-model/seed.yml | 8 +++---
seed/csharp-sdk/seed.yml | 8 +++---
seed/fern-cli/seed.yml | 15 ++++++-----
seed/go-fiber/seed.yml | 6 ++---
seed/go-model/seed.yml | 6 ++---
seed/go-sdk/seed.yml | 6 ++---
seed/java-sdk/seed.yml | 6 ++---
seed/openapi/seed.yml | 4 +--
seed/php-model/seed.yml | 8 +++---
seed/php-sdk/seed.yml | 8 +++---
seed/postman/seed.yml | 4 +--
seed/pydantic-v2/seed.yml | 2 +-
seed/python-sdk/seed.yml | 4 +--
seed/ruby-model/seed.yml | 4 +--
seed/ruby-sdk-v2/seed.yml | 6 ++---
seed/ruby-sdk/seed.yml | 4 +--
seed/rust-model/seed.yml | 6 ++---
seed/rust-sdk/seed.yml | 6 ++---
seed/swift-sdk/seed.yml | 8 +++---
seed/ts-express/seed.yml | 8 +++---
seed/ts-mcp/seed.yml | 4 +--
seed/ts-sdk/seed.yml | 8 +++---
turbo.json | 19 ++++++++++++++
87 files changed, 467 insertions(+), 184 deletions(-)
create mode 100644 generators/csharp/model/turbo.jsonc
create mode 100644 generators/csharp/sdk/turbo.jsonc
create mode 100644 generators/go-v2/model/turbo.jsonc
create mode 100644 generators/go-v2/sdk/turbo.jsonc
create mode 100644 generators/php/model/turbo.jsonc
create mode 100644 generators/php/sdk/turbo.jsonc
create mode 100644 generators/ruby-v2/sdk/turbo.jsonc
create mode 100644 generators/rust/model/turbo.jsonc
create mode 100644 generators/swift/model/turbo.jsonc
create mode 100644 generators/swift/sdk/turbo.jsonc
create mode 100644 generators/typescript-mcp/model/turbo.jsonc
create mode 100644 generators/typescript-mcp/server/turbo.jsonc
create mode 100644 generators/typescript/sdk/cli/turbo.jsonc
diff --git a/generators/csharp/model/build.mjs b/generators/csharp/model/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/csharp/model/build.mjs
+++ b/generators/csharp/model/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/csharp/model/package.json b/generators/csharp/model/package.json
index b45c071483e0..78976e83aa4e 100644
--- a/generators/csharp/model/package.json
+++ b/generators/csharp/model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-csharp-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-csharp-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-csharp-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-csharp-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/csharp/model/turbo.jsonc b/generators/csharp/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/csharp/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/csharp/sdk/build.mjs b/generators/csharp/sdk/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/csharp/sdk/build.mjs
+++ b/generators/csharp/sdk/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/csharp/sdk/package.json b/generators/csharp/sdk/package.json
index 628d3ad07fb6..88daaa4d72a3 100644
--- a/generators/csharp/sdk/package.json
+++ b/generators/csharp/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-csharp-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-csharp-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-csharp-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-csharp-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/csharp/sdk/turbo.jsonc b/generators/csharp/sdk/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/csharp/sdk/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/go-v2/model/build.mjs b/generators/go-v2/model/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/go-v2/model/build.mjs
+++ b/generators/go-v2/model/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/go-v2/model/package.json b/generators/go-v2/model/package.json
index 62ca8c65ba71..d4b07d645e63 100644
--- a/generators/go-v2/model/package.json
+++ b/generators/go-v2/model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-go-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-go-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-go-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-go-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/go-v2/model/turbo.jsonc b/generators/go-v2/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/go-v2/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/go-v2/sdk/build.mjs b/generators/go-v2/sdk/build.mjs
index 4126ef44c1f0..c5df3758b579 100644
--- a/generators/go-v2/sdk/build.mjs
+++ b/generators/go-v2/sdk/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: { from: '../base/src/asIs', to: './dist/asIs' }
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/go-v2/sdk/package.json b/generators/go-v2/sdk/package.json
index 8e65f39fae75..2ea93e940f2b 100644
--- a/generators/go-v2/sdk/package.json
+++ b/generators/go-v2/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/go-v2/sdk/turbo.jsonc b/generators/go-v2/sdk/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/go-v2/sdk/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/java-v2/sdk/build.mjs b/generators/java-v2/sdk/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/java-v2/sdk/build.mjs
+++ b/generators/java-v2/sdk/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/java-v2/sdk/package.json b/generators/java-v2/sdk/package.json
index db418bb0bce5..13e4b3955df8 100644
--- a/generators/java-v2/sdk/package.json
+++ b/generators/java-v2/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-java-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-java-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-java-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-java-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/openapi/build.mjs b/generators/openapi/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/openapi/build.mjs
+++ b/generators/openapi/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/openapi/package.json b/generators/openapi/package.json
index f8a7dfb2a3a1..4f01f8967aee 100644
--- a/generators/openapi/package.json
+++ b/generators/openapi/package.json
@@ -28,13 +28,13 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-openapi:latest .",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-openapi:latest .",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-openapi:${0} .",
"format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-openapi:latest .",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-openapi:latest .",
"podmanTagVersion": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-openapi:${0} .",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
diff --git a/generators/php/model/build.mjs b/generators/php/model/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/php/model/build.mjs
+++ b/generators/php/model/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/php/model/package.json b/generators/php/model/package.json
index 25630ad0981d..8f0453f83ced 100644
--- a/generators/php/model/package.json
+++ b/generators/php/model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-php-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-php-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-php-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-php-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/php/model/turbo.jsonc b/generators/php/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/php/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/php/sdk/build.mjs b/generators/php/sdk/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/php/sdk/build.mjs
+++ b/generators/php/sdk/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/php/sdk/package.json b/generators/php/sdk/package.json
index 9296f5418e82..125f53cd6155 100644
--- a/generators/php/sdk/package.json
+++ b/generators/php/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-php-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-php-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-php-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-php-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/php/sdk/turbo.jsonc b/generators/php/sdk/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/php/sdk/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/postman/build.mjs b/generators/postman/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/postman/build.mjs
+++ b/generators/postman/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/postman/package.json b/generators/postman/package.json
index b58ac48e01d1..3c76bf0d1bd4 100644
--- a/generators/postman/package.json
+++ b/generators/postman/package.json
@@ -28,13 +28,13 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-postman:latest .",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-postman:latest .",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-postman:${0} .",
"format": "prettier --write --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-postman:latest .",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-postman:latest .",
"podmanTagVersion": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-postman:${0} .",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
diff --git a/generators/python-v2/fastapi/build.mjs b/generators/python-v2/fastapi/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/python-v2/fastapi/build.mjs
+++ b/generators/python-v2/fastapi/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/python-v2/fastapi/package.json b/generators/python-v2/fastapi/package.json
index ef9d1610bc8d..94c84de96b39 100644
--- a/generators/python-v2/fastapi/package.json
+++ b/generators/python-v2/fastapi/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-fastapi-server-v2:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-fastapi-server-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-fastapi-server-v2:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-fastapi-server-v2:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/python-v2/pydantic-model/build.mjs b/generators/python-v2/pydantic-model/build.mjs
index 486e05fa6fc7..a21b5cd4cabd 100644
--- a/generators/python-v2/pydantic-model/build.mjs
+++ b/generators/python-v2/pydantic-model/build.mjs
@@ -1,6 +1,6 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
+await buildGenerator(getDirname(import.meta.url), {
tsupOptions: {
external: ['@wasm-fmt/ruff_fmt']
}
diff --git a/generators/python-v2/pydantic-model/package.json b/generators/python-v2/pydantic-model/package.json
index 2585cdebfcdf..7ec6bdbdac46 100644
--- a/generators/python-v2/pydantic-model/package.json
+++ b/generators/python-v2/pydantic-model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-pydantic-model-v2:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-pydantic-model-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-pydantic-model-v2:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-pydantic-model-v2:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/python-v2/sdk/build.mjs b/generators/python-v2/sdk/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/python-v2/sdk/build.mjs
+++ b/generators/python-v2/sdk/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/python-v2/sdk/package.json b/generators/python-v2/sdk/package.json
index 9296411a5c53..31647ecf1eb4 100644
--- a/generators/python-v2/sdk/package.json
+++ b/generators/python-v2/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-python-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-python-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-python-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-python-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/ruby-v2/model/Dockerfile b/generators/ruby-v2/model/Dockerfile
index fbbf1133a6d1..1fadf102cad2 100644
--- a/generators/ruby-v2/model/Dockerfile
+++ b/generators/ruby-v2/model/Dockerfile
@@ -1,2 +1,21 @@
-*
-!generators/ruby-v2/model/dist
\ No newline at end of file
+FROM ruby:3.3-alpine3.20
+
+RUN apk --no-cache add \
+ bash \
+ build-base \
+ curl \
+ git \
+ zip \
+ nodejs \
+ npm
+
+RUN gem install rubocop rubocop-minitest
+
+RUN git config --global user.name "fern" && git config --global user.email "hey@buildwithfern.com"
+
+RUN npm install -f -g @fern-api/generator-cli@0.2.0
+
+COPY generators/ruby-v2/sdk/features.yml /assets/features.yml
+COPY generators/ruby-v2/sdk/dist /dist
+
+ENTRYPOINT ["node", "--enable-source-maps", "/dist/cli.cjs"]
\ No newline at end of file
diff --git a/generators/ruby-v2/model/Dockerfile.dockerignore b/generators/ruby-v2/model/Dockerfile.dockerignore
index 81767894be33..e163e4505f81 100644
--- a/generators/ruby-v2/model/Dockerfile.dockerignore
+++ b/generators/ruby-v2/model/Dockerfile.dockerignore
@@ -1,23 +1,3 @@
-FROM node:20.18-alpine3.20 AS node
-FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21
-
-ENV YARN_CACHE_FOLDER=/.yarn
-ENV PATH="$PATH:/root/.dotnet/tools"
-ENV DOTNET_NOLOGO=1
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
-
-RUN apk --no-cache add bash curl git zip
-RUN git config --global user.name "fern" && git config --global user.email "hey@buildwithfern.com"
-
-# Copy over node contents to be able to run the compiled CLI
-COPY --from=node /usr/local/bin/node /usr/local/bin/
-COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
-RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
- && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
-
-# RUN dotnet tool install -g ruby-formatter --version "1.*"
-
-COPY generators/ruby-v2/model/dist /dist
-
-ENTRYPOINT ["node", "/dist/cli.cjs"]
\ No newline at end of file
+*
+!generators/ruby-v2/sdk/features.yml
+!generators/ruby-v2/sdk/dist
\ No newline at end of file
diff --git a/generators/ruby-v2/model/build.mjs b/generators/ruby-v2/model/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/ruby-v2/model/build.mjs
+++ b/generators/ruby-v2/model/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/ruby-v2/model/package.json b/generators/ruby-v2/model/package.json
index 2bcb8990f98f..f9aef18d9420 100644
--- a/generators/ruby-v2/model/package.json
+++ b/generators/ruby-v2/model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/ruby-v2/sdk/build.mjs b/generators/ruby-v2/sdk/build.mjs
index 4126ef44c1f0..c5df3758b579 100644
--- a/generators/ruby-v2/sdk/build.mjs
+++ b/generators/ruby-v2/sdk/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: { from: '../base/src/asIs', to: './dist/asIs' }
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/ruby-v2/sdk/package.json b/generators/ruby-v2/sdk/package.json
index 0aeb76824d6d..726521acfc7d 100644
--- a/generators/ruby-v2/sdk/package.json
+++ b/generators/ruby-v2/sdk/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-sdk-v2:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-ruby-sdk-v2:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-ruby-sdk-v2:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-ruby-sdk-v2:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/ruby-v2/sdk/turbo.jsonc b/generators/ruby-v2/sdk/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/ruby-v2/sdk/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/ruby/model/build.mjs b/generators/ruby/model/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/ruby/model/build.mjs
+++ b/generators/ruby/model/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/ruby/model/package.json b/generators/ruby/model/package.json
index e565cd420ce5..3ecb7865180b 100644
--- a/generators/ruby/model/package.json
+++ b/generators/ruby/model/package.json
@@ -27,13 +27,13 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"dockerTagVersion": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-ruby-model:${0} ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-ruby-model:latest ../../..",
"podmanTagVersion": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-ruby-model:${0} ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
diff --git a/generators/ruby/sdk/build.mjs b/generators/ruby/sdk/build.mjs
index f062576559e0..2a1865dff73e 100644
--- a/generators/ruby/sdk/build.mjs
+++ b/generators/ruby/sdk/build.mjs
@@ -1,3 +1,3 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url));
+await buildGenerator(getDirname(import.meta.url));
diff --git a/generators/rust/model/build.mjs b/generators/rust/model/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/rust/model/build.mjs
+++ b/generators/rust/model/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/rust/model/package.json b/generators/rust/model/package.json
index 82e5696d62e5..be9c3a4c8d13 100644
--- a/generators/rust/model/package.json
+++ b/generators/rust/model/package.json
@@ -27,12 +27,12 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-rust-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-rust-model:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-rust-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-rust-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/rust/model/turbo.jsonc b/generators/rust/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/rust/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/rust/sdk/build.mjs b/generators/rust/sdk/build.mjs
index 7750e47703ad..2fc77932adce 100644
--- a/generators/rust/sdk/build.mjs
+++ b/generators/rust/sdk/build.mjs
@@ -1,10 +1,10 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
+await buildGenerator(getDirname(import.meta.url), {
tsupOptions: {
noExternal: [/@fern-api\/.*/, /dedent/],
},
- copyFrom: [
+ copy: [
{ from: './features.yml', to: './dist/assets/features.yml' },
{ from: '../base/src/asIs', to: './dist/asIs' },
],
diff --git a/generators/rust/sdk/package.json b/generators/rust/sdk/package.json
index 9c8a0518cd00..fa3a68050e43 100644
--- a/generators/rust/sdk/package.json
+++ b/generators/rust/sdk/package.json
@@ -26,12 +26,12 @@
"compile": "tsc --build",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-rust-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-rust-sdk:latest ../../..",
"format": "prettier --write --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../../shared/.prettierignore \"**\"",
"lint:eslint": "eslint --max-warnings 0 . --ignore-pattern=../../../.eslintignore",
"lint:eslint:fix": "yarn lint:eslint --fix",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-rust-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-rust-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:update": "vitest --passWithNoTests --run -u"
diff --git a/generators/swift/model/build.mjs b/generators/swift/model/build.mjs
index 0db93d3ff435..107d8359e863 100644
--- a/generators/swift/model/build.mjs
+++ b/generators/swift/model/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: ['../base/src/asIs', '../base/src/template']
+await buildGenerator(getDirname(import.meta.url), {
+ copy: [{ from: '../base/src/asIs', to: './dist/asIs' }, { from: '../base/src/template', to: './dist/template' }]
});
diff --git a/generators/swift/model/package.json b/generators/swift/model/package.json
index 4f0a489df302..642edaa0acde 100644
--- a/generators/swift/model/package.json
+++ b/generators/swift/model/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-swift-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/swift/model/turbo.jsonc b/generators/swift/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/swift/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/swift/sdk/build.mjs b/generators/swift/sdk/build.mjs
index 0db93d3ff435..b821019d1244 100644
--- a/generators/swift/sdk/build.mjs
+++ b/generators/swift/sdk/build.mjs
@@ -1,5 +1,6 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: ['../base/src/asIs', '../base/src/template']
+await buildGenerator(getDirname(import.meta.url), {
+ copy: [
+ { from: '../base/src/asIs', to: './dist/asIs' }, { from: '../base/src/template', to: './dist/template' }]
});
diff --git a/generators/swift/sdk/package.json b/generators/swift/sdk/package.json
index fa392739a8b6..258592d01229 100644
--- a/generators/swift/sdk/package.json
+++ b/generators/swift/sdk/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-swift-sdk:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && yarn npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/swift/sdk/turbo.jsonc b/generators/swift/sdk/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/swift/sdk/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/typescript-mcp/model/build.mjs b/generators/typescript-mcp/model/build.mjs
index 9556cd96c667..286bbee80b30 100644
--- a/generators/typescript-mcp/model/build.mjs
+++ b/generators/typescript-mcp/model/build.mjs
@@ -1,5 +1,6 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/typescript-mcp/model/package.json b/generators/typescript-mcp/model/package.json
index f5faee288b12..d396b03fde7f 100644
--- a/generators/typescript-mcp/model/package.json
+++ b/generators/typescript-mcp/model/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-model:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/typescript-mcp/model/turbo.jsonc b/generators/typescript-mcp/model/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/typescript-mcp/model/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/typescript-mcp/server/build.mjs b/generators/typescript-mcp/server/build.mjs
index 9556cd96c667..c5df3758b579 100644
--- a/generators/typescript-mcp/server/build.mjs
+++ b/generators/typescript-mcp/server/build.mjs
@@ -1,5 +1,5 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: '../base/src/asIs'
+await buildGenerator(getDirname(import.meta.url), {
+ copy: { from: '../base/src/asIs', to: './dist/asIs' }
});
diff --git a/generators/typescript-mcp/server/package.json b/generators/typescript-mcp/server/package.json
index 930bb1583693..bf25b1184035 100644
--- a/generators/typescript-mcp/server/package.json
+++ b/generators/typescript-mcp/server/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-typescript-mcp-server:latest ../../..",
"publish:cli": "pnpm dist:cli && cd dist && npm publish",
"test": "vitest --passWithNoTests --run",
"test:debug": "pnpm run test --inspect --no-file-parallelism",
diff --git a/generators/typescript-mcp/server/turbo.jsonc b/generators/typescript-mcp/server/turbo.jsonc
new file mode 100644
index 000000000000..1b43312be3f1
--- /dev/null
+++ b/generators/typescript-mcp/server/turbo.jsonc
@@ -0,0 +1,20 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../base/src/asIs/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/generators/typescript/express/cli/build.mjs b/generators/typescript/express/cli/build.mjs
index 6dd17ce89722..ec5c9d0504d5 100644
--- a/generators/typescript/express/cli/build.mjs
+++ b/generators/typescript/express/cli/build.mjs
@@ -1,7 +1,7 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: [
+await buildGenerator(getDirname(import.meta.url), {
+ copy: [
{ from: '../../asIs/', to: './dist/assets/asIs' },
{ from: '../../utils/core-utilities/', to: './dist/assets/core-utilities' },
],
diff --git a/generators/typescript/express/cli/package.json b/generators/typescript/express/cli/package.json
index 198fc0d67ed4..e8baff373b2c 100644
--- a/generators/typescript/express/cli/package.json
+++ b/generators/typescript/express/cli/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../.."
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-typescript-express:latest ../../../.."
},
"devDependencies": {
"@fern-api/configs": "workspace:*",
diff --git a/generators/typescript/sdk/cli/build.mjs b/generators/typescript/sdk/cli/build.mjs
index 8455ff60203e..2e49245ecf97 100644
--- a/generators/typescript/sdk/cli/build.mjs
+++ b/generators/typescript/sdk/cli/build.mjs
@@ -1,7 +1,7 @@
import { buildGenerator, getDirname } from '@fern-api/configs/build-utils.mjs';
-buildGenerator(getDirname(import.meta.url), {
- copyFrom: [
+await buildGenerator(getDirname(import.meta.url), {
+ copy: [
{ from: '../features.yml', to: './dist/assets/features.yml' },
{ from: '../../asIs/readme/binary-response-addendum.md', to: './dist/assets/readme/binary-response-addendum.md' },
{ from: '../../asIs/', to: './dist/assets/asIs' },
diff --git a/generators/typescript/sdk/cli/package.json b/generators/typescript/sdk/cli/package.json
index 91074645e735..052c57e20cef 100644
--- a/generators/typescript/sdk/cli/package.json
+++ b/generators/typescript/sdk/cli/package.json
@@ -27,8 +27,8 @@
"compile:debug": "tsc --build --sourceMap",
"depcheck": "depcheck",
"dist:cli": "node build.mjs",
- "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../..",
- "podmanTagLatest": "pnpm dist:cli && podman build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../.."
+ "dockerTagLatest": "docker build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../..",
+ "podmanTagLatest": "podman build -f ./Dockerfile -t fernapi/fern-typescript-node-sdk:latest -t fernapi/fern-typescript-sdk:latest ../../../.."
},
"devDependencies": {
"@fern-api/base-generator": "workspace:*",
diff --git a/generators/typescript/sdk/cli/turbo.jsonc b/generators/typescript/sdk/cli/turbo.jsonc
new file mode 100644
index 000000000000..25fe5c8387ae
--- /dev/null
+++ b/generators/typescript/sdk/cli/turbo.jsonc
@@ -0,0 +1,22 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli": {
+ "inputs": [
+ "../features.yml",
+ "../../asIs/**",
+ "../../utils/core-utilities/**",
+ /** same as global task **/
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build.mjs",
+ "$TURBO_ROOT$/packages/configs/**",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json"
+ ]
+ }
+ }
+}
diff --git a/package.json b/package.json
index 0a0fd9a10af9..9878fe6c7a87 100644
--- a/package.json
+++ b/package.json
@@ -143,4 +143,4 @@
"dependencies": {
"js-yaml": "^4.1.1"
}
-}
\ No newline at end of file
+}
diff --git a/packages/configs/build-utils.mjs b/packages/configs/build-utils.mjs
index e1f0c38df232..7d69dbf109d5 100644
--- a/packages/configs/build-utils.mjs
+++ b/packages/configs/build-utils.mjs
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'url';
* @param {Object} options - Build options
* @param {string} [options.entry='src/cli.ts'] - Entry point for tsup
* @param {Object} [options.tsupOptions={}] - Additional tsup configuration options to merge
- * @param {string|string[]|Object|Object[]|null} [options.copyFrom=null] - Files/folders to copy after build
+ * @param {string|string[]|Object|Object[]|null} [options.copy=null] - Files/folders to copy after build
* Can be:
* - string: '../base/src/asIs' - copies to dist/
* - array of strings: ['../base/src/asIs', '../base/src/template'] - copies each to dist/
@@ -20,7 +20,7 @@ export async function buildGenerator(dirname, options = {}) {
const {
entry = 'src/cli.ts',
tsupOptions = {},
- copyFrom = null
+ copy = null
} = options;
// Build with tsup (merge default options with custom ones)
@@ -38,8 +38,8 @@ export async function buildGenerator(dirname, options = {}) {
});
// Copy additional files if needed
- if (copyFrom) {
- const copyOperations = Array.isArray(copyFrom) ? copyFrom : [copyFrom];
+ if (copy) {
+ const copyOperations = Array.isArray(copy) ? copy : [copy];
for (const copyOp of copyOperations) {
if (typeof copyOp === 'string') {
diff --git a/seed/csharp-model/seed.yml b/seed/csharp-model/seed.yml
index aff7a36d6b70..fb63896bb48f 100644
--- a/seed/csharp-model/seed.yml
+++ b/seed/csharp-model/seed.yml
@@ -11,7 +11,7 @@ buildScripts:
publish:
preBuildCommands:
- ./.github/actions/install
- - pnpm --filter @fern-api/fern-csharp-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/fern-csharp-model
docker:
file: ./generators/csharp/model/Dockerfile
image: fernapi/fern-csharp-model
@@ -19,14 +19,14 @@ publish:
test:
docker:
image: fernapi/fern-csharp-model:latest
- command: pnpm --filter @fern-api/fern-csharp-model dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/fern-csharp-model
podman:
image: fernapi/fern-csharp-model:latest
- command: pnpm --filter @fern-api/fern-csharp-model podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/fern-csharp-model
local:
workingDirectory: generators/csharp
buildCommand:
- - pnpm --filter @fern-api/fern-csharp-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/fern-csharp-model
runCommand: node --enable-source-maps model/dist/cli.cjs {CONFIG_PATH}
language: csharp
diff --git a/seed/csharp-sdk/seed.yml b/seed/csharp-sdk/seed.yml
index fed7ee53e2b6..44f289dce3a9 100644
--- a/seed/csharp-sdk/seed.yml
+++ b/seed/csharp-sdk/seed.yml
@@ -16,7 +16,7 @@ buildScripts:
publish:
preBuildCommands:
- - pnpm --filter @fern-api/fern-csharp-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/fern-csharp-sdk
docker:
file: ./generators/csharp/sdk/Dockerfile
image: fernapi/fern-csharp-sdk
@@ -24,14 +24,14 @@ publish:
test:
docker:
image: fernapi/fern-csharp-sdk:latest
- command: pnpm --filter @fern-api/fern-csharp-sdk dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/fern-csharp-sdk
podman:
image: fernapi/fern-csharp-sdk:latest
- command: pnpm --filter @fern-api/fern-csharp-sdk podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/fern-csharp-sdk
local:
workingDirectory: generators/csharp
buildCommand:
- - pnpm --filter @fern-api/fern-csharp-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/fern-csharp-sdk
runCommand: node --enable-source-maps sdk/dist/cli.cjs {CONFIG_PATH}
language: csharp
diff --git a/seed/fern-cli/seed.yml b/seed/fern-cli/seed.yml
index 5f015675b9ae..c4f38005c30e 100644
--- a/seed/fern-cli/seed.yml
+++ b/seed/fern-cli/seed.yml
@@ -5,8 +5,8 @@ publishGa:
command:
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- pnpm install
- - pnpm --filter @fern-api/cli compile
- - pnpm --filter @fern-api/cli dist:cli:prod $VERSION
+ - pnpm turbo run compile --filter @fern-api/cli
+ - pnpm turbo run dist:cli:prod --filter @fern-api/cli -- $VERSION
- pnpm --filter @fern-api/cli publish:cli:prod --tag latest
publishRc:
workingDirectory: packages/cli/cli
@@ -14,13 +14,14 @@ publishRc:
command:
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- pnpm install
- - pnpm --filter @fern-api/cli compile
- - pnpm --filter @fern-api/cli dist:cli:prod $VERSION
+ - pnpm turbo run compile --filter @fern-api/cli
+ - pnpm turbo run dist:cli:prod --filter @fern-api/cli -- $VERSION
- pnpm --filter @fern-api/cli publish:cli:prod --tag prerelease
publishDev:
workingDirectory: packages/cli/cli
versionSubstitution: $VERSION
command:
- - pnpm --filter @fern-api/cli compile
- - pnpm --filter @fern-api/cli dist:cli:dev $VERSION
- - pnpm --filter @fern-api/cli publish:cli:dev --access restricted
+ - pnpm install
+ - pnpm turbo run compile --filter @fern-api/cli
+ - pnpm turbo run dist:cli:dev --filter @fern-api/cli -- $VERSION
+ - pnpm --filter @fern-api/cli publish:cli:dev --access restricted
\ No newline at end of file
diff --git a/seed/go-fiber/seed.yml b/seed/go-fiber/seed.yml
index 69b3dd293b9c..2a3b88c98848 100644
--- a/seed/go-fiber/seed.yml
+++ b/seed/go-fiber/seed.yml
@@ -15,7 +15,7 @@ publish:
workingDirectory: generators/go
preBuildCommands:
- go build ./...
- - pnpm --filter @fern-api/go-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/go-sdk
docker:
file: ./generators/go/fiber/Dockerfile
image: fernapi/fern-go-fiber
@@ -23,10 +23,10 @@ publish:
test:
docker:
image: fernapi/fern-go-fiber:latest
- command: pnpm --filter @fern-api/go-sdk dist:cli && docker build -f ./generators/go/fiber/Dockerfile -t fernapi/fern-go-fiber:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-sdk && docker build -f ./generators/go/fiber/Dockerfile -t fernapi/fern-go-fiber:latest .
podman:
image: fernapi/fern-go-fiber:latest
- command: pnpm --filter @fern-api/go-sdk dist:cli && podman build -f ./generators/go/fiber/Dockerfile -t fernapi/fern-go-fiber:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-sdk && podman build -f ./generators/go/fiber/Dockerfile -t fernapi/fern-go-fiber:latest .
language: go
generatorType: Model
defaultOutputMode: github
diff --git a/seed/go-model/seed.yml b/seed/go-model/seed.yml
index 9ef654994996..d2a58e1767c0 100644
--- a/seed/go-model/seed.yml
+++ b/seed/go-model/seed.yml
@@ -17,7 +17,7 @@ buildScripts:
publish:
workingDirectory: generators/go-v2
preBuildCommands:
- - pnpm --filter @fern-api/go-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/go-model
docker:
file: ./generators/go/model/Dockerfile
image: fernapi/fern-go-model
@@ -26,10 +26,10 @@ publish:
test:
docker:
image: fernapi/fern-go-model:latest
- command: pnpm --filter @fern-api/go-model dist:cli && docker build -f ./generators/go/model/Dockerfile -t fernapi/fern-go-model:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-model && docker build -f ./generators/go/model/Dockerfile -t fernapi/fern-go-model:latest .
podman:
image: fernapi/fern-go-model:latest
- command: pnpm --filter @fern-api/go-model dist:cli && podman build -f ./generators/go/model/Dockerfile -t fernapi/fern-go-model:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-model && podman build -f ./generators/go/model/Dockerfile -t fernapi/fern-go-model:latest .
fixtures:
streaming:
diff --git a/seed/go-sdk/seed.yml b/seed/go-sdk/seed.yml
index 84681805bf7f..7c38569a5dff 100644
--- a/seed/go-sdk/seed.yml
+++ b/seed/go-sdk/seed.yml
@@ -19,7 +19,7 @@ publish:
workingDirectory: generators/go
preBuildCommands:
- go build ./...
- - pnpm --filter @fern-api/go-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/go-sdk
docker:
file: ./generators/go/sdk/Dockerfile
image: fernapi/fern-go-sdk
@@ -27,10 +27,10 @@ publish:
test:
docker:
image: fernapi/fern-go-sdk:latest
- command: pnpm --filter @fern-api/go-sdk dist:cli && docker build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-sdk && docker build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest .
podman:
image: fernapi/fern-go-sdk:latest
- command: pnpm --filter @fern-api/go-sdk dist:cli && podman build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/go-sdk && podman build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest .
language: go
generatorType: SDK
defaultOutputMode: github
diff --git a/seed/java-sdk/seed.yml b/seed/java-sdk/seed.yml
index 796a84ea39c8..06b004e100d3 100644
--- a/seed/java-sdk/seed.yml
+++ b/seed/java-sdk/seed.yml
@@ -12,7 +12,7 @@ publish:
workingDirectory: generators/java
preBuildCommands:
- ./gradlew :sdk:distTar
- - pnpm --filter @fern-api/java-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/java-sdk
docker:
file: ./generators/java/sdk/Dockerfile
image: fernapi/fern-java-sdk
@@ -25,7 +25,7 @@ test:
- cd generators/java
- ./gradlew :sdk:distTar
- cd ../../
- - pnpm --filter @fern-api/java-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/java-sdk
- docker build -f generators/java/sdk/Dockerfile -t fernapi/fern-java-sdk:latest .
podman:
image: fernapi/fern-java-sdk:latest
@@ -34,7 +34,7 @@ test:
- cd generators/java
- ./gradlew :sdk:distTar
- cd ../../
- - pnpm --filter @fern-api/java-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/java-sdk
- podman build -f generators/java/sdk/Dockerfile -t fernapi/fern-java-sdk:latest .
local:
workingDirectory: generators/java
diff --git a/seed/openapi/seed.yml b/seed/openapi/seed.yml
index 2cae87038119..4169f1ae89f1 100644
--- a/seed/openapi/seed.yml
+++ b/seed/openapi/seed.yml
@@ -4,7 +4,7 @@ image: fernapi/fern-openapi
changelogLocation: ../../generators/openapi/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/openapi-generator dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/openapi-generator
docker:
file: ./generators/openapi/Dockerfile
image: fernapi/fern-openapi
@@ -12,7 +12,7 @@ publish:
test:
docker:
image: fernapi/fern-openapi:latest
- command: pnpm --filter @fern-api/openapi-generator dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/openapi-generator
generatorType: Documentation
defaultOutputMode: local_files
fixtures:
diff --git a/seed/php-model/seed.yml b/seed/php-model/seed.yml
index 61a905a11db6..502e57642c42 100644
--- a/seed/php-model/seed.yml
+++ b/seed/php-model/seed.yml
@@ -4,7 +4,7 @@ image: fernapi/fern-php-model
changelogLocation: ../../generators/php/model/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/php-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/php-model
docker:
file: ./generators/php/model/Dockerfile
image: fernapi/fern-php-model
@@ -12,14 +12,14 @@ publish:
test:
docker:
image: fernapi/fern-php-model:latest
- command: pnpm --filter @fern-api/php-model dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/php-model
podman:
image: fernapi/fern-php-model:latest
- command: pnpm --filter @fern-api/php-model podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/php-model
local:
workingDirectory: generators/php
buildCommand:
- - pnpm --filter @fern-api/php-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/php-model
runCommand: node --enable-source-maps model/dist/cli.cjs {CONFIG_PATH}
language: php
diff --git a/seed/php-sdk/seed.yml b/seed/php-sdk/seed.yml
index 4a560780e7ec..5d8e88b8eb28 100644
--- a/seed/php-sdk/seed.yml
+++ b/seed/php-sdk/seed.yml
@@ -4,7 +4,7 @@ image: fernapi/fern-php-sdk
changelogLocation: ../../generators/php/sdk/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/php-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/php-sdk
docker:
file: ./generators/php/sdk/Dockerfile
image: fernapi/fern-php-sdk
@@ -12,14 +12,14 @@ publish:
test:
docker:
image: fernapi/fern-php-sdk:latest
- command: pnpm --filter @fern-api/php-sdk dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/php-sdk
podman:
image: fernapi/fern-php-sdk:latest
- command: pnpm --filter @fern-api/php-sdk podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/php-sdk
local:
workingDirectory: generators/php
buildCommand:
- - pnpm --filter @fern-api/php-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/php-sdk
runCommand: node --enable-source-maps sdk/dist/cli.cjs {CONFIG_PATH}
language: php
generatorType: SDK
diff --git a/seed/postman/seed.yml b/seed/postman/seed.yml
index 8f826d18cbbb..30f21387d0ca 100644
--- a/seed/postman/seed.yml
+++ b/seed/postman/seed.yml
@@ -4,7 +4,7 @@ image: fernapi/fern-postman
changelogLocation: ../../generators/postman/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/postman-generator dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/postman-generator
docker:
file: ./generators/postman/Dockerfile
image: fernapi/fern-postman
@@ -12,7 +12,7 @@ publish:
test:
docker:
image: fernapi/fern-postman:latest
- command: pnpm --filter @fern-api/postman-generator dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/postman-generator
fixtures:
imdb:
- customConfig: null
diff --git a/seed/pydantic-v2/seed.yml b/seed/pydantic-v2/seed.yml
index 1d40da2e0cee..1e6056848c81 100644
--- a/seed/pydantic-v2/seed.yml
+++ b/seed/pydantic-v2/seed.yml
@@ -5,7 +5,7 @@ changelogLocation: ../../generators/python/pydantic/versions.yml
test:
docker:
image: fernapi/fern-pydantic-model-v2:latest
- command: pnpm --filter @fern-api/fern-pydantic-model dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/fern-pydantic-model
language: python
generatorType: Model
diff --git a/seed/python-sdk/seed.yml b/seed/python-sdk/seed.yml
index dcfb9b37385f..16404ce6f5c3 100644
--- a/seed/python-sdk/seed.yml
+++ b/seed/python-sdk/seed.yml
@@ -15,7 +15,7 @@ buildScripts:
publish:
workingDirectory: generators/python
preBuildCommands:
- - pnpm --filter @fern-api/python-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/python-sdk
- pip install poetry
- poetry config virtualenvs.in-project true
- poetry install
@@ -26,7 +26,7 @@ publish:
test:
docker:
image: fernapi/fern-python-sdk:latest
- command: pnpm --filter @fern-api/python-sdk dist:cli && docker build -f ./generators/python/sdk/Dockerfile -t fernapi/fern-python-sdk:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/python-sdk && docker build -f ./generators/python/sdk/Dockerfile -t fernapi/fern-python-sdk:latest .
local:
workingDirectory: generators/python
buildCommand:
diff --git a/seed/ruby-model/seed.yml b/seed/ruby-model/seed.yml
index 3ba2a7228507..ff321ac618df 100644
--- a/seed/ruby-model/seed.yml
+++ b/seed/ruby-model/seed.yml
@@ -5,7 +5,7 @@ changelogLocation: ../../generators/ruby/model/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/fern-ruby-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/fern-ruby-model
docker:
file: ./generators/ruby/model/Dockerfile
image: fernapi/fern-ruby-model
@@ -13,7 +13,7 @@ publish:
test:
docker:
image: fernapi/fern-ruby-model:latest
- command: pnpm --filter @fern-api/fern-ruby-model dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/fern-ruby-model
language: ruby
generatorType: Model
defaultOutputMode: local_files
diff --git a/seed/ruby-sdk-v2/seed.yml b/seed/ruby-sdk-v2/seed.yml
index 7dea7d7991bf..2c87afbc3626 100644
--- a/seed/ruby-sdk-v2/seed.yml
+++ b/seed/ruby-sdk-v2/seed.yml
@@ -17,14 +17,14 @@ buildScripts:
test:
docker:
image: fernapi/fern-ruby-sdk-v2:latest
- command: pnpm --filter @fern-api/ruby-sdk dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/ruby-sdk
podman:
image: fernapi/fern-ruby-sdk-v2:latest
- command: pnpm --filter @fern-api/ruby-sdk podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/ruby-sdk
publish:
preBuildCommands:
- - pnpm --filter @fern-api/ruby-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/ruby-sdk
docker:
file: ./generators/ruby-v2/sdk/Dockerfile
image: fernapi/fern-ruby-sdk-v2
diff --git a/seed/ruby-sdk/seed.yml b/seed/ruby-sdk/seed.yml
index b997f43675fe..a38b710c5cf0 100644
--- a/seed/ruby-sdk/seed.yml
+++ b/seed/ruby-sdk/seed.yml
@@ -13,7 +13,7 @@ buildScripts:
publish:
preBuildCommands:
- - pnpm --filter @fern-api/ruby-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/ruby-sdk
docker:
file: ./generators/ruby-v2/sdk/Dockerfile
image: fernapi/fern-ruby-sdk
@@ -25,7 +25,7 @@ publish:
test:
docker:
image: fernapi/fern-ruby-sdk:latest
- command: pnpm --filter @fern-api/fern-ruby-sdk dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/fern-ruby-sdk
language: ruby
generatorType: SDK
defaultOutputMode: github
diff --git a/seed/rust-model/seed.yml b/seed/rust-model/seed.yml
index 9e42daf98d7f..dfbebef2b308 100644
--- a/seed/rust-model/seed.yml
+++ b/seed/rust-model/seed.yml
@@ -14,17 +14,17 @@ buildScripts:
test:
docker:
image: fernapi/fern-rust-model:latest
- command: pnpm --filter @fern-api/rust-model dist:cli && docker build -f ./generators/rust/model/Dockerfile -t fernapi/fern-rust-model:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/rust-model && docker build -f ./generators/rust/model/Dockerfile -t fernapi/fern-rust-model:latest .
local:
workingDirectory: generators/rust
buildCommand:
- - pnpm --filter @fern-api/rust-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/rust-model
runCommand: node --enable-source-maps ./model/dist/cli.cjs {CONFIG_PATH}
publish:
workingDirectory: generators/rust
preBuildCommands:
- - pnpm --filter @fern-api/rust-model dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/rust-model
docker:
file: ./generators/rust/model/Dockerfile
image: fernapi/fern-rust-model
diff --git a/seed/rust-sdk/seed.yml b/seed/rust-sdk/seed.yml
index 21c23c272804..257ec6fa8d68 100644
--- a/seed/rust-sdk/seed.yml
+++ b/seed/rust-sdk/seed.yml
@@ -14,7 +14,7 @@ buildScripts:
publish:
workingDirectory: generators/rust
preBuildCommands:
- - pnpm --filter @fern-api/rust-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/rust-sdk
docker:
file: ./generators/rust/sdk/Dockerfile
image: fernapi/fern-rust-sdk
@@ -23,11 +23,11 @@ publish:
test:
docker:
image: fernapi/fern-rust-sdk:latest
- command: pnpm --filter @fern-api/rust-sdk dist:cli && docker build -f ./generators/rust/sdk/Dockerfile -t fernapi/fern-rust-sdk:latest .
+ command: pnpm turbo run dist:cli --filter @fern-api/rust-sdk && docker build -f ./generators/rust/sdk/Dockerfile -t fernapi/fern-rust-sdk:latest .
local:
workingDirectory: generators/rust
buildCommand:
- - pnpm --filter @fern-api/rust-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/rust-sdk
runCommand: node --enable-source-maps ./sdk/dist/cli.cjs {CONFIG_PATH}
language: rust
diff --git a/seed/swift-sdk/seed.yml b/seed/swift-sdk/seed.yml
index 741e15a1c6c0..7375a93ae5a1 100644
--- a/seed/swift-sdk/seed.yml
+++ b/seed/swift-sdk/seed.yml
@@ -5,7 +5,7 @@ changelogLocation: ../../generators/swift/sdk/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-api/swift-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/swift-sdk
docker:
file: ./generators/swift/sdk/Dockerfile
image: fernapi/fern-swift-sdk
@@ -14,14 +14,14 @@ publish:
test:
docker:
image: fernapi/fern-swift-sdk:latest
- command: pnpm --filter @fern-api/swift-sdk dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/swift-sdk
podman:
image: fernapi/fern-swift-sdk:latest
- command: pnpm --filter @fern-api/swift-sdk podmanTagLatest
+ command: pnpm turbo run podmanTagLatest --filter @fern-api/swift-sdk
local:
workingDirectory: generators/swift
buildCommand:
- - pnpm --filter @fern-api/swift-sdk dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/swift-sdk
runCommand: node --enable-source-maps sdk/dist/cli.cjs {CONFIG_PATH}
language: swift
diff --git a/seed/ts-express/seed.yml b/seed/ts-express/seed.yml
index 02a047fbfc4d..63fafcda0d63 100644
--- a/seed/ts-express/seed.yml
+++ b/seed/ts-express/seed.yml
@@ -13,7 +13,7 @@ buildScripts:
publish:
preBuildCommands:
- - pnpm --filter @fern-typescript/express-generator-cli dist:cli
+ - pnpm turbo run dist:cli --filter @fern-typescript/express-generator-cli
docker:
file: ./generators/typescript/express/cli/Dockerfile
image: fernapi/fern-typescript-express
@@ -22,15 +22,15 @@ test:
docker:
image: fernapi/fern-typescript-express:latest
command:
- - pnpm --filter @fern-typescript/express-generator-cli dockerTagLatest
+ - pnpm turbo run dockerTagLatest --filter @fern-typescript/express-generator-cli
podman:
image: fernapi/fern-typescript-express:latest
command:
- - pnpm --filter @fern-typescript/express-generator-cli podmanTagLatest
+ - pnpm turbo run podmanTagLatest --filter @fern-typescript/express-generator-cli
local:
workingDirectory: generators/typescript
buildCommand:
- - pnpm --filter @fern-typescript/express-generator-cli dist:cli
+ - pnpm turbo run dist:cli --filter @fern-typescript/express-generator-cli
runCommand: node --enable-source-maps express/cli/dist/cli.cjs {CONFIG_PATH}
env:
NODE_ENV: test
diff --git a/seed/ts-mcp/seed.yml b/seed/ts-mcp/seed.yml
index 48b6212bfeb1..aef9491f611d 100644
--- a/seed/ts-mcp/seed.yml
+++ b/seed/ts-mcp/seed.yml
@@ -5,11 +5,11 @@ changelogLocation: ../../generators/typescript-mcp/model/versions.yml
test:
docker:
image: fernapi/fern-typescript-mcp-server:latest
- command: pnpm --filter @fern-api/typescript-mcp-server dockerTagLatest
+ command: pnpm turbo run dockerTagLatest --filter @fern-api/typescript-mcp-server
local:
workingDirectory: generators/typescript-mcp
buildCommand:
- - pnpm --filter @fern-api/typescript-mcp-server dist:cli
+ - pnpm turbo run dist:cli --filter @fern-api/typescript-mcp-server
runCommand: node --enable-source-maps server/dist/cli.cjs {CONFIG_PATH}
language: typescript
generatorType: SDK
diff --git a/seed/ts-sdk/seed.yml b/seed/ts-sdk/seed.yml
index 450373c75ff7..3ba426f67093 100644
--- a/seed/ts-sdk/seed.yml
+++ b/seed/ts-sdk/seed.yml
@@ -5,7 +5,7 @@ imageAliases: [fernapi/fern-typescript-node-sdk]
changelogLocation: ../../generators/typescript/sdk/versions.yml
publish:
preBuildCommands:
- - pnpm --filter @fern-typescript/sdk-generator-cli dist:cli
+ - pnpm turbo run dist:cli --filter @fern-typescript/sdk-generator-cli
docker:
file: ./generators/typescript/sdk/cli/Dockerfile
image: fernapi/fern-typescript-sdk
@@ -15,15 +15,15 @@ test:
docker:
image: fernapi/fern-typescript-sdk:latest
command:
- - pnpm --filter @fern-typescript/sdk-generator-cli dockerTagLatest
+ - pnpm turbo run dockerTagLatest --filter @fern-typescript/sdk-generator-cli
podman:
image: fernapi/fern-typescript-sdk:latest
command:
- - pnpm --filter @fern-typescript/sdk-generator-cli podmanTagLatest
+ - pnpm turbo run podmanTagLatest --filter @fern-typescript/sdk-generator-cli
local:
workingDirectory: generators/typescript
buildCommand:
- - pnpm --filter @fern-typescript/sdk-generator-cli dist:cli
+ - pnpm turbo run dist:cli --filter @fern-typescript/sdk-generator-cli
runCommand: node --enable-source-maps sdk/cli/dist/cli.cjs {CONFIG_PATH}
env:
NODE_ENV: test
diff --git a/turbo.json b/turbo.json
index c82625190cbe..2e2f1b40f612 100644
--- a/turbo.json
+++ b/turbo.json
@@ -162,6 +162,25 @@
"$TURBO_ROOT$/tsconfig.eslint.json",
"$TURBO_ROOT$/packages/configs/**"
]
+ },
+ "dockerTagLatest": {
+ "outputs": [],
+ "dependsOn": ["dist:cli"],
+ "inputs": ["dist/**", "Dockerfile", "Dockerfile.*"]
+ },
+ "podmanTagLatest": {
+ "outputs": [],
+ "dependsOn": ["dist:cli"],
+ "inputs": [
+ "Dockerfile",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/packages/configs/**",
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json"
+ ]
}
}
}
From 222d07112d357e1e5771d7eebbc2d4ed024a8d51 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Wed, 26 Nov 2025 01:05:45 -0500
Subject: [PATCH 13/16] chore: update workflows and scripts to use turbo for
builds and tests
---
.github/workflows/ci-dynamic-snippets.yml | 32 +++++++++----------
.github/workflows/ci.yml | 6 ++--
.github/workflows/definitions-validation.yml | 2 +-
.github/workflows/publish-cli.yml | 2 +-
.../publish-csharp-dynamic-snippets.yml | 6 ++--
.../workflows/publish-go-dynamic-snippets.yml | 6 ++--
.../publish-java-dynamic-snippets.yml | 6 ++--
.../publish-php-dynamic-snippets.yml | 6 ++--
.../publish-python-dynamic-snippets.yml | 8 ++---
.../publish-ruby-dynamic-snippets.yml | 6 ++--
.github/workflows/publish-snippets-core.yml | 6 ++--
.../publish-swift-dynamic-snippets.yml | 6 ++--
.../workflows/publish-ts-dynamic-snippets.yml | 6 ++--
.github/workflows/sdk-ete-tests.yml | 2 +-
.github/workflows/test-definitions.yml | 2 +-
CONTRIBUTING.md | 4 +--
generators/csharp/CLAUDE.md | 4 +--
generators/rust/CLAUDE.md | 8 ++---
package.json | 7 ++--
packages/cli/cli/turbo.jsonc | 21 ++++++++++++
packages/seed/fern/definition/config.yml | 2 +-
.../resources/config/types/PublishCommand.ts | 2 +-
seed/fern-cli/seed.yml | 3 --
23 files changed, 85 insertions(+), 68 deletions(-)
create mode 100644 packages/cli/cli/turbo.jsonc
diff --git a/.github/workflows/ci-dynamic-snippets.yml b/.github/workflows/ci-dynamic-snippets.yml
index 42534dd31845..b4e3ee4e64f0 100644
--- a/.github/workflows/ci-dynamic-snippets.yml
+++ b/.github/workflows/ci-dynamic-snippets.yml
@@ -41,10 +41,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/typescript-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/typescript-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/typescript-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/typescript-dynamic-snippets
test-python:
runs-on: ubuntu-latest
@@ -71,10 +71,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/python-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/python-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/python-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/python-dynamic-snippets
test-csharp:
runs-on: ubuntu-latest
@@ -101,10 +101,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/csharp-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/csharp-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/csharp-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/csharp-dynamic-snippets
test-go:
runs-on: ubuntu-latest
@@ -131,10 +131,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/go-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/go-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/go-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/go-dynamic-snippets
test-ruby:
runs-on: ubuntu-latest
@@ -161,10 +161,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/ruby-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/ruby-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/ruby-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/ruby-dynamic-snippets
test-php:
runs-on: ubuntu-latest
@@ -191,10 +191,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/php-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/php-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/php-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/php-dynamic-snippets
test-swift:
runs-on: ubuntu-latest
@@ -221,10 +221,10 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/swift-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/swift-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/swift-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/swift-dynamic-snippets
test-java:
runs-on: ubuntu-latest
@@ -251,7 +251,7 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=@fern-api/java-dynamic-snippets compile
+ run: pnpm turbo run compile --filter=@fern-api/java-dynamic-snippets
- name: 🧪 Test
- run: pnpm --filter=@fern-api/java-dynamic-snippets test
+ run: pnpm turbo run test --filter=@fern-api/java-dynamic-snippets
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a7f18e202843..f3a645141ee0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -222,7 +222,7 @@ jobs:
AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
run: |
- pnpm --filter @fern-api/cli dist:cli:dev
+ pnpm turbo run dist:cli:dev --filter @fern-api/cli
cli_path="$(pwd)/packages/cli/cli/dist/dev/cli.cjs"
./scripts/live-test.sh "$cli_path" "$FERN_ORG_TOKEN_DEV"
@@ -249,7 +249,7 @@ jobs:
AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
run: |
- pnpm --filter @fern-api/cli dist:cli:dev
+ pnpm turbo run dist:cli:dev --filter @fern-api/cli
$cliPath = Join-Path $env:GITHUB_WORKSPACE "packages\cli\cli\dist\dev\cli.cjs"
if (-not (Test-Path $cliPath)) {
Write-Error "CLI path does not exist: $cliPath"
@@ -285,7 +285,7 @@ jobs:
AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
run: |
- pnpm --filter @fern-api/cli dist:cli:dev
+ pnpm turbo run dist:cli:dev --filter @fern-api/cli
$cliPath = Join-Path $env:GITHUB_WORKSPACE "packages\cli\cli\dist\dev\cli.cjs"
if (-not (Test-Path $cliPath)) {
Write-Error "CLI path does not exist: $cliPath"
diff --git a/.github/workflows/definitions-validation.yml b/.github/workflows/definitions-validation.yml
index 1b3c70709b94..c15236f6d471 100644
--- a/.github/workflows/definitions-validation.yml
+++ b/.github/workflows/definitions-validation.yml
@@ -25,6 +25,6 @@ jobs:
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }}
run: |
- pnpm --filter @fern-api/cli dist:cli:prod
+ pnpm turbo run dist:cli:prod --filter @fern-api/cli
cli_path="$(pwd)/packages/cli/cli/dist/prod/cli.cjs"
node $cli_path check
diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml
index 85c001fb5041..1db116af8af5 100644
--- a/.github/workflows/publish-cli.yml
+++ b/.github/workflows/publish-cli.yml
@@ -98,7 +98,7 @@ jobs:
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }}
run: |
- pnpm --filter @fern-api/cli dist:cli:prod
+ pnpm turbo run dist:cli:prod --filter @fern-api/cli
cli_path="$(pwd)/packages/cli/cli/dist/prod/cli.cjs"
./scripts/live-test.sh "$cli_path" "$FERN_TOKEN" "true"
diff --git a/.github/workflows/publish-csharp-dynamic-snippets.yml b/.github/workflows/publish-csharp-dynamic-snippets.yml
index afb16bd3548f..cafde09626ad 100644
--- a/.github/workflows/publish-csharp-dynamic-snippets.yml
+++ b/.github/workflows/publish-csharp-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/csharp-dynamic-snippets
run: |
cd generators/csharp/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-go-dynamic-snippets.yml b/.github/workflows/publish-go-dynamic-snippets.yml
index 27d0ade14fcd..72f912a62681 100644
--- a/.github/workflows/publish-go-dynamic-snippets.yml
+++ b/.github/workflows/publish-go-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/go-dynamic-snippets
run: |
cd generators/go-v2/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-java-dynamic-snippets.yml b/.github/workflows/publish-java-dynamic-snippets.yml
index 5331cc2cc3b3..6ed1b9b5a061 100644
--- a/.github/workflows/publish-java-dynamic-snippets.yml
+++ b/.github/workflows/publish-java-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/java-dynamic-snippets
run: |
cd generators/java-v2/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-php-dynamic-snippets.yml b/.github/workflows/publish-php-dynamic-snippets.yml
index 69b83b285aa2..a1064e19f1ae 100644
--- a/.github/workflows/publish-php-dynamic-snippets.yml
+++ b/.github/workflows/publish-php-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/php-dynamic-snippets
run: |
cd generators/php/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-python-dynamic-snippets.yml b/.github/workflows/publish-python-dynamic-snippets.yml
index d9f5e85489a5..e775cd32dfa3 100644
--- a/.github/workflows/publish-python-dynamic-snippets.yml
+++ b/.github/workflows/publish-python-dynamic-snippets.yml
@@ -64,17 +64,17 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/python-dynamic-snippets
run: |
cd generators/python-v2/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
- npm publish --access public -tag latest
+ npm publish --access public --tag latest
- name: Update fern-platform dependency
uses: ./.github/actions/update-fern-platform-dependency
diff --git a/.github/workflows/publish-ruby-dynamic-snippets.yml b/.github/workflows/publish-ruby-dynamic-snippets.yml
index 0b81c345b340..b2bfd9c45e79 100644
--- a/.github/workflows/publish-ruby-dynamic-snippets.yml
+++ b/.github/workflows/publish-ruby-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/ruby-dynamic-snippets
run: |
cd generators/ruby-v2/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-snippets-core.yml b/.github/workflows/publish-snippets-core.yml
index b417be6e8cc5..9a9aad4a0884 100644
--- a/.github/workflows/publish-snippets-core.yml
+++ b/.github/workflows/publish-snippets-core.yml
@@ -44,14 +44,14 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/snippets-core
run: |
cd packages/snippets/core
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ inputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ inputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-swift-dynamic-snippets.yml b/.github/workflows/publish-swift-dynamic-snippets.yml
index 6c7912234254..e757cf934983 100644
--- a/.github/workflows/publish-swift-dynamic-snippets.yml
+++ b/.github/workflows/publish-swift-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/swift-dynamic-snippets
run: |
cd generators/swift/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/publish-ts-dynamic-snippets.yml b/.github/workflows/publish-ts-dynamic-snippets.yml
index 28c0868c1b17..3876ecf3f579 100644
--- a/.github/workflows/publish-ts-dynamic-snippets.yml
+++ b/.github/workflows/publish-ts-dynamic-snippets.yml
@@ -64,15 +64,15 @@ jobs:
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest
- name: 🧪 Build
- run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
+ run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
- name: 🧪 Test
- run: pnpm --filter=${{ env.PACKAGE_NAME }} test
+ run: pnpm turbo run test --filter=${{ env.PACKAGE_NAME }}
- name: Publish @fern-api/typescript-dynamic-snippets
run: |
cd generators/typescript-v2/dynamic-snippets
- pnpm --filter=${{ env.PACKAGE_NAME }} dist ${{ steps.ver.outputs.version }}
+ pnpm turbo run dist --filter=${{ env.PACKAGE_NAME }} -- ${{ steps.ver.outputs.version }}
cd dist
npm publish --access public --tag latest
diff --git a/.github/workflows/sdk-ete-tests.yml b/.github/workflows/sdk-ete-tests.yml
index df633dbd5156..3d4653ffab70 100644
--- a/.github/workflows/sdk-ete-tests.yml
+++ b/.github/workflows/sdk-ete-tests.yml
@@ -74,7 +74,7 @@ jobs:
- name: Build PHP SDK generator
if: ${{ inputs.language == 'php' || github.event_name == 'pull_request' }}
- run: pnpm --filter @fern-api/php-sdk dist:cli
+ run: pnpm turbo run dist:cli --filter @fern-api/php-sdk
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
diff --git a/.github/workflows/test-definitions.yml b/.github/workflows/test-definitions.yml
index 5c36f4d9a7a9..92bce4903c8f 100644
--- a/.github/workflows/test-definitions.yml
+++ b/.github/workflows/test-definitions.yml
@@ -47,7 +47,7 @@ jobs:
env:
FORCE_COLOR: "2"
run: |
- pnpm --filter @fern-api/cli dist:cli:dev
+ pnpm turbo run dist:cli:dev --filter @fern-api/cli
cli_path="$(pwd)/packages/cli/cli/dist/dev/cli.cjs"
cd test-definitions
FERN_NO_VERSION_REDIRECTION=true node $cli_path check
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e7edb65b44a8..a9dbaf5e9364 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,7 +85,7 @@ pnpm install
To compile all the packages in this monorepo, run `pnpm compile`.
-To compile a single package, filter to the relevant package: `pnpm --filter @fern-api/openapi-parser compile`.
+To compile a single package, filter to the relevant package: `pnpm turbo run compile --filter @fern-api/openapi-parser`.
### Step 3: Testing
@@ -93,7 +93,7 @@ This repo contains both unit tests and integration (end-to-end) tests.
To run all the unit tests: `pnpm test`.
-To run unit tests for a single package: `pnpm --filter @fern-api/openapi-parser test`
+To run unit tests for a single package: `pnpm turbo run test --filter @fern-api/openapi-parser`
To run the integration tests: `pnpm test:ete`.
diff --git a/generators/csharp/CLAUDE.md b/generators/csharp/CLAUDE.md
index 620eca31312d..c84b4a365388 100644
--- a/generators/csharp/CLAUDE.md
+++ b/generators/csharp/CLAUDE.md
@@ -33,8 +33,8 @@ This file provides guidance for Claude Code when working with the C# generator.
```bash
pnpm install
-pnpm --filter @fern-api/fern-csharp-sdk compile
-pnpm --filter @fern-api/fern-csharp-model compile
+pnpm turbo run compile --filter @fern-api/fern-csharp-sdk
+pnpm turbo run compile --filter @fern-api/fern-csharp-model
```
### Configuration Options
diff --git a/generators/rust/CLAUDE.md b/generators/rust/CLAUDE.md
index 76fccae01f4d..a6f7440b6c64 100644
--- a/generators/rust/CLAUDE.md
+++ b/generators/rust/CLAUDE.md
@@ -43,10 +43,10 @@ This file provides guidance for Claude Code when working with the Rust generator
```bash
pnpm install
-pnpm --filter @fern-api/rust-sdk compile
-pnpm --filter @fern-api/rust-model compile
-pnpm --filter @fern-api/rust-sdk dist:cli # Build Docker CLI
-pnpm --filter @fern-api/rust-model dist:cli # Build Docker CLI
+pnpm turbo run compile --filter @fern-api/rust-sdk
+pnpm turbo run compile --filter @fern-api/rust-model
+pnpm turbo run dist:cli --filter @fern-api/rust-sdk # Build Docker CLI
+pnpm turbo run dist:cli --filter @fern-api/rust-model # Build Docker CLI
```
### Configuration Options
diff --git a/package.json b/package.json
index 9878fe6c7a87..e55dc4dfbb6e 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"fern:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/prod/cli.cjs",
"fern-dev:local": "FERN_NO_VERSION_REDIRECTION=true node --enable-source-maps ./packages/cli/cli/dist/dev/cli.cjs",
"fern:build": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
- "fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" pnpm --filter @fern-api/cli dist:cli:prod:unminified && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
+ "fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod:unminified --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern-dev:build": "turbo run dist:cli:dev --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
"fern-local:build": "turbo run dist:cli:local --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
"generator-cli:generate": "pnpm fern generate --api generator-cli --local && pnpm --filter=@fern-api/generator-cli compile",
@@ -63,7 +63,7 @@
"root-package:check": "pnpm fern-script check-root-package",
"root-package:fix": "pnpm root-package:check --fix",
"seed": "node --enable-source-maps packages/seed/dist/cli.cjs",
- "seed:local": "pnpm --filter @fern-api/seed-cli dist:cli && node --enable-source-maps packages/seed/dist/cli.cjs",
+ "seed:local": "pnpm turbo run dist:cli --filter @fern-api/seed-cli && node --enable-source-maps packages/seed/dist/cli.cjs",
"ir:generate": "pnpm --filter @fern-api/ir-sdk generate",
"ir:generate:go": "cd packages/ir-sdk && fern ir ../../generators/go/internal/fern/ir.json --api ir-types-latest --language go && cd ../../generators/go && make install && make generate",
"openapi-ir:generate": "pnpm --filter @fern-api/openapi-ir generate",
@@ -75,8 +75,7 @@
"update:generators": "pnpm generators:generate && pnpm generators-yml:jsonschema",
"prepare": "husky",
"pre-commit": "tsx scripts/pre-commit.ts",
- "fix:references": "npx nx g @nx/js:typescript-sync --updateReferences=true --updatePaths=false",
- "test:update-package": "sh -c 'for arg in \"$@\"; do if [[ $arg == --package=* ]]; then PACKAGE=${arg#--package=}; break; fi; done; pnpm --filter \"$PACKAGE\" compile && pnpm --filter \"$PACKAGE\" test:update' --"
+ "fix:references": "npx nx g @nx/js:typescript-sync --updateReferences=true --updatePaths=false"
},
"devDependencies": {
"@babel/core": "^7.26.0",
diff --git a/packages/cli/cli/turbo.jsonc b/packages/cli/cli/turbo.jsonc
new file mode 100644
index 000000000000..b94381a95258
--- /dev/null
+++ b/packages/cli/cli/turbo.jsonc
@@ -0,0 +1,21 @@
+{
+ "extends": ["//"],
+ "tasks": {
+ "dist:cli:prod:unminified": {
+ "outputs": ["dist/**"],
+ "dependsOn": ["^compile"],
+ "inputs": [
+ "src/**",
+ "tests/**",
+ "package.json",
+ "tsconfig.json",
+ "build-utils.mjs",
+ "build.prod.mjs",
+ "$TURBO_ROOT$/shared/.prettierignore",
+ "$TURBO_ROOT$/shared/stylelintrc.shared.json",
+ "$TURBO_ROOT$/tsconfig.eslint.json",
+ "$TURBO_ROOT$/packages/configs/**"
+ ]
+ }
+ }
+}
diff --git a/packages/seed/fern/definition/config.yml b/packages/seed/fern/definition/config.yml
index 85d5308869b2..236eac5ca145 100644
--- a/packages/seed/fern/definition/config.yml
+++ b/packages/seed/fern/definition/config.yml
@@ -63,7 +63,7 @@ types:
PublishCommand:
docs: |
Configuration for publishing from a command, assuming something packaged up, like with the TypeScript generator.
- ex. `pnpm --filter @fern-typescript/express-generator-cli dockerTagVersion "$VERSION"`
+ ex. `pnpm turbo run dockerTagVersion --filter @fern-typescript/express-generator-cli -- "$VERSION"`
Commands can be multi-line, we'll run them all!
properties:
workingDirectory: optional
diff --git a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts
index f7268a287a5d..fb6925a245e1 100644
--- a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts
+++ b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts
@@ -6,7 +6,7 @@ import * as FernSeedConfig from "../../../index";
/**
* Configuration for publishing from a command, assuming something packaged up, like with the TypeScript generator.
- * ex. `pnpm --filter @fern-typescript/express-generator-cli dockerTagVersion "$VERSION"`
+ * ex. `pnpm turbo run dockerTagVersion --filter @fern-typescript/express-generator-cli -- "$VERSION"`
* Commands can be multi-line, we'll run them all!
*/
export interface PublishCommand {
diff --git a/seed/fern-cli/seed.yml b/seed/fern-cli/seed.yml
index c4f38005c30e..9aa5132827de 100644
--- a/seed/fern-cli/seed.yml
+++ b/seed/fern-cli/seed.yml
@@ -5,7 +5,6 @@ publishGa:
command:
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- pnpm install
- - pnpm turbo run compile --filter @fern-api/cli
- pnpm turbo run dist:cli:prod --filter @fern-api/cli -- $VERSION
- pnpm --filter @fern-api/cli publish:cli:prod --tag latest
publishRc:
@@ -14,7 +13,6 @@ publishRc:
command:
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- pnpm install
- - pnpm turbo run compile --filter @fern-api/cli
- pnpm turbo run dist:cli:prod --filter @fern-api/cli -- $VERSION
- pnpm --filter @fern-api/cli publish:cli:prod --tag prerelease
publishDev:
@@ -22,6 +20,5 @@ publishDev:
versionSubstitution: $VERSION
command:
- pnpm install
- - pnpm turbo run compile --filter @fern-api/cli
- pnpm turbo run dist:cli:dev --filter @fern-api/cli -- $VERSION
- pnpm --filter @fern-api/cli publish:cli:dev --access restricted
\ No newline at end of file
From 50228a4d18f1b630adcc85dc72791538a5bfc01b Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Wed, 26 Nov 2025 11:20:33 -0500
Subject: [PATCH 14/16] chore: remove fern seed build from turbo and update
generator-cli scripts
---
.github/workflows/publish-generator-cli.yml | 4 ++--
package.json | 2 +-
.../generation/protoc-gen-fern/package.json | 2 +-
packages/generator-cli/bin/cli | 2 ++
packages/generator-cli/build.mjs | 22 +++++++++++++++++++
packages/generator-cli/package.json | 15 ++++++-------
.../generator-cli/src/__test__/pr.test.ts | 2 +-
.../src/__test__/testGenerateReadme.ts | 2 +-
.../src/__test__/testGenerateReference.ts | 2 +-
packages/generator-cli/tsup.config.ts | 12 ----------
packages/generator-cli/turbo.json | 8 +++----
turbo.json | 4 ++--
12 files changed, 44 insertions(+), 33 deletions(-)
create mode 100755 packages/generator-cli/bin/cli
create mode 100644 packages/generator-cli/build.mjs
delete mode 100644 packages/generator-cli/tsup.config.ts
diff --git a/.github/workflows/publish-generator-cli.yml b/.github/workflows/publish-generator-cli.yml
index 648f50a48325..d31b95b5db77 100644
--- a/.github/workflows/publish-generator-cli.yml
+++ b/.github/workflows/publish-generator-cli.yml
@@ -79,10 +79,10 @@ jobs:
- name: Update npm
run: npm install -g npm@latest
- - name: Compile
+ - name: Build CLI
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
- run: pnpm turbo run compile --filter=${{ env.PACKAGE_NAME }}
+ run: pnpm turbo run dist:cli --filter=${{ env.PACKAGE_NAME }}
- name: Test
env:
diff --git a/package.json b/package.json
index e55dc4dfbb6e..4cf7c9d8e513 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"fern:build:unminified": "cross-env POSTHOG_API_KEY=\"\" turbo run dist:cli:prod:unminified --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/prod/cli.cjs'",
"fern-dev:build": "turbo run dist:cli:dev --filter=@fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/dev/cli.cjs'",
"fern-local:build": "turbo run dist:cli:local --filter @fern-api/cli && echo 'Run node --enable-source-maps packages/cli/cli/dist/local/cli.cjs'",
- "generator-cli:generate": "pnpm fern generate --api generator-cli --local && pnpm --filter=@fern-api/generator-cli compile",
+ "generator-cli:generate": "pnpm fern:build && pnpm fern generate --api generator-cli --local && turbo dist:cli --filter=@fern-api/generator-cli",
"seed:build": "turbo run dist:cli --filter=@fern-api/seed-cli && echo 'Run node --enable-source-maps packages/seed/dist/cli.cjs'",
"publish": "pnpm -r publish --access public --no-git-checks --loglevel=verbose",
"jsonschema": "pnpm definition-yml:jsonschema && pnpm api-yml:jsonschema && pnpm package-yml:jsonschema && pnpm docs-yml:jsonschema && pnpm generators-yml:jsonschema && pnpm versions-yml:jsonschema && pnpm products-yml:jsonschema",
diff --git a/packages/cli/generation/protoc-gen-fern/package.json b/packages/cli/generation/protoc-gen-fern/package.json
index d882b3fcf386..9b93730863fe 100644
--- a/packages/cli/generation/protoc-gen-fern/package.json
+++ b/packages/cli/generation/protoc-gen-fern/package.json
@@ -24,7 +24,7 @@
"bin": {
"protoc-gen-fern": "bin/protoc-gen-fern"
},
- "files": ["lib"],
+ "files": ["bin", "lib"],
"scripts": {
"clean": "rm -rf ./lib && tsc --build --clean",
"compile": "tsc --build",
diff --git a/packages/generator-cli/bin/cli b/packages/generator-cli/bin/cli
new file mode 100755
index 000000000000..0b20ac1914ac
--- /dev/null
+++ b/packages/generator-cli/bin/cli
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require("../dist/cli.js");
diff --git a/packages/generator-cli/build.mjs b/packages/generator-cli/build.mjs
new file mode 100644
index 000000000000..3a711542f25f
--- /dev/null
+++ b/packages/generator-cli/build.mjs
@@ -0,0 +1,22 @@
+import { writeFile } from 'fs/promises';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import tsup from 'tsup';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+main();
+
+async function main() {
+ await tsup.build({
+ entry: ['src/cli.ts', 'src/api.ts'],
+ format: ['cjs'],
+ dts: true,
+ // Bundle all workspace dependencies to avoid ESM resolution issues
+ noExternal: ['@fern-api/fs-utils', '@fern-api/github'],
+ minify: false,
+ sourcemap: false,
+ outDir: 'dist',
+ clean: true
+ });
+}
diff --git a/packages/generator-cli/package.json b/packages/generator-cli/package.json
index ca258c9eb2a5..c241c4973384 100644
--- a/packages/generator-cli/package.json
+++ b/packages/generator-cli/package.json
@@ -5,19 +5,18 @@
"type": "git",
"url": "https://github.com/fern-api/fern"
},
- "type": "module",
- "main": "dist/api.cjs",
+ "type": "commonjs",
+ "main": "dist/api.js",
"source": "src/index.ts",
- "types": "dist/api.d.cts",
+ "types": "dist/api.d.ts",
"bin": {
- "generator-cli": "dist/cli.cjs"
+ "generator-cli": "dist/cli"
},
- "files": ["dist"],
+ "files": ["bin", "dist"],
"scripts": {
- "clean": "rm -rf ./dist && tsc --build --clean",
- "compile": "tsup && pnpm compile:api:dts && echo '#!/usr/bin/env node' | cat - dist/cli.cjs > dist/tmp && mv dist/tmp dist/cli.cjs",
- "compile:api:dts": "tsup ./src/api.ts --format cjs --dts --dts-only",
+ "clean": "rm -rf ./lib && rm -rf ./dist && tsc --build --clean",
"depcheck": "depcheck",
+ "dist:cli": "node build.mjs",
"test": "vitest --run --passWithNoTests --globals --disable-console-intercept",
"test:update": "vitest -u --run --passWithNoTests --globals --disable-console-intercept"
},
diff --git a/packages/generator-cli/src/__test__/pr.test.ts b/packages/generator-cli/src/__test__/pr.test.ts
index 92245da429f9..d52c3c40079c 100644
--- a/packages/generator-cli/src/__test__/pr.test.ts
+++ b/packages/generator-cli/src/__test__/pr.test.ts
@@ -24,7 +24,7 @@ describe("GitHub PR CLI call", () => {
const file = await tmp.file();
await writeFile(file.path, JSON.stringify(config, undefined, 2));
- const args = [path.join(__dirname, "../../dist/cli.cjs"), "github", "pr", "--config", file.path];
+ const args = [path.join(__dirname, "../../bin/cli"), "github", "pr", "--config", file.path];
const { stdout } = await execa("node", args);
expect(stdout).toMatchSnapshot();
diff --git a/packages/generator-cli/src/__test__/testGenerateReadme.ts b/packages/generator-cli/src/__test__/testGenerateReadme.ts
index 3113c29c174f..098681139935 100644
--- a/packages/generator-cli/src/__test__/testGenerateReadme.ts
+++ b/packages/generator-cli/src/__test__/testGenerateReadme.ts
@@ -26,7 +26,7 @@ export function testGenerateReadme({
const json = JSON.stringify(await serializers.ReadmeConfig.jsonOrThrow(config), undefined, 2);
await writeFile(file.path, json);
- const args = [path.join(__dirname, "../../dist/cli.cjs"), "generate", "readme", "--config", file.path];
+ const args = [path.join(__dirname, "../../bin/cli"), "generate", "readme", "--config", file.path];
if (originalReadme != null) {
args.push(
...[
diff --git a/packages/generator-cli/src/__test__/testGenerateReference.ts b/packages/generator-cli/src/__test__/testGenerateReference.ts
index f635c78fa263..fb535a8d0167 100644
--- a/packages/generator-cli/src/__test__/testGenerateReference.ts
+++ b/packages/generator-cli/src/__test__/testGenerateReference.ts
@@ -19,7 +19,7 @@ export function testGenerateReference({
const json = JSON.stringify(await serializers.ReferenceConfig.jsonOrThrow(config), undefined, 2);
await writeFile(file.path, json);
- const args = [path.join(__dirname, "../../dist/cli.cjs"), "generate-reference", "--config", file.path];
+ const args = [path.join(__dirname, "../../bin/cli"), "generate-reference", "--config", file.path];
const { stdout } = await execa("node", args);
expect(stdout).toMatchFileSnapshot(`__snapshots__/${fixtureName}.md`);
});
diff --git a/packages/generator-cli/tsup.config.ts b/packages/generator-cli/tsup.config.ts
deleted file mode 100644
index 93e4f5a9ddde..000000000000
--- a/packages/generator-cli/tsup.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineConfig } from "tsup";
-
-export default defineConfig({
- entry: ["src/cli.ts", "src/api.ts"],
- format: ["cjs"],
- dts: false,
- bundle: true,
- // Bundle all workspace dependencies to avoid ESM resolution issues
- noExternal: ["@fern-api/fs-utils", "@fern-api/github"],
- minify: false,
- sourcemap: false
-});
diff --git a/packages/generator-cli/turbo.json b/packages/generator-cli/turbo.json
index d233306d1226..349321ab33a7 100644
--- a/packages/generator-cli/turbo.json
+++ b/packages/generator-cli/turbo.json
@@ -2,11 +2,11 @@
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
- "compile": {
- "outputs": ["dist/**", "lib/**"]
- },
"test": {
- "dependsOn": ["compile", "^compile"]
+ "dependsOn": ["dist:cli"]
+ },
+ "test:update": {
+ "dependsOn": ["dist:cli"]
}
}
}
diff --git a/turbo.json b/turbo.json
index 2e2f1b40f612..b59b36d2dbfc 100644
--- a/turbo.json
+++ b/turbo.json
@@ -38,7 +38,7 @@
"inputs": ["src/**", "tests/**", "$TURBO_ROOT$/.eslintrc.js", "$TURBO_ROOT$/.eslintignore", "tsconfig.json"]
},
"test": {
- "dependsOn": ["^compile"],
+ "dependsOn": ["^compile", "compile"],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
@@ -70,7 +70,7 @@
]
},
"test:update": {
- "dependsOn": ["^compile"],
+ "dependsOn": ["^compile", "compile"],
"outputs": [],
"inputs": [
"$TURBO_ROOT$/packages/configs/**",
From 46b61616f289376a290f57b9bbdbf3751cbe26c9 Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Sat, 29 Nov 2025 23:47:38 -0500
Subject: [PATCH 15/16] seed
---
seed/go-model/examples/commons/types.go | 6 +-
seed/go-model/exhaustive/types/docs.go | 6 +-
seed/go-model/exhaustive/types/object.go | 48 +-
.../idempotency-headers/.fern/metadata.json | 12 +
.../internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
.../idempotency-headers/internal/stringer.go | 13 +
.../idempotency-headers/internal/time.go | 137 +++++
.../reserved-keywords/.fern/metadata.json | 5 +
seed/go-model/reserved-keywords/doc.go | 1 +
seed/go-model/reserved-keywords/go.mod | 17 +-
seed/go-model/reserved-keywords/go.sum | 10 +
seed/go-model/reserved-keywords/package.go | 173 +++---
.../reserved-keywords/snippet-templates.json | 0
seed/go-model/streaming/.fern/metadata.json | 11 +
.../streaming/internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
seed/go-model/streaming/internal/stringer.go | 13 +
seed/go-model/streaming/internal/time.go | 137 +++++
seed/go-model/unions-with-local-date/types.go | 16 +-
seed/go-model/unions/types.go | 16 +-
seed/go-sdk/accept-header/internal/query.go | 3 +
.../accept-header/internal/query_test.go | 21 +
seed/go-sdk/alias/internal/query.go | 3 +
seed/go-sdk/alias/internal/query_test.go | 21 +
seed/go-sdk/any-auth/internal/query.go | 3 +
seed/go-sdk/any-auth/internal/query_test.go | 21 +
.../api-wide-base-path/internal/query.go | 3 +
.../api-wide-base-path/internal/query_test.go | 21 +
seed/go-sdk/audiences/internal/query.go | 3 +
seed/go-sdk/audiences/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/basic-auth/internal/query.go | 3 +
seed/go-sdk/basic-auth/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/bytes-upload/internal/query.go | 3 +
.../bytes-upload/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../circular-references/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../client-side-params/internal/query.go | 3 +
.../client-side-params/internal/query_test.go | 21 +
seed/go-sdk/content-type/internal/query.go | 3 +
.../content-type/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/empty-clients/internal/query.go | 3 +
.../empty-clients/internal/query_test.go | 21 +
seed/go-sdk/enum/internal/query.go | 3 +
seed/go-sdk/enum/internal/query_test.go | 21 +
seed/go-sdk/error-property/internal/query.go | 3 +
.../error-property/internal/query_test.go | 21 +
seed/go-sdk/errors/internal/query.go | 3 +
seed/go-sdk/errors/internal/query_test.go | 21 +
.../commons/types.go | 32 +-
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../commons/types.go | 32 +-
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../examples/client-name/commons/types.go | 32 +-
.../examples/client-name/internal/query.go | 3 +
.../client-name/internal/query_test.go | 21 +
.../commons/types.go | 32 +-
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../exported-client-name/commons/types.go | 32 +-
.../exported-client-name/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../getters-pass-by-value/commons/types.go | 32 +-
.../getters-pass-by-value/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../no-custom-config/commons/types.go | 32 +-
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../pleaseinhere/commons/types.go | 32 +-
.../pleaseinhere/internal/query.go | 3 +
.../pleaseinhere/internal/query_test.go | 21 +
.../examples/readme-config/commons/types.go | 32 +-
.../examples/readme-config/internal/query.go | 3 +
.../readme-config/internal/query_test.go | 21 +
seed/go-sdk/examples/v0/commons/types.go | 32 +-
seed/go-sdk/examples/v0/internal/query.go | 3 +
.../go-sdk/examples/v0/internal/query_test.go | 21 +
.../dynamic-snippets/example1/snippet.go | 4 +-
.../dynamic-snippets/example11/snippet.go | 2 +-
.../dynamic-snippets/example12/snippet.go | 2 +-
.../dynamic-snippets/example13/snippet.go | 2 +-
.../dynamic-snippets/example15/snippet.go | 2 +-
.../dynamic-snippets/example16/snippet.go | 2 +-
.../dynamic-snippets/example18/snippet.go | 4 +-
.../dynamic-snippets/example19/snippet.go | 4 +-
.../dynamic-snippets/example20/snippet.go | 8 +-
.../dynamic-snippets/example3/snippet.go | 2 +-
.../dynamic-snippets/example44/snippet.go | 2 +-
.../dynamic-snippets/example45/snippet.go | 2 +-
.../dynamic-snippets/example5/snippet.go | 2 +-
.../dynamic-snippets/example6/snippet.go | 2 +-
.../dynamic-snippets/example7/snippet.go | 2 +-
.../dynamic-snippets/example8/snippet.go | 2 +-
seed/go-sdk/exhaustive/inlined_requests.go | 12 +-
seed/go-sdk/exhaustive/internal/query.go | 3 +
seed/go-sdk/exhaustive/internal/query_test.go | 21 +
seed/go-sdk/exhaustive/reference.md | 42 +-
seed/go-sdk/exhaustive/snippet.json | 30 +-
seed/go-sdk/exhaustive/types/docs.go | 16 +-
seed/go-sdk/exhaustive/types/object.go | 112 ++--
seed/go-sdk/extends/internal/query.go | 3 +
seed/go-sdk/extends/internal/query_test.go | 21 +
.../go-sdk/extra-properties/internal/query.go | 3 +
.../extra-properties/internal/query_test.go | 21 +
seed/go-sdk/file-download/internal/query.go | 3 +
.../file-download/internal/query_test.go | 21 +
.../file-upload-openapi/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../package-name/internal/query.go | 3 +
.../package-name/internal/query_test.go | 21 +
seed/go-sdk/file-upload/v0/internal/query.go | 3 +
.../file-upload/v0/internal/query_test.go | 21 +
seed/go-sdk/folders/internal/query.go | 3 +
seed/go-sdk/folders/internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/go-content-type/internal/query.go | 3 +
.../go-content-type/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/header-auth/internal/query.go | 3 +
.../go-sdk/header-auth/internal/query_test.go | 21 +
seed/go-sdk/http-head/internal/query.go | 3 +
seed/go-sdk/http-head/internal/query_test.go | 21 +
.../idempotency-headers/.fern/metadata.json | 13 +
.../.github/workflows/ci.yml | 35 ++
.../idempotency-headers/client/client.go | 33 ++
.../idempotency-headers/client/client_test.go | 45 ++
.../idempotency-headers/client/options.go | 45 ++
.../idempotency-headers/core/api_error.go | 47 ++
seed/go-sdk/idempotency-headers/core/http.go | 15 +
.../core/idempotent_request_option.go | 72 +++
.../core/request_option.go | 153 ++++++
.../dynamic-snippets/example0/snippet.go | 27 +
.../dynamic-snippets/example1/snippet.go | 22 +
.../idempotency-headers/internal/caller.go | 250 +++++++++
.../internal/caller_test.go | 395 ++++++++++++++
.../internal/error_decoder.go | 64 +++
.../internal/error_decoder_test.go | 59 +++
.../internal/explicit_fields.go | 116 ++++
.../internal/explicit_fields_test.go | 497 ++++++++++++++++++
.../internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
.../idempotency-headers/internal/http.go | 71 +++
.../idempotency-headers/internal/query.go | 353 +++++++++++++
.../internal/query_test.go | 395 ++++++++++++++
.../idempotency-headers/internal/retrier.go | 230 ++++++++
.../internal/retrier_test.go | 300 +++++++++++
.../idempotency-headers/internal/stringer.go | 13 +
.../idempotency-headers/internal/time.go | 137 +++++
.../option/idempotent_request_option.go | 24 +
.../option/request_option.go | 71 +++
.../idempotency-headers/payment/client.go | 66 +++
.../idempotency-headers/payment/raw_client.go | 114 ++++
.../the/way/in/here/please/internal/query.go | 3 +
.../way/in/here/please/internal/query_test.go | 21 +
.../imdb/no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../inhereplease/internal/query.go | 3 +
.../inhereplease/internal/query_test.go | 21 +
.../with-wiremock-tests/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../inferred-auth-explicit/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../inferred-auth-implicit/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/license/internal/query.go | 3 +
seed/go-sdk/license/internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../default-values/internal/query.go | 3 +
.../default-values/internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../mixed-file-directory/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/multi-line-docs/internal/query.go | 3 +
.../multi-line-docs/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../multi-url-environment/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../multiple-request-bodies/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/no-environment/internal/query.go | 3 +
.../no-environment/internal/query_test.go | 21 +
seed/go-sdk/no-retries/internal/query.go | 3 +
seed/go-sdk/no-retries/internal/query_test.go | 21 +
.../nullable-optional/internal/query.go | 3 +
.../nullable-optional/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/nullable/internal/query.go | 3 +
seed/go-sdk/nullable/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/object/internal/query.go | 3 +
seed/go-sdk/object/internal/query_test.go | 21 +
.../objects-with-imports/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/optional/internal/query.go | 3 +
seed/go-sdk/optional/internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
seed/go-sdk/pagination/internal/query.go | 3 +
seed/go-sdk/pagination/internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../package-name/internal/query.go | 3 +
.../package-name/internal/query_test.go | 21 +
.../path-parameters/v0/internal/query.go | 3 +
.../path-parameters/v0/internal/query_test.go | 21 +
seed/go-sdk/plain-text/internal/query.go | 3 +
seed/go-sdk/plain-text/internal/query_test.go | 21 +
seed/go-sdk/property-access/internal/query.go | 3 +
.../property-access/internal/query_test.go | 21 +
seed/go-sdk/public-object/internal/query.go | 3 +
.../public-object/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../go-sdk/query-parameters/internal/query.go | 3 +
.../query-parameters/internal/query_test.go | 21 +
.../required-nullable/internal/query.go | 3 +
.../required-nullable/internal/query_test.go | 21 +
.../reserved-keywords/.fern/metadata.json | 8 +
.../.github/workflows/ci.yml | 35 ++
seed/go-sdk/reserved-keywords/README.md | 193 +++++++
.../go-sdk/reserved-keywords/client/client.go | 33 ++
.../reserved-keywords/client/client_test.go | 45 ++
.../reserved-keywords/core/api_error.go | 47 ++
seed/go-sdk/reserved-keywords/core/http.go | 15 +
.../reserved-keywords/core/request_option.go | 109 ++++
.../dynamic-snippets/example0/snippet.go | 23 +
seed/go-sdk/reserved-keywords/error_codes.go | 9 +
seed/go-sdk/reserved-keywords/file_param.go | 41 ++
seed/go-sdk/reserved-keywords/go.mod | 16 +
seed/go-sdk/reserved-keywords/go.sum | 12 +
.../reserved-keywords/internal/caller.go | 250 +++++++++
.../reserved-keywords/internal/caller_test.go | 395 ++++++++++++++
.../internal/error_decoder.go | 64 +++
.../internal/error_decoder_test.go | 59 +++
.../internal/explicit_fields.go | 116 ++++
.../internal/explicit_fields_test.go | 497 ++++++++++++++++++
.../internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
.../go-sdk/reserved-keywords/internal/http.go | 71 +++
.../reserved-keywords/internal/query.go | 353 +++++++++++++
.../reserved-keywords/internal/query_test.go | 395 ++++++++++++++
.../reserved-keywords/internal/retrier.go | 230 ++++++++
.../internal/retrier_test.go | 300 +++++++++++
.../reserved-keywords/internal/stringer.go | 13 +
.../go-sdk/reserved-keywords/internal/time.go | 137 +++++
.../option/request_option.go | 64 +++
seed/go-sdk/reserved-keywords/package.go | 207 ++++++++
.../reserved-keywords/package_/client.go | 49 ++
.../reserved-keywords/package_/raw_client.go | 76 +++
seed/go-sdk/reserved-keywords/pointer.go | 132 +++++
seed/go-sdk/reserved-keywords/reference.md | 48 ++
seed/go-sdk/reserved-keywords/snippet.json | 15 +
.../response-property/internal/query.go | 3 +
.../response-property/internal/query_test.go | 21 +
.../with-wire-tests/.fern/metadata.json | 12 +
.../with-wire-tests/.github/workflows/ci.yml | 35 ++
.../with-wire-tests/README.md | 193 +++++++
.../with-wire-tests/client/client.go | 33 ++
.../with-wire-tests/client/client_test.go | 45 ++
.../with-wire-tests/completions.go | 129 +++++
.../with-wire-tests/completions/client.go | 71 +++
.../completions_test/completions_test.go | 84 +++
.../with-wire-tests/completions/raw_client.go | 27 +
.../with-wire-tests/core/api_error.go | 47 ++
.../with-wire-tests/core/http.go | 15 +
.../with-wire-tests/core/request_option.go | 109 ++++
.../with-wire-tests/core/stream.go | 368 +++++++++++++
.../dynamic-snippets/example0/snippet.go | 23 +
.../dynamic-snippets/example1/snippet.go | 23 +
.../with-wire-tests/error_codes.go | 9 +
.../with-wire-tests/file_param.go | 41 ++
.../with-wire-tests/go.mod | 16 +
.../with-wire-tests/go.sum | 12 +
.../with-wire-tests/internal/caller.go | 250 +++++++++
.../with-wire-tests/internal/caller_test.go | 395 ++++++++++++++
.../with-wire-tests/internal/error_decoder.go | 64 +++
.../internal/error_decoder_test.go | 59 +++
.../internal/explicit_fields.go | 116 ++++
.../internal/explicit_fields_test.go | 497 ++++++++++++++++++
.../internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
.../with-wire-tests/internal/http.go | 71 +++
.../with-wire-tests/internal/query.go | 353 +++++++++++++
.../with-wire-tests/internal/query_test.go | 395 ++++++++++++++
.../with-wire-tests/internal/retrier.go | 230 ++++++++
.../with-wire-tests/internal/retrier_test.go | 300 +++++++++++
.../with-wire-tests/internal/streamer.go | 118 +++++
.../with-wire-tests/internal/stringer.go | 13 +
.../with-wire-tests/internal/time.go | 137 +++++
.../with-wire-tests/option/request_option.go | 64 +++
.../with-wire-tests/pointer.go | 132 +++++
.../with-wire-tests/reference.md | 48 ++
.../with-wire-tests/snippet.json | 15 +
.../wiremock/docker-compose.test.yml | 8 +
.../wiremock/wiremock-mappings.json | 1 +
.../with-wire-tests/.fern/metadata.json | 12 +
.../with-wire-tests/.github/workflows/ci.yml | 35 ++
.../with-wire-tests/README.md | 193 +++++++
.../with-wire-tests/client/client.go | 33 ++
.../with-wire-tests/client/client_test.go | 45 ++
.../with-wire-tests/completions.go | 129 +++++
.../with-wire-tests/completions/client.go | 71 +++
.../completions_test/completions_test.go | 84 +++
.../with-wire-tests/completions/raw_client.go | 27 +
.../with-wire-tests/core/api_error.go | 47 ++
.../with-wire-tests/core/http.go | 15 +
.../with-wire-tests/core/request_option.go | 109 ++++
.../with-wire-tests/core/stream.go | 368 +++++++++++++
.../dynamic-snippets/example0/snippet.go | 23 +
.../with-wire-tests/error_codes.go | 9 +
.../with-wire-tests/file_param.go | 41 ++
.../server-sent-events/with-wire-tests/go.mod | 16 +
.../server-sent-events/with-wire-tests/go.sum | 12 +
.../with-wire-tests/internal/caller.go | 250 +++++++++
.../with-wire-tests/internal/caller_test.go | 395 ++++++++++++++
.../with-wire-tests/internal/error_decoder.go | 64 +++
.../internal/error_decoder_test.go | 59 +++
.../internal/explicit_fields.go | 116 ++++
.../internal/explicit_fields_test.go | 497 ++++++++++++++++++
.../internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
.../with-wire-tests/internal/http.go | 71 +++
.../with-wire-tests/internal/query.go | 353 +++++++++++++
.../with-wire-tests/internal/query_test.go | 395 ++++++++++++++
.../with-wire-tests/internal/retrier.go | 230 ++++++++
.../with-wire-tests/internal/retrier_test.go | 300 +++++++++++
.../with-wire-tests/internal/streamer.go | 118 +++++
.../with-wire-tests/internal/stringer.go | 13 +
.../with-wire-tests/internal/time.go | 137 +++++
.../with-wire-tests/option/request_option.go | 64 +++
.../with-wire-tests/pointer.go | 132 +++++
.../with-wire-tests/reference.md | 48 ++
.../with-wire-tests/snippet.json | 15 +
.../wiremock/docker-compose.test.yml | 8 +
.../wiremock/wiremock-mappings.json | 1 +
seed/go-sdk/simple-api/internal/query.go | 3 +
seed/go-sdk/simple-api/internal/query_test.go | 21 +
seed/go-sdk/simple-fhir/internal/query.go | 3 +
.../go-sdk/simple-fhir/internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/streaming/.fern/metadata.json | 12 +
.../go-sdk/streaming/.github/workflows/ci.yml | 35 ++
seed/go-sdk/streaming/client/client.go | 33 ++
seed/go-sdk/streaming/client/client_test.go | 45 ++
seed/go-sdk/streaming/core/api_error.go | 47 ++
seed/go-sdk/streaming/core/http.go | 15 +
seed/go-sdk/streaming/core/request_option.go | 109 ++++
seed/go-sdk/streaming/core/stream.go | 368 +++++++++++++
seed/go-sdk/streaming/dummy/client.go | 83 +++
seed/go-sdk/streaming/dummy/raw_client.go | 72 +++
.../dynamic-snippets/example0/snippet.go | 23 +
.../dynamic-snippets/example1/snippet.go | 23 +
.../dynamic-snippets/example2/snippet.go | 23 +
seed/go-sdk/streaming/internal/caller.go | 250 +++++++++
seed/go-sdk/streaming/internal/caller_test.go | 395 ++++++++++++++
.../streaming/internal/error_decoder.go | 64 +++
.../streaming/internal/error_decoder_test.go | 59 +++
.../streaming/internal/explicit_fields.go | 116 ++++
.../internal/explicit_fields_test.go | 497 ++++++++++++++++++
.../streaming/internal/extra_properties.go | 141 +++++
.../internal/extra_properties_test.go | 228 ++++++++
seed/go-sdk/streaming/internal/http.go | 71 +++
seed/go-sdk/streaming/internal/query.go | 353 +++++++++++++
seed/go-sdk/streaming/internal/query_test.go | 395 ++++++++++++++
seed/go-sdk/streaming/internal/retrier.go | 230 ++++++++
.../go-sdk/streaming/internal/retrier_test.go | 300 +++++++++++
seed/go-sdk/streaming/internal/streamer.go | 118 +++++
seed/go-sdk/streaming/internal/stringer.go | 13 +
seed/go-sdk/streaming/internal/time.go | 137 +++++
.../go-sdk/streaming/option/request_option.go | 64 +++
.../internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
.../v0/internal/query.go | 3 +
.../v0/internal/query_test.go | 21 +
.../unions-with-local-date/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/unions-with-local-date/types.go | 72 +--
.../unions/no-custom-config/internal/query.go | 3 +
.../no-custom-config/internal/query_test.go | 21 +
seed/go-sdk/unions/no-custom-config/types.go | 72 +--
.../unions/package-name/internal/query.go | 3 +
.../package-name/internal/query_test.go | 21 +
seed/go-sdk/unions/package-name/types.go | 72 +--
seed/go-sdk/unions/v0/internal/query.go | 3 +
seed/go-sdk/unions/v0/internal/query_test.go | 21 +
seed/go-sdk/unions/v0/types.go | 72 +--
seed/go-sdk/unknown/internal/query.go | 3 +
seed/go-sdk/unknown/internal/query_test.go | 21 +
.../go-sdk/url-form-encoded/internal/query.go | 3 +
.../url-form-encoded/internal/query_test.go | 21 +
seed/go-sdk/validation/internal/query.go | 3 +
seed/go-sdk/validation/internal/query_test.go | 21 +
seed/go-sdk/variables/internal/query.go | 3 +
seed/go-sdk/variables/internal/query_test.go | 21 +
.../version-no-default/internal/query.go | 3 +
.../version-no-default/internal/query_test.go | 21 +
seed/go-sdk/version/internal/query.go | 3 +
seed/go-sdk/version/internal/query_test.go | 21 +
.../websocket-bearer-auth/internal/query.go | 3 +
.../internal/query_test.go | 21 +
.../websocket-inferred-auth/internal/query.go | 3 +
.../internal/query_test.go | 21 +
seed/go-sdk/websocket/internal/query.go | 3 +
seed/go-sdk/websocket/internal/query_test.go | 21 +
444 files changed, 26804 insertions(+), 570 deletions(-)
create mode 100644 seed/go-model/idempotency-headers/.fern/metadata.json
create mode 100644 seed/go-model/idempotency-headers/internal/extra_properties.go
create mode 100644 seed/go-model/idempotency-headers/internal/extra_properties_test.go
create mode 100644 seed/go-model/idempotency-headers/internal/stringer.go
create mode 100644 seed/go-model/idempotency-headers/internal/time.go
create mode 100644 seed/go-model/reserved-keywords/.fern/metadata.json
create mode 100644 seed/go-model/reserved-keywords/doc.go
create mode 100644 seed/go-model/reserved-keywords/go.sum
delete mode 100644 seed/go-model/reserved-keywords/snippet-templates.json
create mode 100644 seed/go-model/streaming/.fern/metadata.json
create mode 100644 seed/go-model/streaming/internal/extra_properties.go
create mode 100644 seed/go-model/streaming/internal/extra_properties_test.go
create mode 100644 seed/go-model/streaming/internal/stringer.go
create mode 100644 seed/go-model/streaming/internal/time.go
create mode 100644 seed/go-sdk/idempotency-headers/.fern/metadata.json
create mode 100644 seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
create mode 100644 seed/go-sdk/idempotency-headers/client/client.go
create mode 100644 seed/go-sdk/idempotency-headers/client/client_test.go
create mode 100644 seed/go-sdk/idempotency-headers/client/options.go
create mode 100644 seed/go-sdk/idempotency-headers/core/api_error.go
create mode 100644 seed/go-sdk/idempotency-headers/core/http.go
create mode 100644 seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
create mode 100644 seed/go-sdk/idempotency-headers/core/request_option.go
create mode 100644 seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
create mode 100644 seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/caller.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/caller_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/error_decoder.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/explicit_fields.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/extra_properties.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/http.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/query.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/query_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/retrier.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/retrier_test.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/stringer.go
create mode 100644 seed/go-sdk/idempotency-headers/internal/time.go
create mode 100644 seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
create mode 100644 seed/go-sdk/idempotency-headers/option/request_option.go
create mode 100644 seed/go-sdk/idempotency-headers/payment/client.go
create mode 100644 seed/go-sdk/idempotency-headers/payment/raw_client.go
create mode 100644 seed/go-sdk/reserved-keywords/.fern/metadata.json
create mode 100644 seed/go-sdk/reserved-keywords/.github/workflows/ci.yml
create mode 100644 seed/go-sdk/reserved-keywords/README.md
create mode 100644 seed/go-sdk/reserved-keywords/client/client.go
create mode 100644 seed/go-sdk/reserved-keywords/client/client_test.go
create mode 100644 seed/go-sdk/reserved-keywords/core/api_error.go
create mode 100644 seed/go-sdk/reserved-keywords/core/http.go
create mode 100644 seed/go-sdk/reserved-keywords/core/request_option.go
create mode 100644 seed/go-sdk/reserved-keywords/dynamic-snippets/example0/snippet.go
create mode 100644 seed/go-sdk/reserved-keywords/error_codes.go
create mode 100644 seed/go-sdk/reserved-keywords/file_param.go
create mode 100644 seed/go-sdk/reserved-keywords/go.mod
create mode 100644 seed/go-sdk/reserved-keywords/go.sum
create mode 100644 seed/go-sdk/reserved-keywords/internal/caller.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/caller_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/error_decoder.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/error_decoder_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/explicit_fields.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/explicit_fields_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/extra_properties.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/extra_properties_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/http.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/query.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/query_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/retrier.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/retrier_test.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/stringer.go
create mode 100644 seed/go-sdk/reserved-keywords/internal/time.go
create mode 100644 seed/go-sdk/reserved-keywords/option/request_option.go
create mode 100644 seed/go-sdk/reserved-keywords/package.go
create mode 100644 seed/go-sdk/reserved-keywords/package_/client.go
create mode 100644 seed/go-sdk/reserved-keywords/package_/raw_client.go
create mode 100644 seed/go-sdk/reserved-keywords/pointer.go
create mode 100644 seed/go-sdk/reserved-keywords/reference.md
create mode 100644 seed/go-sdk/reserved-keywords/snippet.json
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/.fern/metadata.json
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/.github/workflows/ci.yml
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/README.md
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/completions.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/client.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/completions_test/completions_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/raw_client.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/core/api_error.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/core/http.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/core/request_option.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/core/stream.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example0/snippet.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example1/snippet.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/error_codes.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/file_param.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/go.mod
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/go.sum
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/http.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier_test.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/streamer.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/stringer.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/time.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/option/request_option.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/pointer.go
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/reference.md
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/snippet.json
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/docker-compose.test.yml
create mode 100644 seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/wiremock-mappings.json
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/.fern/metadata.json
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/.github/workflows/ci.yml
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/README.md
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/client/client.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/client/client_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/completions.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/completions/client.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/completions/completions_test/completions_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/completions/raw_client.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/core/api_error.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/core/http.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/core/request_option.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/core/stream.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/dynamic-snippets/example0/snippet.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/error_codes.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/file_param.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/go.mod
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/go.sum
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/caller.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/caller_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/http.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/query.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/query_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier_test.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/streamer.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/stringer.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/internal/time.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/option/request_option.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/pointer.go
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/reference.md
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/snippet.json
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/wiremock/docker-compose.test.yml
create mode 100644 seed/go-sdk/server-sent-events/with-wire-tests/wiremock/wiremock-mappings.json
create mode 100644 seed/go-sdk/streaming/.fern/metadata.json
create mode 100644 seed/go-sdk/streaming/.github/workflows/ci.yml
create mode 100644 seed/go-sdk/streaming/client/client.go
create mode 100644 seed/go-sdk/streaming/client/client_test.go
create mode 100644 seed/go-sdk/streaming/core/api_error.go
create mode 100644 seed/go-sdk/streaming/core/http.go
create mode 100644 seed/go-sdk/streaming/core/request_option.go
create mode 100644 seed/go-sdk/streaming/core/stream.go
create mode 100644 seed/go-sdk/streaming/dummy/client.go
create mode 100644 seed/go-sdk/streaming/dummy/raw_client.go
create mode 100644 seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
create mode 100644 seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
create mode 100644 seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
create mode 100644 seed/go-sdk/streaming/internal/caller.go
create mode 100644 seed/go-sdk/streaming/internal/caller_test.go
create mode 100644 seed/go-sdk/streaming/internal/error_decoder.go
create mode 100644 seed/go-sdk/streaming/internal/error_decoder_test.go
create mode 100644 seed/go-sdk/streaming/internal/explicit_fields.go
create mode 100644 seed/go-sdk/streaming/internal/explicit_fields_test.go
create mode 100644 seed/go-sdk/streaming/internal/extra_properties.go
create mode 100644 seed/go-sdk/streaming/internal/extra_properties_test.go
create mode 100644 seed/go-sdk/streaming/internal/http.go
create mode 100644 seed/go-sdk/streaming/internal/query.go
create mode 100644 seed/go-sdk/streaming/internal/query_test.go
create mode 100644 seed/go-sdk/streaming/internal/retrier.go
create mode 100644 seed/go-sdk/streaming/internal/retrier_test.go
create mode 100644 seed/go-sdk/streaming/internal/streamer.go
create mode 100644 seed/go-sdk/streaming/internal/stringer.go
create mode 100644 seed/go-sdk/streaming/internal/time.go
create mode 100644 seed/go-sdk/streaming/option/request_option.go
diff --git a/seed/go-model/examples/commons/types.go b/seed/go-model/examples/commons/types.go
index 9f260dce10e1..522589f10bb3 100644
--- a/seed/go-model/examples/commons/types.go
+++ b/seed/go-model/examples/commons/types.go
@@ -84,7 +84,7 @@ type EventInfo struct {
}
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
diff --git a/seed/go-model/exhaustive/types/docs.go b/seed/go-model/exhaustive/types/docs.go
index 9c83f2e8a24a..102392e92688 100644
--- a/seed/go-model/exhaustive/types/docs.go
+++ b/seed/go-model/exhaustive/types/docs.go
@@ -68,17 +68,17 @@ type ObjectWithDocs struct {
// - ** /: PHPDoc comment end
// - *: Can interfere with comment blocks
// - &: HTML entities
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
extraProperties map[string]any
rawJSON json.RawMessage
}
-func (o *ObjectWithDocs) GetString() string {
+func (o *ObjectWithDocs) GetFieldString() string {
if o == nil {
return ""
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithDocs) GetExtraProperties() map[string]any {
diff --git a/seed/go-model/exhaustive/types/object.go b/seed/go-model/exhaustive/types/object.go
index 2d55cbfc012b..51f6dd8284f9 100644
--- a/seed/go-model/exhaustive/types/object.go
+++ b/seed/go-model/exhaustive/types/object.go
@@ -12,29 +12,29 @@ import (
type ObjectWithOptionalField struct {
// This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered.
- String *string `json:"string,omitempty" url:"string,omitempty"`
- Integer *int `json:"integer,omitempty" url:"integer,omitempty"`
- Long *int64 `json:"long,omitempty" url:"long,omitempty"`
- Double *float64 `json:"double,omitempty" url:"double,omitempty"`
- Bool *bool `json:"bool,omitempty" url:"bool,omitempty"`
- Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"`
- Date *time.Time `json:"date,omitempty" url:"date,omitempty"`
- Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"`
- Base64 []byte `json:"base64,omitempty" url:"base64,omitempty"`
- List []string `json:"list,omitempty" url:"list,omitempty"`
- Set []string `json:"set,omitempty" url:"set,omitempty"`
- Map map[int]string `json:"map,omitempty" url:"map,omitempty"`
- Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"`
+ FieldString *string `json:"string,omitempty" url:"string,omitempty"`
+ Integer *int `json:"integer,omitempty" url:"integer,omitempty"`
+ Long *int64 `json:"long,omitempty" url:"long,omitempty"`
+ Double *float64 `json:"double,omitempty" url:"double,omitempty"`
+ Bool *bool `json:"bool,omitempty" url:"bool,omitempty"`
+ Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"`
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty"`
+ Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"`
+ Base64 []byte `json:"base64,omitempty" url:"base64,omitempty"`
+ List []string `json:"list,omitempty" url:"list,omitempty"`
+ Set []string `json:"set,omitempty" url:"set,omitempty"`
+ Map map[int]string `json:"map,omitempty" url:"map,omitempty"`
+ Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"`
extraProperties map[string]any
rawJSON json.RawMessage
}
-func (o *ObjectWithOptionalField) GetString() *string {
+func (o *ObjectWithOptionalField) GetFieldString() *string {
if o == nil {
return nil
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithOptionalField) GetInteger() *int {
@@ -181,17 +181,17 @@ func (o *ObjectWithOptionalField) String() string {
}
type ObjectWithRequiredField struct {
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
extraProperties map[string]any
rawJSON json.RawMessage
}
-func (o *ObjectWithRequiredField) GetString() string {
+func (o *ObjectWithRequiredField) GetFieldString() string {
if o == nil {
return ""
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithRequiredField) GetExtraProperties() map[string]any {
@@ -283,18 +283,18 @@ func (o *ObjectWithMapOfMap) String() string {
}
type NestedObjectWithOptionalField struct {
- String *string `json:"string,omitempty" url:"string,omitempty"`
+ FieldString *string `json:"string,omitempty" url:"string,omitempty"`
NestedObject *ObjectWithOptionalField `json:"NestedObject,omitempty" url:"NestedObject,omitempty"`
extraProperties map[string]any
rawJSON json.RawMessage
}
-func (n *NestedObjectWithOptionalField) GetString() *string {
+func (n *NestedObjectWithOptionalField) GetFieldString() *string {
if n == nil {
return nil
}
- return n.String
+ return n.FieldString
}
func (n *NestedObjectWithOptionalField) GetNestedObject() *ObjectWithOptionalField {
@@ -342,18 +342,18 @@ func (n *NestedObjectWithOptionalField) String() string {
}
type NestedObjectWithRequiredField struct {
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
NestedObject *ObjectWithOptionalField `json:"NestedObject" url:"NestedObject"`
extraProperties map[string]any
rawJSON json.RawMessage
}
-func (n *NestedObjectWithRequiredField) GetString() string {
+func (n *NestedObjectWithRequiredField) GetFieldString() string {
if n == nil {
return ""
}
- return n.String
+ return n.FieldString
}
func (n *NestedObjectWithRequiredField) GetNestedObject() *ObjectWithOptionalField {
diff --git a/seed/go-model/idempotency-headers/.fern/metadata.json b/seed/go-model/idempotency-headers/.fern/metadata.json
new file mode 100644
index 000000000000..52154345880d
--- /dev/null
+++ b/seed/go-model/idempotency-headers/.fern/metadata.json
@@ -0,0 +1,12 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-model",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "packageName": "fern",
+ "module": {
+ "path": "github.com/idempotency-headers/fern"
+ },
+ "includeLegacyClientOptions": true
+ }
+}
\ No newline at end of file
diff --git a/seed/go-model/idempotency-headers/internal/extra_properties.go b/seed/go-model/idempotency-headers/internal/extra_properties.go
new file mode 100644
index 000000000000..57517691f132
--- /dev/null
+++ b/seed/go-model/idempotency-headers/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler any, key string, value any) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]any{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler any, extraProperties map[string]any) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value any, exclude ...string) (map[string]any, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]any
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value any) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-model/idempotency-headers/internal/extra_properties_test.go b/seed/go-model/idempotency-headers/internal/extra_properties_test.go
new file mode 100644
index 000000000000..0d46257763fb
--- /dev/null
+++ b/seed/go-model/idempotency-headers/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler any
+ giveExtraProperties map[string]any
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]any{42: "value"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]any{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]any{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]any{},
+ giveExtraProperties: map[string]any{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]any{},
+ giveExtraProperties: map[string]any{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{
+ "user": map[string]any{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{
+ "metadata": map[string]any{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]any{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]any{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]any)
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-model/idempotency-headers/internal/stringer.go b/seed/go-model/idempotency-headers/internal/stringer.go
new file mode 100644
index 000000000000..0be54d1b5359
--- /dev/null
+++ b/seed/go-model/idempotency-headers/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value any) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-model/idempotency-headers/internal/time.go b/seed/go-model/idempotency-headers/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-model/idempotency-headers/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-model/reserved-keywords/.fern/metadata.json b/seed/go-model/reserved-keywords/.fern/metadata.json
new file mode 100644
index 000000000000..508350d068b1
--- /dev/null
+++ b/seed/go-model/reserved-keywords/.fern/metadata.json
@@ -0,0 +1,5 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-model",
+ "generatorVersion": "latest"
+}
\ No newline at end of file
diff --git a/seed/go-model/reserved-keywords/doc.go b/seed/go-model/reserved-keywords/doc.go
new file mode 100644
index 000000000000..4ec8dbdf77c0
--- /dev/null
+++ b/seed/go-model/reserved-keywords/doc.go
@@ -0,0 +1 @@
+package nurseryapi
\ No newline at end of file
diff --git a/seed/go-model/reserved-keywords/go.mod b/seed/go-model/reserved-keywords/go.mod
index b82d6802c4f7..6782928d3dd7 100644
--- a/seed/go-model/reserved-keywords/go.mod
+++ b/seed/go-model/reserved-keywords/go.mod
@@ -1,5 +1,14 @@
module github.com/reserved-keywords/fern
-go 1.18
-require github.com/google/uuid v1.4.0
-require github.com/stretchr/testify v1.7.0
-require gopkg.in/yaml.v3 v3.0.1
\ No newline at end of file
+
+go 1.21
+
+toolchain go1.23.8
+
+require github.com/stretchr/testify v1.8.4
+
+require gopkg.in/yaml.v3 v3.0.1 // indirect
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+)
diff --git a/seed/go-model/reserved-keywords/go.sum b/seed/go-model/reserved-keywords/go.sum
new file mode 100644
index 000000000000..fa4b6e6825c4
--- /dev/null
+++ b/seed/go-model/reserved-keywords/go.sum
@@ -0,0 +1,10 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/seed/go-model/reserved-keywords/package.go b/seed/go-model/reserved-keywords/package.go
index cd49c3020ea3..7d324d8c9313 100644
--- a/seed/go-model/reserved-keywords/package.go
+++ b/seed/go-model/reserved-keywords/package.go
@@ -3,120 +3,117 @@
package nurseryapi
import (
- json "encoding/json"
- internal "github.com/reserved-keywords/fern/internal"
- fmt "fmt"
+ json "encoding/json"
+ fmt "fmt"
+ internal "github.com/reserved-keywords/fern/internal"
)
-
type Package struct {
- Name string `json:"name" url:"name"`
+ Name string `json:"name" url:"name"`
- extraProperties map[string]any
- rawJSON json.RawMessage
+ extraProperties map[string]any
+ rawJSON json.RawMessage
}
-func (p *Package) GetName() string{
- if p == nil {
- return ""
- }
- return p.Name
+func (p *Package) GetName() string {
+ if p == nil {
+ return ""
+ }
+ return p.Name
}
-func (p *Package) GetExtraProperties() map[string]any{
- if p == nil {
- return nil
- }
- return p.extraProperties
+func (p *Package) GetExtraProperties() map[string]any {
+ if p == nil {
+ return nil
+ }
+ return p.extraProperties
}
func (p *Package) UnmarshalJSON(
- data []byte,
-) error{
- type unmarshaler Package
- var value unmarshaler
- if err := json.Unmarshal(data, &value); err != nil {
- return err
- }
- *p = Package(value)
- extraProperties, err := internal.ExtractExtraProperties(data, *p)
- if err != nil {
- return err
- }
- p.extraProperties = extraProperties
- p.rawJSON = json.RawMessage(data)
- return nil
+ data []byte,
+) error {
+ type unmarshaler Package
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *p = Package(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *p)
+ if err != nil {
+ return err
+ }
+ p.extraProperties = extraProperties
+ p.rawJSON = json.RawMessage(data)
+ return nil
}
-func (p *Package) String() string{
- if len(p.rawJSON) > 0 {
- if value, err := internal.StringifyJSON(p.rawJSON); err == nil {
- return value
- }
- }
- if value, err := internal.StringifyJSON(p); err == nil {
- return value
- }
- return fmt.Sprintf("%#v", p)
+func (p *Package) String() string {
+ if len(p.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(p.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(p); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", p)
}
-
type Record struct {
- Foo map[string]string `json:"foo" url:"foo"`
- 3D int `json:"3d" url:"3d"`
+ Foo map[string]string `json:"foo" url:"foo"`
+ Field3D int `json:"3d" url:"3d"`
- extraProperties map[string]any
- rawJSON json.RawMessage
+ extraProperties map[string]any
+ rawJSON json.RawMessage
}
-func (r *Record) GetFoo() map[string]string{
- if r == nil {
- return nil
- }
- return r.Foo
+func (r *Record) GetFoo() map[string]string {
+ if r == nil {
+ return nil
+ }
+ return r.Foo
}
-func (r *Record) Get3D() int{
- if r == nil {
- return 0
- }
- return r.3D
+func (r *Record) GetField3D() int {
+ if r == nil {
+ return 0
+ }
+ return r.Field3D
}
-func (r *Record) GetExtraProperties() map[string]any{
- if r == nil {
- return nil
- }
- return r.extraProperties
+func (r *Record) GetExtraProperties() map[string]any {
+ if r == nil {
+ return nil
+ }
+ return r.extraProperties
}
func (r *Record) UnmarshalJSON(
- data []byte,
-) error{
- type unmarshaler Record
- var value unmarshaler
- if err := json.Unmarshal(data, &value); err != nil {
- return err
- }
- *r = Record(value)
- extraProperties, err := internal.ExtractExtraProperties(data, *r)
- if err != nil {
- return err
- }
- r.extraProperties = extraProperties
- r.rawJSON = json.RawMessage(data)
- return nil
+ data []byte,
+) error {
+ type unmarshaler Record
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *r = Record(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *r)
+ if err != nil {
+ return err
+ }
+ r.extraProperties = extraProperties
+ r.rawJSON = json.RawMessage(data)
+ return nil
}
-func (r *Record) String() string{
- if len(r.rawJSON) > 0 {
- if value, err := internal.StringifyJSON(r.rawJSON); err == nil {
- return value
- }
- }
- if value, err := internal.StringifyJSON(r); err == nil {
- return value
- }
- return fmt.Sprintf("%#v", r)
+func (r *Record) String() string {
+ if len(r.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(r.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(r); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", r)
}
-
diff --git a/seed/go-model/reserved-keywords/snippet-templates.json b/seed/go-model/reserved-keywords/snippet-templates.json
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/seed/go-model/streaming/.fern/metadata.json b/seed/go-model/streaming/.fern/metadata.json
new file mode 100644
index 000000000000..0feece7292ec
--- /dev/null
+++ b/seed/go-model/streaming/.fern/metadata.json
@@ -0,0 +1,11 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-model",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "packageName": "stream",
+ "module": {
+ "path": "github.com/fern-api/stream-go"
+ }
+ }
+}
\ No newline at end of file
diff --git a/seed/go-model/streaming/internal/extra_properties.go b/seed/go-model/streaming/internal/extra_properties.go
new file mode 100644
index 000000000000..57517691f132
--- /dev/null
+++ b/seed/go-model/streaming/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler any, key string, value any) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]any{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler any, extraProperties map[string]any) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value any, exclude ...string) (map[string]any, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]any
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value any) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-model/streaming/internal/extra_properties_test.go b/seed/go-model/streaming/internal/extra_properties_test.go
new file mode 100644
index 000000000000..0d46257763fb
--- /dev/null
+++ b/seed/go-model/streaming/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler any
+ giveExtraProperties map[string]any
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]any{42: "value"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]any{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]any{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]any{},
+ giveExtraProperties: map[string]any{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]any{},
+ giveExtraProperties: map[string]any{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{
+ "user": map[string]any{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]any{"key": "value"},
+ giveExtraProperties: map[string]any{
+ "metadata": map[string]any{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]any{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]any{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]any)
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-model/streaming/internal/stringer.go b/seed/go-model/streaming/internal/stringer.go
new file mode 100644
index 000000000000..0be54d1b5359
--- /dev/null
+++ b/seed/go-model/streaming/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value any) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-model/streaming/internal/time.go b/seed/go-model/streaming/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-model/streaming/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-model/unions-with-local-date/types.go b/seed/go-model/unions-with-local-date/types.go
index af6cab627082..49579bf3fad3 100644
--- a/seed/go-model/unions-with-local-date/types.go
+++ b/seed/go-model/unions-with-local-date/types.go
@@ -24,9 +24,9 @@ type UnionWithDiscriminant struct {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
type UnionWithDuplicatePrimitive struct {
@@ -63,11 +63,11 @@ type UnionWithLiteral struct {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo Foo
}
type UnionWithTime struct {
diff --git a/seed/go-model/unions/types.go b/seed/go-model/unions/types.go
index 10bb0ca268a6..3dbbe65a26d3 100644
--- a/seed/go-model/unions/types.go
+++ b/seed/go-model/unions/types.go
@@ -24,9 +24,9 @@ type UnionWithDiscriminant struct {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
type UnionWithDuplicatePrimitive struct {
@@ -63,11 +63,11 @@ type UnionWithLiteral struct {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo Foo
}
type UnionWithTime struct {
diff --git a/seed/go-sdk/accept-header/internal/query.go b/seed/go-sdk/accept-header/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/accept-header/internal/query.go
+++ b/seed/go-sdk/accept-header/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/accept-header/internal/query_test.go b/seed/go-sdk/accept-header/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/accept-header/internal/query_test.go
+++ b/seed/go-sdk/accept-header/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/alias/internal/query.go b/seed/go-sdk/alias/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/alias/internal/query.go
+++ b/seed/go-sdk/alias/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/alias/internal/query_test.go b/seed/go-sdk/alias/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/alias/internal/query_test.go
+++ b/seed/go-sdk/alias/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/any-auth/internal/query.go b/seed/go-sdk/any-auth/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/any-auth/internal/query.go
+++ b/seed/go-sdk/any-auth/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/any-auth/internal/query_test.go b/seed/go-sdk/any-auth/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/any-auth/internal/query_test.go
+++ b/seed/go-sdk/any-auth/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/api-wide-base-path/internal/query.go b/seed/go-sdk/api-wide-base-path/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/api-wide-base-path/internal/query.go
+++ b/seed/go-sdk/api-wide-base-path/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/api-wide-base-path/internal/query_test.go b/seed/go-sdk/api-wide-base-path/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/api-wide-base-path/internal/query_test.go
+++ b/seed/go-sdk/api-wide-base-path/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/audiences/internal/query.go b/seed/go-sdk/audiences/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/audiences/internal/query.go
+++ b/seed/go-sdk/audiences/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/audiences/internal/query_test.go b/seed/go-sdk/audiences/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/audiences/internal/query_test.go
+++ b/seed/go-sdk/audiences/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/basic-auth-environment-variables/internal/query.go b/seed/go-sdk/basic-auth-environment-variables/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/basic-auth-environment-variables/internal/query.go
+++ b/seed/go-sdk/basic-auth-environment-variables/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/basic-auth-environment-variables/internal/query_test.go b/seed/go-sdk/basic-auth-environment-variables/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/basic-auth-environment-variables/internal/query_test.go
+++ b/seed/go-sdk/basic-auth-environment-variables/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/basic-auth/internal/query.go b/seed/go-sdk/basic-auth/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/basic-auth/internal/query.go
+++ b/seed/go-sdk/basic-auth/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/basic-auth/internal/query_test.go b/seed/go-sdk/basic-auth/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/basic-auth/internal/query_test.go
+++ b/seed/go-sdk/basic-auth/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/bearer-token-environment-variable/internal/query.go b/seed/go-sdk/bearer-token-environment-variable/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/bearer-token-environment-variable/internal/query.go
+++ b/seed/go-sdk/bearer-token-environment-variable/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/bearer-token-environment-variable/internal/query_test.go b/seed/go-sdk/bearer-token-environment-variable/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/bearer-token-environment-variable/internal/query_test.go
+++ b/seed/go-sdk/bearer-token-environment-variable/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/bytes-upload/internal/query.go b/seed/go-sdk/bytes-upload/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/bytes-upload/internal/query.go
+++ b/seed/go-sdk/bytes-upload/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/bytes-upload/internal/query_test.go b/seed/go-sdk/bytes-upload/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/bytes-upload/internal/query_test.go
+++ b/seed/go-sdk/bytes-upload/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/circular-references-advanced/internal/query.go b/seed/go-sdk/circular-references-advanced/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/circular-references-advanced/internal/query.go
+++ b/seed/go-sdk/circular-references-advanced/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/circular-references-advanced/internal/query_test.go b/seed/go-sdk/circular-references-advanced/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/circular-references-advanced/internal/query_test.go
+++ b/seed/go-sdk/circular-references-advanced/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/circular-references/internal/query.go b/seed/go-sdk/circular-references/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/circular-references/internal/query.go
+++ b/seed/go-sdk/circular-references/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/circular-references/internal/query_test.go b/seed/go-sdk/circular-references/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/circular-references/internal/query_test.go
+++ b/seed/go-sdk/circular-references/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/client-side-params/internal/query.go b/seed/go-sdk/client-side-params/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/client-side-params/internal/query.go
+++ b/seed/go-sdk/client-side-params/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/client-side-params/internal/query_test.go b/seed/go-sdk/client-side-params/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/client-side-params/internal/query_test.go
+++ b/seed/go-sdk/client-side-params/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/content-type/internal/query.go b/seed/go-sdk/content-type/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/content-type/internal/query.go
+++ b/seed/go-sdk/content-type/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/content-type/internal/query_test.go b/seed/go-sdk/content-type/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/content-type/internal/query_test.go
+++ b/seed/go-sdk/content-type/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/cross-package-type-names/internal/query.go b/seed/go-sdk/cross-package-type-names/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/cross-package-type-names/internal/query.go
+++ b/seed/go-sdk/cross-package-type-names/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/cross-package-type-names/internal/query_test.go b/seed/go-sdk/cross-package-type-names/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/cross-package-type-names/internal/query_test.go
+++ b/seed/go-sdk/cross-package-type-names/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/empty-clients/internal/query.go b/seed/go-sdk/empty-clients/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/empty-clients/internal/query.go
+++ b/seed/go-sdk/empty-clients/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/empty-clients/internal/query_test.go b/seed/go-sdk/empty-clients/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/empty-clients/internal/query_test.go
+++ b/seed/go-sdk/empty-clients/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/enum/internal/query.go b/seed/go-sdk/enum/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/enum/internal/query.go
+++ b/seed/go-sdk/enum/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/enum/internal/query_test.go b/seed/go-sdk/enum/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/enum/internal/query_test.go
+++ b/seed/go-sdk/enum/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/error-property/internal/query.go b/seed/go-sdk/error-property/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/error-property/internal/query.go
+++ b/seed/go-sdk/error-property/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/error-property/internal/query_test.go b/seed/go-sdk/error-property/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/error-property/internal/query_test.go
+++ b/seed/go-sdk/error-property/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/errors/internal/query.go b/seed/go-sdk/errors/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/errors/internal/query.go
+++ b/seed/go-sdk/errors/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/errors/internal/query_test.go b/seed/go-sdk/errors/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/errors/internal/query_test.go
+++ b/seed/go-sdk/errors/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/always-send-required-properties/commons/types.go b/seed/go-sdk/examples/always-send-required-properties/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/always-send-required-properties/commons/types.go
+++ b/seed/go-sdk/examples/always-send-required-properties/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/always-send-required-properties/internal/query.go b/seed/go-sdk/examples/always-send-required-properties/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/always-send-required-properties/internal/query.go
+++ b/seed/go-sdk/examples/always-send-required-properties/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/always-send-required-properties/internal/query_test.go b/seed/go-sdk/examples/always-send-required-properties/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/always-send-required-properties/internal/query_test.go
+++ b/seed/go-sdk/examples/always-send-required-properties/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/client-name-with-custom-constructor-name/commons/types.go b/seed/go-sdk/examples/client-name-with-custom-constructor-name/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/client-name-with-custom-constructor-name/commons/types.go
+++ b/seed/go-sdk/examples/client-name-with-custom-constructor-name/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query.go b/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query.go
+++ b/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query_test.go b/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query_test.go
+++ b/seed/go-sdk/examples/client-name-with-custom-constructor-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/client-name/commons/types.go b/seed/go-sdk/examples/client-name/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/client-name/commons/types.go
+++ b/seed/go-sdk/examples/client-name/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/client-name/internal/query.go b/seed/go-sdk/examples/client-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/client-name/internal/query.go
+++ b/seed/go-sdk/examples/client-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/client-name/internal/query_test.go b/seed/go-sdk/examples/client-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/client-name/internal/query_test.go
+++ b/seed/go-sdk/examples/client-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/export-all-requests-at-root/commons/types.go b/seed/go-sdk/examples/export-all-requests-at-root/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/export-all-requests-at-root/commons/types.go
+++ b/seed/go-sdk/examples/export-all-requests-at-root/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/export-all-requests-at-root/internal/query.go b/seed/go-sdk/examples/export-all-requests-at-root/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/export-all-requests-at-root/internal/query.go
+++ b/seed/go-sdk/examples/export-all-requests-at-root/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/export-all-requests-at-root/internal/query_test.go b/seed/go-sdk/examples/export-all-requests-at-root/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/export-all-requests-at-root/internal/query_test.go
+++ b/seed/go-sdk/examples/export-all-requests-at-root/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/exported-client-name/commons/types.go b/seed/go-sdk/examples/exported-client-name/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/exported-client-name/commons/types.go
+++ b/seed/go-sdk/examples/exported-client-name/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/exported-client-name/internal/query.go b/seed/go-sdk/examples/exported-client-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/exported-client-name/internal/query.go
+++ b/seed/go-sdk/examples/exported-client-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/exported-client-name/internal/query_test.go b/seed/go-sdk/examples/exported-client-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/exported-client-name/internal/query_test.go
+++ b/seed/go-sdk/examples/exported-client-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/getters-pass-by-value/commons/types.go b/seed/go-sdk/examples/getters-pass-by-value/commons/types.go
index d4bf6d23ae0d..09c01b70b5bf 100644
--- a/seed/go-sdk/examples/getters-pass-by-value/commons/types.go
+++ b/seed/go-sdk/examples/getters-pass-by-value/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/getters-pass-by-value/internal/query.go b/seed/go-sdk/examples/getters-pass-by-value/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/getters-pass-by-value/internal/query.go
+++ b/seed/go-sdk/examples/getters-pass-by-value/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/getters-pass-by-value/internal/query_test.go b/seed/go-sdk/examples/getters-pass-by-value/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/getters-pass-by-value/internal/query_test.go
+++ b/seed/go-sdk/examples/getters-pass-by-value/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/no-custom-config/commons/types.go b/seed/go-sdk/examples/no-custom-config/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/no-custom-config/commons/types.go
+++ b/seed/go-sdk/examples/no-custom-config/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/no-custom-config/internal/query.go b/seed/go-sdk/examples/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/no-custom-config/internal/query.go
+++ b/seed/go-sdk/examples/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/no-custom-config/internal/query_test.go b/seed/go-sdk/examples/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/examples/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/package-path/pleaseinhere/commons/types.go b/seed/go-sdk/examples/package-path/pleaseinhere/commons/types.go
index 2c782856e8c0..cd9749acf717 100644
--- a/seed/go-sdk/examples/package-path/pleaseinhere/commons/types.go
+++ b/seed/go-sdk/examples/package-path/pleaseinhere/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/package-path/pleaseinhere/internal/query.go b/seed/go-sdk/examples/package-path/pleaseinhere/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/package-path/pleaseinhere/internal/query.go
+++ b/seed/go-sdk/examples/package-path/pleaseinhere/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/package-path/pleaseinhere/internal/query_test.go b/seed/go-sdk/examples/package-path/pleaseinhere/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/package-path/pleaseinhere/internal/query_test.go
+++ b/seed/go-sdk/examples/package-path/pleaseinhere/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/readme-config/commons/types.go b/seed/go-sdk/examples/readme-config/commons/types.go
index 6c997a66df8b..065458046697 100644
--- a/seed/go-sdk/examples/readme-config/commons/types.go
+++ b/seed/go-sdk/examples/readme-config/commons/types.go
@@ -10,9 +10,9 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
func (d *Data) GetType() string {
@@ -22,11 +22,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -50,12 +50,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -72,13 +72,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
if err := d.validate(); err != nil {
return nil, err
}
- if d.String != "" {
+ if d.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
}
@@ -96,13 +96,13 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
func (d *Data) Accept(visitor DataVisitor) error {
- if d.String != "" {
- return visitor.VisitString(d.String)
+ if d.FieldString != "" {
+ return visitor.VisitFieldString(d.FieldString)
}
if d.Base64 != nil {
return visitor.VisitBase64(d.Base64)
@@ -115,7 +115,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/readme-config/internal/query.go b/seed/go-sdk/examples/readme-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/readme-config/internal/query.go
+++ b/seed/go-sdk/examples/readme-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/readme-config/internal/query_test.go b/seed/go-sdk/examples/readme-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/readme-config/internal/query_test.go
+++ b/seed/go-sdk/examples/readme-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/examples/v0/commons/types.go b/seed/go-sdk/examples/v0/commons/types.go
index db2d24fce191..2b4f32b6f8cf 100644
--- a/seed/go-sdk/examples/v0/commons/types.go
+++ b/seed/go-sdk/examples/v0/commons/types.go
@@ -10,13 +10,13 @@ import (
)
type Data struct {
- Type string
- String string
- Base64 []byte
+ Type string
+ FieldString string
+ Base64 []byte
}
-func NewDataFromString(value string) *Data {
- return &Data{Type: "string", String: value}
+func NewDataFromFieldString(value string) *Data {
+ return &Data{Type: "string", FieldString: value}
}
func NewDataFromBase64(value []byte) *Data {
@@ -30,11 +30,11 @@ func (d *Data) GetType() string {
return d.Type
}
-func (d *Data) GetString() string {
+func (d *Data) GetFieldString() string {
if d == nil {
return ""
}
- return d.String
+ return d.FieldString
}
func (d *Data) GetBase64() []byte {
@@ -58,12 +58,12 @@ func (d *Data) UnmarshalJSON(data []byte) error {
switch unmarshaler.Type {
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- d.String = valueUnmarshaler.String
+ d.FieldString = valueUnmarshaler.FieldString
case "base64":
var valueUnmarshaler struct {
Base64 []byte `json:"value"`
@@ -85,11 +85,11 @@ func (d Data) MarshalJSON() ([]byte, error) {
return nil, fmt.Errorf("invalid type %s in %T", d.Type, d)
case "string":
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: d.String,
+ Type: "string",
+ FieldString: d.FieldString,
}
return json.Marshal(marshaler)
case "base64":
@@ -105,7 +105,7 @@ func (d Data) MarshalJSON() ([]byte, error) {
}
type DataVisitor interface {
- VisitString(string) error
+ VisitFieldString(string) error
VisitBase64([]byte) error
}
@@ -114,7 +114,7 @@ func (d *Data) Accept(visitor DataVisitor) error {
default:
return fmt.Errorf("invalid type %s in %T", d.Type, d)
case "string":
- return visitor.VisitString(d.String)
+ return visitor.VisitFieldString(d.FieldString)
case "base64":
return visitor.VisitBase64(d.Base64)
}
@@ -125,7 +125,7 @@ func (d *Data) validate() error {
return fmt.Errorf("type %T is nil", d)
}
var fields []string
- if d.String != "" {
+ if d.FieldString != "" {
fields = append(fields, "string")
}
if d.Base64 != nil {
diff --git a/seed/go-sdk/examples/v0/internal/query.go b/seed/go-sdk/examples/v0/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/examples/v0/internal/query.go
+++ b/seed/go-sdk/examples/v0/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/examples/v0/internal/query_test.go b/seed/go-sdk/examples/v0/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/examples/v0/internal/query_test.go
+++ b/seed/go-sdk/examples/v0/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example1/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example1/snippet.go
index 02b89e0834c0..c649859ec738 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example1/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example1/snippet.go
@@ -18,10 +18,10 @@ func do() {
)
request := []*types.ObjectWithRequiredField{
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnListOfObjects(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example11/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example11/snippet.go
index 6f2b6948beec..7b10cb032da4 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example11/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example11/snippet.go
@@ -17,7 +17,7 @@ func do() {
),
)
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.HttpMethods.TestPost(
context.TODO(),
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example12/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example12/snippet.go
index e1d9fd859082..4d2cf5f2dd51 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example12/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example12/snippet.go
@@ -17,7 +17,7 @@ func do() {
),
)
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.HttpMethods.TestPut(
context.TODO(),
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example13/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example13/snippet.go
index 598bf2208851..6a0a20370443 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example13/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example13/snippet.go
@@ -19,7 +19,7 @@ func do() {
),
)
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example15/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example15/snippet.go
index 00dbc763f8b4..e841718dc613 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example15/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example15/snippet.go
@@ -19,7 +19,7 @@ func do() {
),
)
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example16/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example16/snippet.go
index 124f3025fb8b..23b97eb37c1e 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example16/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example16/snippet.go
@@ -17,7 +17,7 @@ func do() {
),
)
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.Object.GetAndReturnWithRequiredField(
context.TODO(),
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example18/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example18/snippet.go
index bb73d1ae9b9b..d64c06a65d2a 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example18/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example18/snippet.go
@@ -19,11 +19,11 @@ func do() {
),
)
request := &types.NestedObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example19/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example19/snippet.go
index 9bdbc9ab13a0..59439a6db5a6 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example19/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example19/snippet.go
@@ -19,9 +19,9 @@ func do() {
),
)
request := &types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example20/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example20/snippet.go
index 0a9e574df594..40377ad215cd 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example20/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example20/snippet.go
@@ -20,9 +20,9 @@ func do() {
)
request := []*types.NestedObjectWithRequiredField{
&types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -69,9 +69,9 @@ func do() {
},
},
&types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example3/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example3/snippet.go
index 6ddae696e071..22cdd3ae73a1 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example3/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example3/snippet.go
@@ -18,7 +18,7 @@ func do() {
)
request := []*types.ObjectWithRequiredField{
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnSetOfObjects(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example44/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example44/snippet.go
index 73d6c6e5acb4..a5efb86f08da 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example44/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example44/snippet.go
@@ -22,7 +22,7 @@ func do() {
String: "string",
Integer: 1,
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example45/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example45/snippet.go
index 73d6c6e5acb4..a5efb86f08da 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example45/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example45/snippet.go
@@ -22,7 +22,7 @@ func do() {
String: "string",
Integer: 1,
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example5/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example5/snippet.go
index 50088d21d7f6..b8a49448ab64 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example5/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example5/snippet.go
@@ -18,7 +18,7 @@ func do() {
)
request := map[string]*types.ObjectWithRequiredField{
"string": &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnMapOfPrimToObject(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example6/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example6/snippet.go
index 61a9283abf97..2becf4b855e9 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example6/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example6/snippet.go
@@ -17,7 +17,7 @@ func do() {
),
)
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.Container.GetAndReturnOptional(
context.TODO(),
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example7/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example7/snippet.go
index 206e86c3a552..29184abae57e 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example7/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example7/snippet.go
@@ -19,7 +19,7 @@ func do() {
),
)
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/dynamic-snippets/example8/snippet.go b/seed/go-sdk/exhaustive/dynamic-snippets/example8/snippet.go
index 6fd47590cff8..c53a12f659c2 100644
--- a/seed/go-sdk/exhaustive/dynamic-snippets/example8/snippet.go
+++ b/seed/go-sdk/exhaustive/dynamic-snippets/example8/snippet.go
@@ -19,7 +19,7 @@ func do() {
),
)
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/inlined_requests.go b/seed/go-sdk/exhaustive/inlined_requests.go
index 10911c460d1a..5e8ec73164ee 100644
--- a/seed/go-sdk/exhaustive/inlined_requests.go
+++ b/seed/go-sdk/exhaustive/inlined_requests.go
@@ -8,13 +8,13 @@ import (
)
var (
- postWithObjectBodyFieldString = big.NewInt(1 << 0)
+ postWithObjectBodyFieldFieldString = big.NewInt(1 << 0)
postWithObjectBodyFieldInteger = big.NewInt(1 << 1)
postWithObjectBodyFieldNestedObject = big.NewInt(1 << 2)
)
type PostWithObjectBody struct {
- String string `json:"string" url:"-"`
+ FieldString string `json:"string" url:"-"`
Integer int `json:"integer" url:"-"`
NestedObject *types.ObjectWithOptionalField `json:"NestedObject,omitempty" url:"-"`
@@ -29,11 +29,11 @@ func (p *PostWithObjectBody) require(field *big.Int) {
p.explicitFields.Or(p.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (p *PostWithObjectBody) SetString(string_ string) {
- p.String = string_
- p.require(postWithObjectBodyFieldString)
+func (p *PostWithObjectBody) SetFieldString(string_ string) {
+ p.FieldString = string_
+ p.require(postWithObjectBodyFieldFieldString)
}
// SetInteger sets the Integer field and marks it as non-optional;
diff --git a/seed/go-sdk/exhaustive/internal/query.go b/seed/go-sdk/exhaustive/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/exhaustive/internal/query.go
+++ b/seed/go-sdk/exhaustive/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/exhaustive/internal/query_test.go b/seed/go-sdk/exhaustive/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/exhaustive/internal/query_test.go
+++ b/seed/go-sdk/exhaustive/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/exhaustive/reference.md b/seed/go-sdk/exhaustive/reference.md
index 7ddd5552cff3..6f3fc115a0df 100644
--- a/seed/go-sdk/exhaustive/reference.md
+++ b/seed/go-sdk/exhaustive/reference.md
@@ -63,10 +63,10 @@ client.Endpoints.Container.GetAndReturnListOfPrimitives(
```go
request := []*types.ObjectWithRequiredField{
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnListOfObjects(
@@ -162,7 +162,7 @@ client.Endpoints.Container.GetAndReturnSetOfPrimitives(
```go
request := []*types.ObjectWithRequiredField{
&types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnSetOfObjects(
@@ -258,7 +258,7 @@ client.Endpoints.Container.GetAndReturnMapPrimToPrim(
```go
request := map[string]*types.ObjectWithRequiredField{
"string": &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
},
}
client.Endpoints.Container.GetAndReturnMapOfPrimToObject(
@@ -306,7 +306,7 @@ client.Endpoints.Container.GetAndReturnMapOfPrimToObject(
```go
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.Container.GetAndReturnOptional(
context.TODO(),
@@ -354,7 +354,7 @@ client.Endpoints.Container.GetAndReturnOptional(
```go
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -444,7 +444,7 @@ client.Endpoints.ContentType.PostJsonPatchContentType(
```go
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -624,7 +624,7 @@ client.Endpoints.HttpMethods.TestGet(
```go
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.HttpMethods.TestPost(
context.TODO(),
@@ -671,7 +671,7 @@ client.Endpoints.HttpMethods.TestPost(
```go
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.HttpMethods.TestPut(
context.TODO(),
@@ -727,7 +727,7 @@ client.Endpoints.HttpMethods.TestPut(
```go
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -871,7 +871,7 @@ client.Endpoints.HttpMethods.TestDelete(
```go
request := &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -961,7 +961,7 @@ client.Endpoints.Object.GetAndReturnWithOptionalField(
```go
request := &types.ObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
}
client.Endpoints.Object.GetAndReturnWithRequiredField(
context.TODO(),
@@ -1059,11 +1059,11 @@ client.Endpoints.Object.GetAndReturnWithMapOfMap(
```go
request := &types.NestedObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -1154,9 +1154,9 @@ client.Endpoints.Object.GetAndReturnNestedWithOptionalField(
```go
request := &types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -1257,9 +1257,9 @@ client.Endpoints.Object.GetAndReturnNestedWithRequiredField(
```go
request := []*types.NestedObjectWithRequiredField{
&types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -1306,9 +1306,9 @@ request := []*types.NestedObjectWithRequiredField{
},
},
&types.NestedObjectWithRequiredField{
- String: "string",
+ FieldString: "string",
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
@@ -2558,7 +2558,7 @@ request := &fern.PostWithObjectBody{
String: "string",
Integer: 1,
NestedObject: &types.ObjectWithOptionalField{
- String: fern.String(
+ FieldString: fern.String(
"string",
),
Integer: fern.Int(
diff --git a/seed/go-sdk/exhaustive/snippet.json b/seed/go-sdk/exhaustive/snippet.json
index 2e49b68bc48e..c061b5066c3c 100644
--- a/seed/go-sdk/exhaustive/snippet.json
+++ b/seed/go-sdk/exhaustive/snippet.json
@@ -8,7 +8,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnListOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t},\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnListOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n"
}
},
{
@@ -30,7 +30,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnMapOfPrimToObject(\n\tcontext.TODO(),\n\tmap[string]*types.ObjectWithRequiredField{\n\t\t\"string\": \u0026types.ObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnMapOfPrimToObject(\n\tcontext.TODO(),\n\tmap[string]*types.ObjectWithRequiredField{\n\t\t\"string\": \u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n"
}
},
{
@@ -52,7 +52,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnOptional(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tString: \"string\",\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnOptional(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n"
}
},
{
@@ -63,7 +63,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnSetOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Container.GetAndReturnSetOfObjects(\n\tcontext.TODO(),\n\t[]*types.ObjectWithRequiredField{\n\t\t\u0026types.ObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t},\n\t},\n)\n"
}
},
{
@@ -96,7 +96,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
}
},
{
@@ -107,7 +107,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nerr := client.Endpoints.ContentType.PostJsonPatchContentWithCharsetType(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
}
},
{
@@ -118,7 +118,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPost(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tString: \"string\",\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPost(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n"
}
},
{
@@ -151,7 +151,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPatch(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithOptionalField{\n\t\tString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPatch(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
}
},
{
@@ -162,7 +162,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPut(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithRequiredField{\n\t\tString: \"string\",\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.HttpMethods.TestPut(\n\tcontext.TODO(),\n\t\"id\",\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n"
}
},
{
@@ -206,7 +206,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.NestedObjectWithOptionalField{\n\t\tString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.NestedObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
}
},
{
@@ -217,7 +217,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList(\n\tcontext.TODO(),\n\t[]*types.NestedObjectWithRequiredField{\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredFieldAsList(\n\tcontext.TODO(),\n\t[]*types.NestedObjectWithRequiredField{\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tFieldString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t\u0026types.NestedObjectWithRequiredField{\n\t\t\tFieldString: \"string\",\n\t\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\t\tFieldString: fern.String(\n\t\t\t\t\t\"string\",\n\t\t\t\t),\n\t\t\t\tInteger: fern.Int(\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t\tLong: fern.Int64(\n\t\t\t\t\t1000000,\n\t\t\t\t),\n\t\t\t\tDouble: fern.Float64(\n\t\t\t\t\t1.1,\n\t\t\t\t),\n\t\t\t\tBool: fern.Bool(\n\t\t\t\t\ttrue,\n\t\t\t\t),\n\t\t\t\tDatetime: fern.Time(\n\t\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tDate: fern.Time(\n\t\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tUuid: fern.UUID(\n\t\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\t\tList: []string{\n\t\t\t\t\t\"list\",\n\t\t\t\t\t\"list\",\n\t\t\t\t},\n\t\t\t\tSet: []string{\n\t\t\t\t\t\"set\",\n\t\t\t\t},\n\t\t\t\tMap: map[int]string{\n\t\t\t\t\t1: \"map\",\n\t\t\t\t},\n\t\t\t\tBigint: fern.String(\n\t\t\t\t\t\"1000000\",\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t},\n)\n"
}
},
{
@@ -228,7 +228,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredField(\n\tcontext.TODO(),\n\t\"string\",\n\t\u0026types.NestedObjectWithRequiredField{\n\t\tString: \"string\",\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnNestedWithRequiredField(\n\tcontext.TODO(),\n\t\"string\",\n\t\u0026types.NestedObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
}
},
{
@@ -250,7 +250,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithOptionalField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithOptionalField{\n\t\tFieldString: fern.String(\n\t\t\t\"string\",\n\t\t),\n\t\tInteger: fern.Int(\n\t\t\t1,\n\t\t),\n\t\tLong: fern.Int64(\n\t\t\t1000000,\n\t\t),\n\t\tDouble: fern.Float64(\n\t\t\t1.1,\n\t\t),\n\t\tBool: fern.Bool(\n\t\t\ttrue,\n\t\t),\n\t\tDatetime: fern.Time(\n\t\t\tfern.MustParseDateTime(\n\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t),\n\t\t),\n\t\tDate: fern.Time(\n\t\t\tfern.MustParseDate(\n\t\t\t\t\"2023-01-15\",\n\t\t\t),\n\t\t),\n\t\tUuid: fern.UUID(\n\t\t\tuuid.MustParse(\n\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t),\n\t\t),\n\t\tBase64: []byte(\"Hello world!\"),\n\t\tList: []string{\n\t\t\t\"list\",\n\t\t\t\"list\",\n\t\t},\n\t\tSet: []string{\n\t\t\t\"set\",\n\t\t},\n\t\tMap: map[int]string{\n\t\t\t1: \"map\",\n\t\t},\n\t\tBigint: fern.String(\n\t\t\t\"1000000\",\n\t\t),\n\t},\n)\n"
}
},
{
@@ -261,7 +261,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithRequiredField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tString: \"string\",\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.Endpoints.Object.GetAndReturnWithRequiredField(\n\tcontext.TODO(),\n\t\u0026types.ObjectWithRequiredField{\n\t\tFieldString: \"string\",\n\t},\n)\n"
}
},
{
@@ -459,7 +459,7 @@
},
"snippet": {
"type": "go",
- "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.InlinedRequests.PostWithObjectBodyandResponse(\n\tcontext.TODO(),\n\t\u0026fern.PostWithObjectBody{\n\t\tString: \"string\",\n\t\tInteger: 1,\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/exhaustive/fern\"\n\tfernclient \"github.com/exhaustive/fern/client\"\n\toption \"github.com/exhaustive/fern/option\"\n\ttypes \"github.com/exhaustive/fern/types\"\n\tuuid \"github.com/google/uuid\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n)\nresponse, err := client.InlinedRequests.PostWithObjectBodyandResponse(\n\tcontext.TODO(),\n\t\u0026fern.PostWithObjectBody{\n\t\tFieldString: \"string\",\n\t\tInteger: 1,\n\t\tNestedObject: \u0026types.ObjectWithOptionalField{\n\t\t\tFieldString: fern.String(\n\t\t\t\t\"string\",\n\t\t\t),\n\t\t\tInteger: fern.Int(\n\t\t\t\t1,\n\t\t\t),\n\t\t\tLong: fern.Int64(\n\t\t\t\t1000000,\n\t\t\t),\n\t\t\tDouble: fern.Float64(\n\t\t\t\t1.1,\n\t\t\t),\n\t\t\tBool: fern.Bool(\n\t\t\t\ttrue,\n\t\t\t),\n\t\t\tDatetime: fern.Time(\n\t\t\t\tfern.MustParseDateTime(\n\t\t\t\t\t\"2024-01-15T09:30:00Z\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tDate: fern.Time(\n\t\t\t\tfern.MustParseDate(\n\t\t\t\t\t\"2023-01-15\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tUuid: fern.UUID(\n\t\t\t\tuuid.MustParse(\n\t\t\t\t\t\"d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32\",\n\t\t\t\t),\n\t\t\t),\n\t\t\tBase64: []byte(\"Hello world!\"),\n\t\t\tList: []string{\n\t\t\t\t\"list\",\n\t\t\t\t\"list\",\n\t\t\t},\n\t\t\tSet: []string{\n\t\t\t\t\"set\",\n\t\t\t},\n\t\t\tMap: map[int]string{\n\t\t\t\t1: \"map\",\n\t\t\t},\n\t\t\tBigint: fern.String(\n\t\t\t\t\"1000000\",\n\t\t\t),\n\t\t},\n\t},\n)\n"
}
},
{
diff --git a/seed/go-sdk/exhaustive/types/docs.go b/seed/go-sdk/exhaustive/types/docs.go
index 4b7119433892..91c1dc5078b9 100644
--- a/seed/go-sdk/exhaustive/types/docs.go
+++ b/seed/go-sdk/exhaustive/types/docs.go
@@ -10,7 +10,7 @@ import (
)
var (
- objectWithDocsFieldString = big.NewInt(1 << 0)
+ objectWithDocsFieldFieldString = big.NewInt(1 << 0)
)
type ObjectWithDocs struct {
@@ -73,7 +73,7 @@ type ObjectWithDocs struct {
// - ** /: PHPDoc comment end
// - *: Can interfere with comment blocks
// - &: HTML entities
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
// Private bitmask of fields set to an explicit value and therefore not to be omitted
explicitFields *big.Int `json:"-" url:"-"`
@@ -82,11 +82,11 @@ type ObjectWithDocs struct {
rawJSON json.RawMessage
}
-func (o *ObjectWithDocs) GetString() string {
+func (o *ObjectWithDocs) GetFieldString() string {
if o == nil {
return ""
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithDocs) GetExtraProperties() map[string]interface{} {
@@ -100,11 +100,11 @@ func (o *ObjectWithDocs) require(field *big.Int) {
o.explicitFields.Or(o.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (o *ObjectWithDocs) SetString(string_ string) {
- o.String = string_
- o.require(objectWithDocsFieldString)
+func (o *ObjectWithDocs) SetFieldString(string_ string) {
+ o.FieldString = string_
+ o.require(objectWithDocsFieldFieldString)
}
func (o *ObjectWithDocs) UnmarshalJSON(data []byte) error {
diff --git a/seed/go-sdk/exhaustive/types/object.go b/seed/go-sdk/exhaustive/types/object.go
index 39ac4a350399..1ef33bb737ce 100644
--- a/seed/go-sdk/exhaustive/types/object.go
+++ b/seed/go-sdk/exhaustive/types/object.go
@@ -90,12 +90,12 @@ func (d *DoubleOptional) String() string {
}
var (
- nestedObjectWithOptionalFieldFieldString = big.NewInt(1 << 0)
+ nestedObjectWithOptionalFieldFieldFieldString = big.NewInt(1 << 0)
nestedObjectWithOptionalFieldFieldNestedObject = big.NewInt(1 << 1)
)
type NestedObjectWithOptionalField struct {
- String *string `json:"string,omitempty" url:"string,omitempty"`
+ FieldString *string `json:"string,omitempty" url:"string,omitempty"`
NestedObject *ObjectWithOptionalField `json:"NestedObject,omitempty" url:"NestedObject,omitempty"`
// Private bitmask of fields set to an explicit value and therefore not to be omitted
@@ -105,11 +105,11 @@ type NestedObjectWithOptionalField struct {
rawJSON json.RawMessage
}
-func (n *NestedObjectWithOptionalField) GetString() *string {
+func (n *NestedObjectWithOptionalField) GetFieldString() *string {
if n == nil {
return nil
}
- return n.String
+ return n.FieldString
}
func (n *NestedObjectWithOptionalField) GetNestedObject() *ObjectWithOptionalField {
@@ -130,11 +130,11 @@ func (n *NestedObjectWithOptionalField) require(field *big.Int) {
n.explicitFields.Or(n.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (n *NestedObjectWithOptionalField) SetString(string_ *string) {
- n.String = string_
- n.require(nestedObjectWithOptionalFieldFieldString)
+func (n *NestedObjectWithOptionalField) SetFieldString(string_ *string) {
+ n.FieldString = string_
+ n.require(nestedObjectWithOptionalFieldFieldFieldString)
}
// SetNestedObject sets the NestedObject field and marks it as non-optional;
@@ -184,12 +184,12 @@ func (n *NestedObjectWithOptionalField) String() string {
}
var (
- nestedObjectWithRequiredFieldFieldString = big.NewInt(1 << 0)
+ nestedObjectWithRequiredFieldFieldFieldString = big.NewInt(1 << 0)
nestedObjectWithRequiredFieldFieldNestedObject = big.NewInt(1 << 1)
)
type NestedObjectWithRequiredField struct {
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
NestedObject *ObjectWithOptionalField `json:"NestedObject" url:"NestedObject"`
// Private bitmask of fields set to an explicit value and therefore not to be omitted
@@ -199,11 +199,11 @@ type NestedObjectWithRequiredField struct {
rawJSON json.RawMessage
}
-func (n *NestedObjectWithRequiredField) GetString() string {
+func (n *NestedObjectWithRequiredField) GetFieldString() string {
if n == nil {
return ""
}
- return n.String
+ return n.FieldString
}
func (n *NestedObjectWithRequiredField) GetNestedObject() *ObjectWithOptionalField {
@@ -224,11 +224,11 @@ func (n *NestedObjectWithRequiredField) require(field *big.Int) {
n.explicitFields.Or(n.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (n *NestedObjectWithRequiredField) SetString(string_ string) {
- n.String = string_
- n.require(nestedObjectWithRequiredFieldFieldString)
+func (n *NestedObjectWithRequiredField) SetFieldString(string_ string) {
+ n.FieldString = string_
+ n.require(nestedObjectWithRequiredFieldFieldFieldString)
}
// SetNestedObject sets the NestedObject field and marks it as non-optional;
@@ -356,36 +356,36 @@ func (o *ObjectWithMapOfMap) String() string {
}
var (
- objectWithOptionalFieldFieldString = big.NewInt(1 << 0)
- objectWithOptionalFieldFieldInteger = big.NewInt(1 << 1)
- objectWithOptionalFieldFieldLong = big.NewInt(1 << 2)
- objectWithOptionalFieldFieldDouble = big.NewInt(1 << 3)
- objectWithOptionalFieldFieldBool = big.NewInt(1 << 4)
- objectWithOptionalFieldFieldDatetime = big.NewInt(1 << 5)
- objectWithOptionalFieldFieldDate = big.NewInt(1 << 6)
- objectWithOptionalFieldFieldUuid = big.NewInt(1 << 7)
- objectWithOptionalFieldFieldBase64 = big.NewInt(1 << 8)
- objectWithOptionalFieldFieldList = big.NewInt(1 << 9)
- objectWithOptionalFieldFieldSet = big.NewInt(1 << 10)
- objectWithOptionalFieldFieldMap = big.NewInt(1 << 11)
- objectWithOptionalFieldFieldBigint = big.NewInt(1 << 12)
+ objectWithOptionalFieldFieldFieldString = big.NewInt(1 << 0)
+ objectWithOptionalFieldFieldInteger = big.NewInt(1 << 1)
+ objectWithOptionalFieldFieldLong = big.NewInt(1 << 2)
+ objectWithOptionalFieldFieldDouble = big.NewInt(1 << 3)
+ objectWithOptionalFieldFieldBool = big.NewInt(1 << 4)
+ objectWithOptionalFieldFieldDatetime = big.NewInt(1 << 5)
+ objectWithOptionalFieldFieldDate = big.NewInt(1 << 6)
+ objectWithOptionalFieldFieldUuid = big.NewInt(1 << 7)
+ objectWithOptionalFieldFieldBase64 = big.NewInt(1 << 8)
+ objectWithOptionalFieldFieldList = big.NewInt(1 << 9)
+ objectWithOptionalFieldFieldSet = big.NewInt(1 << 10)
+ objectWithOptionalFieldFieldMap = big.NewInt(1 << 11)
+ objectWithOptionalFieldFieldBigint = big.NewInt(1 << 12)
)
type ObjectWithOptionalField struct {
// This is a rather long descriptor of this single field in a more complex type. If you ask me I think this is a pretty good description for this field all things considered.
- String *string `json:"string,omitempty" url:"string,omitempty"`
- Integer *int `json:"integer,omitempty" url:"integer,omitempty"`
- Long *int64 `json:"long,omitempty" url:"long,omitempty"`
- Double *float64 `json:"double,omitempty" url:"double,omitempty"`
- Bool *bool `json:"bool,omitempty" url:"bool,omitempty"`
- Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"`
- Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
- Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"`
- Base64 *[]byte `json:"base64,omitempty" url:"base64,omitempty"`
- List []string `json:"list,omitempty" url:"list,omitempty"`
- Set []string `json:"set,omitempty" url:"set,omitempty"`
- Map map[int]string `json:"map,omitempty" url:"map,omitempty"`
- Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"`
+ FieldString *string `json:"string,omitempty" url:"string,omitempty"`
+ Integer *int `json:"integer,omitempty" url:"integer,omitempty"`
+ Long *int64 `json:"long,omitempty" url:"long,omitempty"`
+ Double *float64 `json:"double,omitempty" url:"double,omitempty"`
+ Bool *bool `json:"bool,omitempty" url:"bool,omitempty"`
+ Datetime *time.Time `json:"datetime,omitempty" url:"datetime,omitempty"`
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ Uuid *uuid.UUID `json:"uuid,omitempty" url:"uuid,omitempty"`
+ Base64 *[]byte `json:"base64,omitempty" url:"base64,omitempty"`
+ List []string `json:"list,omitempty" url:"list,omitempty"`
+ Set []string `json:"set,omitempty" url:"set,omitempty"`
+ Map map[int]string `json:"map,omitempty" url:"map,omitempty"`
+ Bigint *string `json:"bigint,omitempty" url:"bigint,omitempty"`
// Private bitmask of fields set to an explicit value and therefore not to be omitted
explicitFields *big.Int `json:"-" url:"-"`
@@ -394,11 +394,11 @@ type ObjectWithOptionalField struct {
rawJSON json.RawMessage
}
-func (o *ObjectWithOptionalField) GetString() *string {
+func (o *ObjectWithOptionalField) GetFieldString() *string {
if o == nil {
return nil
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithOptionalField) GetInteger() *int {
@@ -496,11 +496,11 @@ func (o *ObjectWithOptionalField) require(field *big.Int) {
o.explicitFields.Or(o.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (o *ObjectWithOptionalField) SetString(string_ *string) {
- o.String = string_
- o.require(objectWithOptionalFieldFieldString)
+func (o *ObjectWithOptionalField) SetFieldString(string_ *string) {
+ o.FieldString = string_
+ o.require(objectWithOptionalFieldFieldFieldString)
}
// SetInteger sets the Integer field and marks it as non-optional;
@@ -639,11 +639,11 @@ func (o *ObjectWithOptionalField) String() string {
}
var (
- objectWithRequiredFieldFieldString = big.NewInt(1 << 0)
+ objectWithRequiredFieldFieldFieldString = big.NewInt(1 << 0)
)
type ObjectWithRequiredField struct {
- String string `json:"string" url:"string"`
+ FieldString string `json:"string" url:"string"`
// Private bitmask of fields set to an explicit value and therefore not to be omitted
explicitFields *big.Int `json:"-" url:"-"`
@@ -652,11 +652,11 @@ type ObjectWithRequiredField struct {
rawJSON json.RawMessage
}
-func (o *ObjectWithRequiredField) GetString() string {
+func (o *ObjectWithRequiredField) GetFieldString() string {
if o == nil {
return ""
}
- return o.String
+ return o.FieldString
}
func (o *ObjectWithRequiredField) GetExtraProperties() map[string]interface{} {
@@ -670,11 +670,11 @@ func (o *ObjectWithRequiredField) require(field *big.Int) {
o.explicitFields.Or(o.explicitFields, field)
}
-// SetString sets the String field and marks it as non-optional;
+// SetFieldString sets the FieldString field and marks it as non-optional;
// this prevents an empty or null value for this field from being omitted during serialization.
-func (o *ObjectWithRequiredField) SetString(string_ string) {
- o.String = string_
- o.require(objectWithRequiredFieldFieldString)
+func (o *ObjectWithRequiredField) SetFieldString(string_ string) {
+ o.FieldString = string_
+ o.require(objectWithRequiredFieldFieldFieldString)
}
func (o *ObjectWithRequiredField) UnmarshalJSON(data []byte) error {
diff --git a/seed/go-sdk/extends/internal/query.go b/seed/go-sdk/extends/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/extends/internal/query.go
+++ b/seed/go-sdk/extends/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/extends/internal/query_test.go b/seed/go-sdk/extends/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/extends/internal/query_test.go
+++ b/seed/go-sdk/extends/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/extra-properties/internal/query.go b/seed/go-sdk/extra-properties/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/extra-properties/internal/query.go
+++ b/seed/go-sdk/extra-properties/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/extra-properties/internal/query_test.go b/seed/go-sdk/extra-properties/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/extra-properties/internal/query_test.go
+++ b/seed/go-sdk/extra-properties/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/file-download/internal/query.go b/seed/go-sdk/file-download/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/file-download/internal/query.go
+++ b/seed/go-sdk/file-download/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/file-download/internal/query_test.go b/seed/go-sdk/file-download/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/file-download/internal/query_test.go
+++ b/seed/go-sdk/file-download/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/file-upload-openapi/internal/query.go b/seed/go-sdk/file-upload-openapi/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/file-upload-openapi/internal/query.go
+++ b/seed/go-sdk/file-upload-openapi/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/file-upload-openapi/internal/query_test.go b/seed/go-sdk/file-upload-openapi/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/file-upload-openapi/internal/query_test.go
+++ b/seed/go-sdk/file-upload-openapi/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/query.go b/seed/go-sdk/file-upload/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/file-upload/no-custom-config/internal/query.go
+++ b/seed/go-sdk/file-upload/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/file-upload/package-name/internal/query.go b/seed/go-sdk/file-upload/package-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/file-upload/package-name/internal/query.go
+++ b/seed/go-sdk/file-upload/package-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/file-upload/package-name/internal/query_test.go b/seed/go-sdk/file-upload/package-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/file-upload/package-name/internal/query_test.go
+++ b/seed/go-sdk/file-upload/package-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/file-upload/v0/internal/query.go b/seed/go-sdk/file-upload/v0/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/file-upload/v0/internal/query.go
+++ b/seed/go-sdk/file-upload/v0/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/file-upload/v0/internal/query_test.go b/seed/go-sdk/file-upload/v0/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/file-upload/v0/internal/query_test.go
+++ b/seed/go-sdk/file-upload/v0/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/folders/internal/query.go b/seed/go-sdk/folders/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/folders/internal/query.go
+++ b/seed/go-sdk/folders/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/folders/internal/query_test.go b/seed/go-sdk/folders/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/folders/internal/query_test.go
+++ b/seed/go-sdk/folders/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/go-bytes-request/no-custom-config/internal/query.go b/seed/go-sdk/go-bytes-request/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/go-bytes-request/no-custom-config/internal/query.go
+++ b/seed/go-sdk/go-bytes-request/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/go-bytes-request/no-custom-config/internal/query_test.go b/seed/go-sdk/go-bytes-request/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/go-bytes-request/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/go-bytes-request/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query.go b/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query.go
+++ b/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query_test.go b/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query_test.go
+++ b/seed/go-sdk/go-bytes-request/use-reader-for-bytes-request/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/go-content-type/internal/query.go b/seed/go-sdk/go-content-type/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/go-content-type/internal/query.go
+++ b/seed/go-sdk/go-content-type/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/go-content-type/internal/query_test.go b/seed/go-sdk/go-content-type/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/go-content-type/internal/query_test.go
+++ b/seed/go-sdk/go-content-type/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/header-auth-environment-variable/internal/query.go b/seed/go-sdk/header-auth-environment-variable/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/header-auth-environment-variable/internal/query.go
+++ b/seed/go-sdk/header-auth-environment-variable/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/header-auth-environment-variable/internal/query_test.go b/seed/go-sdk/header-auth-environment-variable/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/header-auth-environment-variable/internal/query_test.go
+++ b/seed/go-sdk/header-auth-environment-variable/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/header-auth/internal/query.go b/seed/go-sdk/header-auth/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/header-auth/internal/query.go
+++ b/seed/go-sdk/header-auth/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/header-auth/internal/query_test.go b/seed/go-sdk/header-auth/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/header-auth/internal/query_test.go
+++ b/seed/go-sdk/header-auth/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/http-head/internal/query.go b/seed/go-sdk/http-head/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/http-head/internal/query.go
+++ b/seed/go-sdk/http-head/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/http-head/internal/query_test.go b/seed/go-sdk/http-head/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/http-head/internal/query_test.go
+++ b/seed/go-sdk/http-head/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/idempotency-headers/.fern/metadata.json b/seed/go-sdk/idempotency-headers/.fern/metadata.json
new file mode 100644
index 000000000000..66bd514ed2c0
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/.fern/metadata.json
@@ -0,0 +1,13 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-sdk",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "enableWireTests": false,
+ "packageName": "fern",
+ "module": {
+ "path": "github.com/idempotency-headers/fern"
+ },
+ "includeLegacyClientOptions": true
+ }
+}
\ No newline at end of file
diff --git a/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml b/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
new file mode 100644
index 000000000000..56310d69624b
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on: [push]
+
+jobs:
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Compile
+ run: go build ./...
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Setup wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
+
+ - name: Test
+ run: go test ./...
+
+ - name: Teardown wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/idempotency-headers/client/client.go b/seed/go-sdk/idempotency-headers/client/client.go
new file mode 100644
index 000000000000..df29cd1e96bd
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/client/client.go
@@ -0,0 +1,33 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ core "github.com/idempotency-headers/fern/core"
+ internal "github.com/idempotency-headers/fern/internal"
+ option "github.com/idempotency-headers/fern/option"
+ payment "github.com/idempotency-headers/fern/payment"
+)
+
+type Client struct {
+ Payment *payment.Client
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(opts ...option.RequestOption) *Client {
+ options := core.NewRequestOptions(opts...)
+ return &Client{
+ Payment: payment.NewClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/client/client_test.go b/seed/go-sdk/idempotency-headers/client/client_test.go
new file mode 100644
index 000000000000..984e654452ff
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/client/client_test.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ option "github.com/idempotency-headers/fern/option"
+ assert "github.com/stretchr/testify/assert"
+ http "net/http"
+ testing "testing"
+ time "time"
+)
+
+func TestNewClient(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ c := NewClient()
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("base url", func(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("test.co"),
+ )
+ assert.Equal(t, "test.co", c.baseURL)
+ })
+
+ t.Run("http client", func(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ c := NewClient(
+ option.WithHTTPClient(httpClient),
+ )
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("http header", func(t *testing.T) {
+ header := make(http.Header)
+ header.Set("X-API-Tenancy", "test")
+ c := NewClient(
+ option.WithHTTPHeader(header),
+ )
+ assert.Empty(t, c.baseURL)
+ assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
+ })
+}
diff --git a/seed/go-sdk/idempotency-headers/client/options.go b/seed/go-sdk/idempotency-headers/client/options.go
new file mode 100644
index 000000000000..1a2d4df5ad59
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/client/options.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ core "github.com/idempotency-headers/fern/core"
+ option "github.com/idempotency-headers/fern/option"
+ http "net/http"
+)
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return option.WithBaseURL(baseURL)
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return option.WithHTTPClient(httpClient)
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return option.WithHTTPHeader(httpHeader)
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return option.WithMaxAttempts(attempts)
+}
+
+// WithToken sets the 'Authorization: Bearer ' request header.
+func WithToken(token string) *core.TokenOption {
+ return option.WithToken(token)
+}
+
+// WithIdempotencyKey sets the idempotencyKey request header.
+func WithIdempotencyKey(idempotencyKey string) *core.IdempotencyKeyOption {
+ return option.WithIdempotencyKey(idempotencyKey)
+}
+
+// WithIdempotencyExpiration sets the idempotencyExpiration request header.
+func WithIdempotencyExpiration(idempotencyExpiration int) *core.IdempotencyExpirationOption {
+ return option.WithIdempotencyExpiration(idempotencyExpiration)
+}
diff --git a/seed/go-sdk/idempotency-headers/core/api_error.go b/seed/go-sdk/idempotency-headers/core/api_error.go
new file mode 100644
index 000000000000..6168388541b4
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/core/api_error.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// APIError is a lightweight wrapper around the standard error
+// interface that preserves the status code from the RPC, if any.
+type APIError struct {
+ err error
+
+ StatusCode int `json:"-"`
+ Header http.Header `json:"-"`
+}
+
+// NewAPIError constructs a new API error.
+func NewAPIError(statusCode int, header http.Header, err error) *APIError {
+ return &APIError{
+ err: err,
+ Header: header,
+ StatusCode: statusCode,
+ }
+}
+
+// Unwrap returns the underlying error. This also makes the error compatible
+// with errors.As and errors.Is.
+func (a *APIError) Unwrap() error {
+ if a == nil {
+ return nil
+ }
+ return a.err
+}
+
+// Error returns the API error's message.
+func (a *APIError) Error() string {
+ if a == nil || (a.err == nil && a.StatusCode == 0) {
+ return ""
+ }
+ if a.err == nil {
+ return fmt.Sprintf("%d", a.StatusCode)
+ }
+ if a.StatusCode == 0 {
+ return a.err.Error()
+ }
+ return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
+}
diff --git a/seed/go-sdk/idempotency-headers/core/http.go b/seed/go-sdk/idempotency-headers/core/http.go
new file mode 100644
index 000000000000..92c435692940
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/core/http.go
@@ -0,0 +1,15 @@
+package core
+
+import "net/http"
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Response is an HTTP response from an HTTP client.
+type Response[T any] struct {
+ StatusCode int
+ Header http.Header
+ Body T
+}
diff --git a/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go b/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
new file mode 100644
index 000000000000..48e4e5628411
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
@@ -0,0 +1,72 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ fmt "fmt"
+ http "net/http"
+)
+
+// IdempotentRequestOption adapts the behavior of an individual request.
+type IdempotentRequestOption interface {
+ applyIdempotentRequestOptions(*IdempotentRequestOptions)
+}
+
+// IdempotentRequestOptions defines all of the possible idempotent request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type IdempotentRequestOptions struct {
+ *RequestOptions
+
+ IdempotencyKey string
+ IdempotencyExpiration int
+}
+
+// NewIdempotentRequestOptions returns a new *IdempotentRequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use IdempotentRequestOption instead.
+func NewIdempotentRequestOptions(opts ...IdempotentRequestOption) *IdempotentRequestOptions {
+ options := &IdempotentRequestOptions{
+ RequestOptions: NewRequestOptions(),
+ }
+ for _, opt := range opts {
+ if requestOption, ok := opt.(RequestOption); ok {
+ requestOption.applyRequestOptions(options.RequestOptions)
+ }
+ opt.applyIdempotentRequestOptions(options)
+ }
+ return options
+}
+
+// IdempotencyKeyOption implements the RequestOption interface.
+type IdempotencyKeyOption struct {
+ IdempotencyKey string
+}
+
+func (i *IdempotencyKeyOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.IdempotencyKey = i.IdempotencyKey
+}
+
+// IdempotencyExpirationOption implements the RequestOption interface.
+type IdempotencyExpirationOption struct {
+ IdempotencyExpiration int
+}
+
+func (i *IdempotencyExpirationOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.IdempotencyExpiration = i.IdempotencyExpiration
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request.
+func (i *IdempotentRequestOptions) ToHeader() http.Header {
+ header := i.RequestOptions.ToHeader()
+ if i.IdempotencyKey != "" {
+ header.Set("Idempotency-Key", fmt.Sprintf("%v", i.IdempotencyKey))
+ }
+ if i.IdempotencyExpiration != 0 {
+ header.Set("Idempotency-Expiration", fmt.Sprintf("%v", i.IdempotencyExpiration))
+ }
+ return header
+}
diff --git a/seed/go-sdk/idempotency-headers/core/request_option.go b/seed/go-sdk/idempotency-headers/core/request_option.go
new file mode 100644
index 000000000000..99b12cc2f920
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/core/request_option.go
@@ -0,0 +1,153 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of the client or an individual request.
+type RequestOption interface {
+ applyRequestOptions(*RequestOptions)
+}
+
+// RequestOptions defines all of the possible request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type RequestOptions struct {
+ BaseURL string
+ HTTPClient HTTPClient
+ HTTPHeader http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ MaxAttempts uint
+ Token string
+}
+
+// NewRequestOptions returns a new *RequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use RequestOption instead.
+func NewRequestOptions(opts ...RequestOption) *RequestOptions {
+ options := &RequestOptions{
+ HTTPHeader: make(http.Header),
+ BodyProperties: make(map[string]interface{}),
+ QueryParameters: make(url.Values),
+ }
+ for _, opt := range opts {
+ opt.applyRequestOptions(options)
+ }
+ return options
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request(s).
+func (r *RequestOptions) ToHeader() http.Header {
+ header := r.cloneHeader()
+ if r.Token != "" {
+ header.Set("Authorization", "Bearer "+r.Token)
+ }
+ return header
+}
+
+func (r *RequestOptions) cloneHeader() http.Header {
+ headers := r.HTTPHeader.Clone()
+ headers.Set("X-Fern-Language", "Go")
+ headers.Set("X-Fern-SDK-Name", "github.com/idempotency-headers/fern")
+ headers.Set("X-Fern-SDK-Version", "v0.0.1")
+ headers.Set("User-Agent", "github.com/idempotency-headers/fern/0.0.1")
+ return headers
+}
+
+// BaseURLOption implements the RequestOption interface.
+type BaseURLOption struct {
+ BaseURL string
+}
+
+func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+func (b *BaseURLOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+// HTTPClientOption implements the RequestOption interface.
+type HTTPClientOption struct {
+ HTTPClient HTTPClient
+}
+
+func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+func (h *HTTPClientOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+// HTTPHeaderOption implements the RequestOption interface.
+type HTTPHeaderOption struct {
+ HTTPHeader http.Header
+}
+
+func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+func (h *HTTPHeaderOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+// BodyPropertiesOption implements the RequestOption interface.
+type BodyPropertiesOption struct {
+ BodyProperties map[string]interface{}
+}
+
+func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+func (b *BodyPropertiesOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+// QueryParametersOption implements the RequestOption interface.
+type QueryParametersOption struct {
+ QueryParameters url.Values
+}
+
+func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+func (q *QueryParametersOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+// MaxAttemptsOption implements the RequestOption interface.
+type MaxAttemptsOption struct {
+ MaxAttempts uint
+}
+
+func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
+
+func (m *MaxAttemptsOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
+
+// TokenOption implements the RequestOption interface.
+type TokenOption struct {
+ Token string
+}
+
+func (t *TokenOption) applyRequestOptions(opts *RequestOptions) {
+ opts.Token = t.Token
+}
+
+func (t *TokenOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
+ opts.Token = t.Token
+}
diff --git a/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go b/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
new file mode 100644
index 000000000000..a4d13a04d72e
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
@@ -0,0 +1,27 @@
+package example
+
+import (
+ client "github.com/idempotency-headers/fern/client"
+ option "github.com/idempotency-headers/fern/option"
+ fern "github.com/idempotency-headers/fern"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ option.WithToken(
+ "",
+ ),
+ )
+ request := &fern.CreatePaymentRequest{
+ Amount: 1,
+ Currency: fern.CurrencyUsd,
+ }
+ client.Payment.Create(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go b/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
new file mode 100644
index 000000000000..45e01220fe25
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
@@ -0,0 +1,22 @@
+package example
+
+import (
+ client "github.com/idempotency-headers/fern/client"
+ option "github.com/idempotency-headers/fern/option"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ option.WithToken(
+ "",
+ ),
+ )
+ client.Payment.Delete(
+ context.TODO(),
+ "paymentId",
+ )
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/caller.go b/seed/go-sdk/idempotency-headers/internal/caller.go
new file mode 100644
index 000000000000..6cc9c680f1bb
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/caller.go
@@ -0,0 +1,250 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+
+ "github.com/idempotency-headers/fern/core"
+)
+
+const (
+ // contentType specifies the JSON Content-Type header value.
+ contentType = "application/json"
+ contentTypeHeader = "Content-Type"
+)
+
+// Caller calls APIs and deserializes their response, if any.
+type Caller struct {
+ client core.HTTPClient
+ retrier *Retrier
+}
+
+// CallerParams represents the parameters used to constrcut a new *Caller.
+type CallerParams struct {
+ Client core.HTTPClient
+ MaxAttempts uint
+}
+
+// NewCaller returns a new *Caller backed by the given parameters.
+func NewCaller(params *CallerParams) *Caller {
+ var httpClient core.HTTPClient = http.DefaultClient
+ if params.Client != nil {
+ httpClient = params.Client
+ }
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+ return &Caller{
+ client: httpClient,
+ retrier: NewRetrier(retryOptions...),
+ }
+}
+
+// CallParams represents the parameters used to issue an API call.
+type CallParams struct {
+ URL string
+ Method string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client core.HTTPClient
+ Request interface{}
+ Response interface{}
+ ResponseIsOptional bool
+ ErrorDecoder ErrorDecoder
+}
+
+// CallResponse is a parsed HTTP response from an API call.
+type CallResponse struct {
+ StatusCode int
+ Header http.Header
+}
+
+// Call issues an API call according to the given call parameters.
+func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := c.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := c.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Close the response body after we're done.
+ defer resp.Body.Close()
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ // Mutate the response parameter in-place.
+ if params.Response != nil {
+ if writer, ok := params.Response.(io.Writer); ok {
+ _, err = io.Copy(writer, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(params.Response)
+ }
+ if err != nil {
+ if err == io.EOF {
+ if params.ResponseIsOptional {
+ // The response is optional, so we should ignore the
+ // io.EOF error
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+ }
+ return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
+ }
+ return nil, err
+ }
+ }
+
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+}
+
+// buildURL constructs the final URL by appending the given query parameters (if any).
+func buildURL(
+ url string,
+ queryParameters url.Values,
+) string {
+ if len(queryParameters) == 0 {
+ return url
+ }
+ if strings.ContainsRune(url, '?') {
+ url += "&"
+ } else {
+ url += "?"
+ }
+ url += queryParameters.Encode()
+ return url
+}
+
+// newRequest returns a new *http.Request with all of the fields
+// required to issue the call.
+func newRequest(
+ ctx context.Context,
+ url string,
+ method string,
+ endpointHeaders http.Header,
+ request interface{},
+ bodyProperties map[string]interface{},
+) (*http.Request, error) {
+ requestBody, err := newRequestBody(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ req.Header.Set(contentTypeHeader, contentType)
+ for name, values := range endpointHeaders {
+ req.Header[name] = values
+ }
+ return req, nil
+}
+
+// newRequestBody returns a new io.Reader that represents the HTTP request body.
+func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
+ if isNil(request) {
+ if len(bodyProperties) == 0 {
+ return nil, nil
+ }
+ requestBytes, err := json.Marshal(bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+ }
+ if body, ok := request.(io.Reader); ok {
+ return body, nil
+ }
+ requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+}
+
+// decodeError decodes the error from the given HTTP response. Note that
+// it's the caller's responsibility to close the response body.
+func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
+ if errorDecoder != nil {
+ // This endpoint has custom errors, so we'll
+ // attempt to unmarshal the error into a structured
+ // type based on the status code.
+ return errorDecoder(response.StatusCode, response.Header, response.Body)
+ }
+ // This endpoint doesn't have any custom error
+ // types, so we just read the body as-is, and
+ // put it into a normal error.
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if err == io.EOF {
+ // The error didn't have a response body,
+ // so all we can do is return an error
+ // with the status code.
+ return core.NewAPIError(response.StatusCode, response.Header, nil)
+ }
+ return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
+}
+
+// isNil is used to determine if the request value is equal to nil (i.e. an interface
+// value that holds a nil concrete value is itself non-nil).
+func isNil(value interface{}) bool {
+ return value == nil || reflect.ValueOf(value).IsNil()
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/caller_test.go b/seed/go-sdk/idempotency-headers/internal/caller_test.go
new file mode 100644
index 000000000000..6a9c0950126d
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/caller_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/idempotency-headers/fern/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// InternalTestCase represents a single test case.
+type InternalTestCase struct {
+ description string
+
+ // Server-side assertions.
+ givePathSuffix string
+ giveMethod string
+ giveResponseIsOptional bool
+ giveHeader http.Header
+ giveErrorDecoder ErrorDecoder
+ giveRequest *InternalTestRequest
+ giveQueryParams url.Values
+ giveBodyProperties map[string]interface{}
+
+ // Client-side assertions.
+ wantResponse *InternalTestResponse
+ wantHeaders http.Header
+ wantError error
+}
+
+// InternalTestRequest a simple request body.
+type InternalTestRequest struct {
+ Id string `json:"id"`
+}
+
+// InternalTestResponse a simple response body.
+type InternalTestResponse struct {
+ Id string `json:"id"`
+ ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
+ QueryParameters url.Values `json:"queryParameters,omitempty"`
+}
+
+// InternalTestNotFoundError represents a 404.
+type InternalTestNotFoundError struct {
+ *core.APIError
+
+ Message string `json:"message"`
+}
+
+func TestCall(t *testing.T) {
+ tests := []*InternalTestCase{
+ {
+ description: "GET success",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ },
+ },
+ {
+ description: "GET success with query",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ },
+ },
+ },
+ {
+ description: "GET not found",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusNotFound),
+ },
+ giveErrorDecoder: newTestErrorDecoder(t),
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(
+ http.StatusNotFound,
+ http.Header{},
+ errors.New(`{"message":"ID \"404\" not found"}`),
+ ),
+ },
+ },
+ {
+ description: "POST empty body",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: nil,
+ wantError: core.NewAPIError(
+ http.StatusBadRequest,
+ http.Header{},
+ errors.New("invalid request"),
+ ),
+ },
+ {
+ description: "POST optional response",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveResponseIsOptional: true,
+ },
+ {
+ description: "POST API error",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusInternalServerError),
+ },
+ wantError: core.NewAPIError(
+ http.StatusInternalServerError,
+ http.Header{},
+ errors.New("failed to process request"),
+ ),
+ },
+ {
+ description: "POST extra properties",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: new(InternalTestRequest),
+ giveBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ wantResponse: &InternalTestResponse{
+ ExtraBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ },
+ },
+ {
+ description: "GET extra query parameters",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "extra": []string{"true"},
+ },
+ },
+ },
+ {
+ description: "GET merge extra query parameters",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ "extra": []string{"true"},
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ var (
+ server = newTestServer(t, test)
+ client = server.Client()
+ )
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL + test.givePathSuffix,
+ Method: test.giveMethod,
+ Headers: test.giveHeader,
+ BodyProperties: test.giveBodyProperties,
+ QueryParameters: test.giveQueryParams,
+ Request: test.giveRequest,
+ Response: &response,
+ ResponseIsOptional: test.giveResponseIsOptional,
+ ErrorDecoder: test.giveErrorDecoder,
+ },
+ )
+ if test.wantError != nil {
+ assert.EqualError(t, err, test.wantError.Error())
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+func TestMergeHeaders(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ merged := MergeHeaders(make(http.Header), make(http.Header))
+ assert.Empty(t, merged)
+ })
+
+ t.Run("empty left", func(t *testing.T) {
+ left := make(http.Header)
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("empty right", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.1")
+
+ right := make(http.Header)
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("single value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.0")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+
+ t.Run("multiple value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Versions", "0.0.0")
+
+ right := make(http.Header)
+ right.Add("X-API-Versions", "0.0.1")
+ right.Add("X-API-Versions", "0.0.2")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
+ })
+
+ t.Run("disjoint merge", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Tenancy", "test")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+}
+
+// newTestServer returns a new *httptest.Server configured with the
+// given test parameters.
+func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tc.giveMethod, r.Method)
+ assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
+ for header, value := range tc.giveHeader {
+ assert.Equal(t, value, r.Header.Values(header))
+ }
+
+ request := new(InternalTestRequest)
+
+ bytes, err := io.ReadAll(r.Body)
+ if tc.giveRequest == nil {
+ require.Empty(t, bytes)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err = w.Write([]byte("invalid request"))
+ require.NoError(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+
+ switch request.Id {
+ case strconv.Itoa(http.StatusNotFound):
+ notFoundError := &InternalTestNotFoundError{
+ APIError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ Message: fmt.Sprintf("ID %q not found", request.Id),
+ }
+ bytes, err = json.Marshal(notFoundError)
+ require.NoError(t, err)
+
+ w.WriteHeader(http.StatusNotFound)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ return
+
+ case strconv.Itoa(http.StatusInternalServerError):
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = w.Write([]byte("failed to process request"))
+ require.NoError(t, err)
+ return
+ }
+
+ if tc.giveResponseIsOptional {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ extraBodyProperties := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
+ delete(extraBodyProperties, "id")
+
+ response := &InternalTestResponse{
+ Id: request.Id,
+ ExtraBodyProperties: extraBodyProperties,
+ QueryParameters: r.URL.Query(),
+ }
+ bytes, err = json.Marshal(response)
+ require.NoError(t, err)
+
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ },
+ ),
+ )
+}
+
+// newTestErrorDecoder returns an error decoder suitable for tests.
+func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ require.NoError(t, err)
+
+ var (
+ apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
+ decoder = json.NewDecoder(bytes.NewReader(raw))
+ )
+ if statusCode == http.StatusNotFound {
+ value := new(InternalTestNotFoundError)
+ value.APIError = apiError
+ require.NoError(t, decoder.Decode(value))
+
+ return value
+ }
+ return apiError
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/error_decoder.go b/seed/go-sdk/idempotency-headers/internal/error_decoder.go
new file mode 100644
index 000000000000..07dd5e0c787c
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/error_decoder.go
@@ -0,0 +1,64 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/idempotency-headers/fern/core"
+)
+
+// ErrorCodes maps HTTP status codes to error constructors.
+type ErrorCodes map[int]func(*core.APIError) error
+
+// ErrorDecoder decodes *http.Response errors and returns a
+// typed API error (e.g. *core.APIError).
+type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
+
+// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
+// errorCodesOverrides is optional and will be merged with the default error codes,
+// with overrides taking precedence.
+func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
+ // Merge default error codes with overrides
+ mergedErrorCodes := make(ErrorCodes)
+
+ // Start with default error codes
+ for statusCode, errorFunc := range errorCodes {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+
+ // Apply overrides if provided
+ if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
+ for statusCode, errorFunc := range errorCodesOverrides[0] {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+ }
+
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ if err != nil {
+ return fmt.Errorf("failed to read error from response body: %w", err)
+ }
+ apiError := core.NewAPIError(
+ statusCode,
+ header,
+ errors.New(string(raw)),
+ )
+ newErrorFunc, ok := mergedErrorCodes[statusCode]
+ if !ok {
+ // This status code isn't recognized, so we return
+ // the API error as-is.
+ return apiError
+ }
+ customError := newErrorFunc(apiError)
+ if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
+ // If we fail to decode the error, we return the
+ // API error as-is.
+ return apiError
+ }
+ return customError
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go b/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
new file mode 100644
index 000000000000..1cefd210fcb8
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/idempotency-headers/fern/core"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorDecoder(t *testing.T) {
+ decoder := NewErrorDecoder(
+ ErrorCodes{
+ http.StatusNotFound: func(apiError *core.APIError) error {
+ return &InternalTestNotFoundError{APIError: apiError}
+ },
+ })
+
+ tests := []struct {
+ description string
+ giveStatusCode int
+ giveHeader http.Header
+ giveBody string
+ wantError error
+ }{
+ {
+ description: "unrecognized status code",
+ giveStatusCode: http.StatusInternalServerError,
+ giveHeader: http.Header{},
+ giveBody: "Internal Server Error",
+ wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
+ },
+ {
+ description: "not found with valid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `{"message": "Resource not found"}`,
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
+ Message: "Resource not found",
+ },
+ },
+ {
+ description: "not found with invalid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `Resource not found`,
+ wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
+ })
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/explicit_fields.go b/seed/go-sdk/idempotency-headers/internal/explicit_fields.go
new file mode 100644
index 000000000000..4bdf34fc2b7c
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/explicit_fields.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+ "math/big"
+ "reflect"
+ "strings"
+)
+
+// HandleExplicitFields processes a struct to remove `omitempty` from
+// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
+// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
+// Returns an interface{} that can be passed to json.Marshal.
+func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
+ val := reflect.ValueOf(marshaler)
+ typ := reflect.TypeOf(marshaler)
+
+ // Handle pointer types
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil
+ }
+ val = val.Elem()
+ typ = typ.Elem()
+ }
+
+ // Only handle struct types
+ if val.Kind() != reflect.Struct {
+ return marshaler
+ }
+
+ // Handle embedded struct pattern
+ var sourceVal reflect.Value
+ var sourceType reflect.Type
+
+ // Check if this is an embedded struct pattern
+ if typ.NumField() == 1 && typ.Field(0).Anonymous {
+ // This is likely an embedded struct, get the embedded value
+ embeddedField := val.Field(0)
+ sourceVal = embeddedField
+ sourceType = embeddedField.Type()
+ } else {
+ // Regular struct
+ sourceVal = val
+ sourceType = typ
+ }
+
+ // If no explicit fields set, use standard marshaling
+ if explicitFields == nil || explicitFields.Sign() == 0 {
+ return marshaler
+ }
+
+ // Create a new struct type with modified tags
+ fields := make([]reflect.StructField, 0, sourceType.NumField())
+
+ for i := 0; i < sourceType.NumField(); i++ {
+ field := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !field.IsExported() || field.Name == "explicitFields" {
+ continue
+ }
+
+ // Check if this field has been explicitly set
+ fieldBit := big.NewInt(1)
+ fieldBit.Lsh(fieldBit, uint(i))
+ if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
+ // Remove omitempty from the json tag
+ tag := field.Tag.Get("json")
+ if tag != "" && tag != "-" {
+ // Parse the json tag, remove omitempty from options
+ parts := strings.Split(tag, ",")
+ if len(parts) > 1 {
+ var newParts []string
+ newParts = append(newParts, parts[0]) // Keep the field name
+ for _, part := range parts[1:] {
+ if strings.TrimSpace(part) != "omitempty" {
+ newParts = append(newParts, part)
+ }
+ }
+ tag = strings.Join(newParts, ",")
+ }
+
+ // Reconstruct the struct tag
+ newTag := `json:"` + tag + `"`
+ if urlTag := field.Tag.Get("url"); urlTag != "" {
+ newTag += ` url:"` + urlTag + `"`
+ }
+
+ field.Tag = reflect.StructTag(newTag)
+ }
+ }
+
+ fields = append(fields, field)
+ }
+
+ // Create new struct type with modified tags
+ newType := reflect.StructOf(fields)
+ newVal := reflect.New(newType).Elem()
+
+ // Copy field values from original struct to new struct
+ fieldIndex := 0
+ for i := 0; i < sourceType.NumField(); i++ {
+ originalField := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !originalField.IsExported() || originalField.Name == "explicitFields" {
+ continue
+ }
+
+ originalValue := sourceVal.Field(i)
+ newVal.Field(fieldIndex).Set(originalValue)
+ fieldIndex++
+ }
+
+ return newVal.Interface()
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go b/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
new file mode 100644
index 000000000000..3d05e88a2ce9
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
@@ -0,0 +1,497 @@
+package internal
+
+import (
+ "encoding/json"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testExplicitFieldsStruct struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+ Count *int `json:"count,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ //lint:ignore unused this field is intentionally unused for testing
+ unexported string `json:"-"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ testFieldName = big.NewInt(1 << 0)
+ testFieldCode = big.NewInt(1 << 1)
+ testFieldCount = big.NewInt(1 << 2)
+ testFieldEnabled = big.NewInt(1 << 3)
+ testFieldTags = big.NewInt(1 << 4)
+)
+
+func (t *testExplicitFieldsStruct) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+func (t *testExplicitFieldsStruct) SetName(name *string) {
+ t.Name = name
+ t.require(testFieldName)
+}
+
+func (t *testExplicitFieldsStruct) SetCode(code *string) {
+ t.Code = code
+ t.require(testFieldCode)
+}
+
+func (t *testExplicitFieldsStruct) SetCount(count *int) {
+ t.Count = count
+ t.require(testFieldCount)
+}
+
+func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
+ t.Enabled = enabled
+ t.require(testFieldEnabled)
+}
+
+func (t *testExplicitFieldsStruct) SetTags(tags []string) {
+ t.Tags = tags
+ t.require(testFieldTags)
+}
+
+func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*t),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
+}
+
+type testStructWithoutExplicitFields struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+}
+
+func TestHandleExplicitFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveInput interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "nil input",
+ giveInput: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "non-struct input",
+ giveInput: "string",
+ wantBytes: []byte(`"string"`),
+ },
+ {
+ desc: "slice input",
+ giveInput: []string{"a", "b"},
+ wantBytes: []byte(`["a","b"]`),
+ },
+ {
+ desc: "map input",
+ giveInput: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "struct without explicitFields field",
+ giveInput: &testStructWithoutExplicitFields{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with no explicit fields set",
+ giveInput: &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with explicit nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null}`),
+ },
+ {
+ desc: "struct with explicit non-nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("explicit"))
+ s.SetCode(stringPtr("also-explicit"))
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
+ },
+ {
+ desc: "struct with mixed explicit and implicit fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Count: intPtr(42),
+ }
+ s.SetCode(nil) // explicit nil
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
+ },
+ {
+ desc: "struct with multiple explicit nil fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ s.SetCount(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
+ },
+ {
+ desc: "struct with slice field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Tags: []string{"tag1", "tag2"},
+ }
+ s.SetTags(nil) // explicit nil slice
+ return s
+ }(),
+ wantBytes: []byte(`{"tags":null}`),
+ },
+ {
+ desc: "struct with boolean field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetEnabled(boolPtr(false)) // explicit false
+ return s
+ }(),
+ wantBytes: []byte(`{"enabled":false}`),
+ },
+ {
+ desc: "struct with all fields explicit",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("test"))
+ s.SetCode(nil)
+ s.SetCount(intPtr(0))
+ s.SetEnabled(boolPtr(false))
+ s.SetTags([]string{})
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ var explicitFields *big.Int
+ if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
+ explicitFields = s.explicitFields
+ }
+ bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
+ t.Run("custom marshaler with explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
+ t.Run("nil pointer", func(t *testing.T) {
+ var s *testExplicitFieldsStruct
+ bytes, err := json.Marshal(HandleExplicitFields(s, nil))
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`null`), bytes)
+ })
+
+ t.Run("pointer to struct", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
+ t.Run("embedded struct with explicit fields", func(t *testing.T) {
+ // Create a struct similar to what MarshalJSON creates
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include both explicit fields (name as null, code as "test-code")
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should only include non-nil fields (omitempty behavior)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with mixed fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Count: intPtr(42), // implicit field
+ }
+ s.SetName(nil) // explicit nil
+ s.SetCode(stringPtr("explicit")) // explicit value
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include explicit null, explicit value, and implicit value
+ assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsTagHandling(t *testing.T) {
+ type testStructWithComplexTags struct {
+ Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
+ Field2 *string `json:"field2,omitempty,string" url:"field2"`
+ Field3 *string `json:"-"`
+ Field4 *string `json:"field4"`
+ explicitFields *big.Int `json:"-"`
+ }
+
+ s := &testStructWithComplexTags{
+ Field1: stringPtr("test1"),
+ Field4: stringPtr("test4"),
+ explicitFields: big.NewInt(1), // Only first field is explicit
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+
+ // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
+ assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
+}
+
+// Test types for nested struct explicit fields testing
+type testNestedStruct struct {
+ NestedName *string `json:"nested_name,omitempty"`
+ NestedCode *string `json:"nested_code,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+type testParentStruct struct {
+ ParentName *string `json:"parent_name,omitempty"`
+ Nested *testNestedStruct `json:"nested,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ nestedFieldName = big.NewInt(1 << 0)
+ nestedFieldCode = big.NewInt(1 << 1)
+)
+
+var (
+ parentFieldName = big.NewInt(1 << 0)
+ parentFieldNested = big.NewInt(1 << 1)
+)
+
+func (n *testNestedStruct) require(field *big.Int) {
+ if n.explicitFields == nil {
+ n.explicitFields = big.NewInt(0)
+ }
+ n.explicitFields.Or(n.explicitFields, field)
+}
+
+func (n *testNestedStruct) SetNestedName(name *string) {
+ n.NestedName = name
+ n.require(nestedFieldName)
+}
+
+func (n *testNestedStruct) SetNestedCode(code *string) {
+ n.NestedCode = code
+ n.require(nestedFieldCode)
+}
+
+func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
+ type embed testNestedStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*n),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
+}
+
+func (p *testParentStruct) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+func (p *testParentStruct) SetParentName(name *string) {
+ p.ParentName = name
+ p.require(parentFieldName)
+}
+
+func (p *testParentStruct) SetNested(nested *testNestedStruct) {
+ p.Nested = nested
+ p.require(parentFieldNested)
+}
+
+func (p *testParentStruct) MarshalJSON() ([]byte, error) {
+ type embed testParentStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
+}
+
+func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
+ tests := []struct {
+ desc string
+ setupFunc func() *testParentStruct
+ wantBytes []byte
+ }{
+ {
+ desc: "nested struct with explicit nil in nested object",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{
+ NestedName: stringPtr("implicit-nested"),
+ }
+ nested.SetNestedCode(nil) // explicit nil
+
+ return &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ Nested: nested,
+ }
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
+ },
+ {
+ desc: "parent with explicit nil nested struct",
+ setupFunc: func() *testParentStruct {
+ parent := &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ }
+ parent.SetNested(nil) // explicit nil nested struct
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
+ },
+ {
+ desc: "all explicit fields in nested structure",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{}
+ nested.SetNestedName(stringPtr("explicit-nested"))
+ nested.SetNestedCode(nil) // explicit nil
+
+ parent := &testParentStruct{}
+ parent.SetParentName(nil) // explicit nil
+ parent.SetNested(nested) // explicit nested struct
+
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ parent := tt.setupFunc()
+ bytes, err := parent.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/extra_properties.go b/seed/go-sdk/idempotency-headers/internal/extra_properties.go
new file mode 100644
index 000000000000..540c3fd89eeb
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]interface{}
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value interface{}) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go b/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
new file mode 100644
index 000000000000..aa2510ee5121
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler interface{}
+ giveExtraProperties map[string]interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]interface{}{42: "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]interface{}{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/http.go b/seed/go-sdk/idempotency-headers/internal/http.go
new file mode 100644
index 000000000000..77863752bb58
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/http.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+)
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// ResolveBaseURL resolves the base URL from the given arguments,
+// preferring the first non-empty value.
+func ResolveBaseURL(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// EncodeURL encodes the given arguments into the URL, escaping
+// values as needed. Pointer arguments are dereferenced before processing.
+func EncodeURL(urlFormat string, args ...interface{}) string {
+ escapedArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ // Dereference the argument if it's a pointer
+ value := dereferenceArg(arg)
+ escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
+ }
+ return fmt.Sprintf(urlFormat, escapedArgs...)
+}
+
+// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
+// If the argument is not a pointer or is nil, it returns the argument as-is.
+func dereferenceArg(arg interface{}) interface{} {
+ if arg == nil {
+ return arg
+ }
+
+ v := reflect.ValueOf(arg)
+
+ // Keep dereferencing until we get to a non-pointer value or hit nil
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return nil
+ }
+ v = v.Elem()
+ }
+
+ return v.Interface()
+}
+
+// MergeHeaders merges the given headers together, where the right
+// takes precedence over the left.
+func MergeHeaders(left, right http.Header) http.Header {
+ for key, values := range right {
+ if len(values) > 1 {
+ left[key] = values
+ continue
+ }
+ if value := right.Get(key); value != "" {
+ left.Set(key, value)
+ }
+ }
+ return left
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/query.go b/seed/go-sdk/idempotency-headers/internal/query.go
new file mode 100644
index 000000000000..1cbaf7fe1c02
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/query.go
@@ -0,0 +1,353 @@
+package internal
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+var (
+ bytesType = reflect.TypeOf([]byte{})
+ queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ uuidType = reflect.TypeOf(uuid.UUID{})
+)
+
+// QueryEncoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type QueryEncoder interface {
+ EncodeQueryValues(key string, v *url.Values) error
+}
+
+// prepareValue handles common validation and unwrapping logic for both functions
+func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return reflect.Value{}, values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return reflect.Value{}, values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ if err != nil {
+ return reflect.Value{}, nil, err
+ }
+
+ return val, values, nil
+}
+
+// QueryValues encodes url.Values from request objects.
+//
+// Note: This type is inspired by Google's query encoding library, but
+// supports far less customization and is tailored to fit this SDK's use case.
+//
+// Ref: https://github.com/google/go-querystring
+func QueryValues(v interface{}) (url.Values, error) {
+ _, values, err := prepareValue(v)
+ return values, err
+}
+
+// QueryValuesWithDefaults encodes url.Values from request objects
+// and default values, merging the defaults into the request.
+// It's expected that the values of defaults are wire names.
+func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
+ val, values, err := prepareValue(v)
+ if err != nil {
+ return values, err
+ }
+ if !val.IsValid() {
+ return values, nil
+ }
+
+ // apply defaults to zero-value fields directly on the original struct
+ valType := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Field(i)
+ fieldType := valType.Field(i)
+ fieldName := fieldType.Name
+
+ if fieldType.PkgPath != "" && !fieldType.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ // check if field is zero value and we have a default for it
+ if field.CanSet() && field.IsZero() {
+ tag := fieldType.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+ wireName, _ := parseTag(tag)
+ if wireName == "" {
+ wireName = fieldName
+ }
+ if defaultVal, exists := defaults[wireName]; exists {
+ values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
+ }
+ }
+ }
+
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+
+ name, opts := parseTag(tag)
+ if name == "" {
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(queryEncoderType) {
+ // If sv is a nil pointer and the custom encoder is defined on a non-pointer
+ // method receiver, set sv to the zero value of the underlying type
+ if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(QueryEncoder)
+ if err := m.EncodeQueryValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ // Recursively dereference pointers, but stop at nil pointers.
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts, sf))
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ if sv.Len() == 0 {
+ // Skip if slice or array is empty.
+ continue
+ }
+ for i := 0; i < sv.Len(); i++ {
+ value := sv.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), name); err != nil {
+ return err
+ }
+ } else {
+ values.Add(name, valueString(value, opts, sf))
+ }
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Map {
+ if err := reflectMap(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ if err := reflectValue(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts, sf))
+ }
+
+ return nil
+}
+
+// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
+func reflectMap(values url.Values, val reflect.Value, scope string) error {
+ if val.IsNil() {
+ return nil
+ }
+
+ iter := val.MapRange()
+ for iter.Next() {
+ k := iter.Key()
+ v := iter.Value()
+
+ key := fmt.Sprint(k.Interface())
+ paramName := scope + "[" + key + "]"
+
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ break
+ }
+ v = v.Elem()
+ }
+
+ for v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Map {
+ if err := reflectMap(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Struct {
+ if err := reflectValue(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ if v.Len() == 0 {
+ continue
+ }
+ for i := 0; i < v.Len(); i++ {
+ value := v.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), paramName); err != nil {
+ return err
+ }
+ } else {
+ values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
+ }
+ }
+ continue
+ }
+
+ values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if format := sf.Tag.Get("format"); format == "date" {
+ return t.Format("2006-01-02")
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ if v.Type() == uuidType {
+ u := v.Interface().(uuid.UUID)
+ return u.String()
+ }
+
+ if v.Type() == bytesType {
+ b := v.Interface().([]byte)
+ return base64.StdEncoding.EncodeToString(b)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ type zeroable interface {
+ IsZero() bool
+ }
+
+ if !v.IsZero() {
+ if z, ok := v.Interface().(zeroable); ok {
+ return z.IsZero()
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
+ return false
+ }
+
+ return false
+}
+
+// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
+func isStructPointer(v reflect.Value) bool {
+ return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/query_test.go b/seed/go-sdk/idempotency-headers/internal/query_test.go
new file mode 100644
index 000000000000..2c28cb8acf68
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/query_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryValues(t *testing.T) {
+ t.Run("empty optional", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+
+ t.Run("empty required", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Equal(t, "required=", values.Encode())
+ })
+
+ t.Run("allow multiple", func(t *testing.T) {
+ type example struct {
+ Values []string `json:"values" url:"values"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Values: []string{"foo", "bar", "baz"},
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
+ })
+
+ t.Run("nested object", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ nestedValue := "nestedValue"
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ Nested: &nested{
+ Value: &nestedValue,
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
+ })
+
+ t.Run("url unspecified", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("url ignored", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound" url:"-"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("datetime", func(t *testing.T) {
+ type example struct {
+ DateTime time.Time `json:"dateTime" url:"dateTime"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
+ })
+
+ t.Run("date", func(t *testing.T) {
+ type example struct {
+ Date time.Time `json:"date" url:"date" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "date=1994-03-16", values.Encode())
+ })
+
+ t.Run("optional time", func(t *testing.T) {
+ type example struct {
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
+ type enum string
+
+ type example struct {
+ Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("object array", func(t *testing.T) {
+ type object struct {
+ Key string `json:"key" url:"key"`
+ Value string `json:"value" url:"value"`
+ }
+ type example struct {
+ Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Objects: []*object{
+ {
+ Key: "hello",
+ Value: "world",
+ },
+ {
+ Key: "foo",
+ Value: "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
+ })
+
+ t.Run("map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "foo": "bar",
+ "baz": "qux",
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map array", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": []string{
+ "one",
+ "two",
+ "three",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
+ })
+}
+
+func TestQueryValuesWithDefaults(t *testing.T) {
+ t.Run("apply defaults to zero values", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ })
+
+ t.Run("preserve non-zero values over defaults", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Name: "actual-name",
+ Age: 30,
+ // Enabled remains false (zero value), should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
+ })
+
+ t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "nonexistent": "should-be-ignored",
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&name=default-name", values.Encode())
+ })
+
+ t.Run("type conversion for compatible defaults", func(t *testing.T) {
+ type example struct {
+ Count int64 `json:"count" url:"count"`
+ Rate float64 `json:"rate" url:"rate"`
+ Message string `json:"message" url:"message"`
+ }
+
+ defaults := map[string]interface{}{
+ "count": int(100), // int -> int64 conversion
+ "rate": float32(2.5), // float32 -> float64 conversion
+ "message": "hello", // string -> string (no conversion needed)
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
+ })
+
+ t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
+ Count int `json:"count,omitempty" url:"count,omitempty"`
+ }
+
+ defaultOptional := "default-optional"
+ defaults := map[string]interface{}{
+ "required": "default-required",
+ "optional": &defaultOptional, // pointer type
+ "count": 42,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Required: "custom-required", // should override default
+ // Optional is nil, should get default
+ // Count is 0, should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
+ })
+
+ t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
+ type example struct {
+ Name *string `json:"name" url:"name"`
+ Age *int `json:"age" url:"age"`
+ Enabled *bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ // first, test that a properly empty request is overridden:
+ {
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ }
+
+ // second, test that a request that contains zeros is not overridden:
+ var (
+ name = ""
+ age = 0
+ enabled = false
+ )
+ values, err := QueryValuesWithDefaults(&example{
+ Name: &name, // explicit empty string should override default
+ Age: &age, // explicit zero should override default
+ Enabled: &enabled, // explicit false should override default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
+ })
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/retrier.go b/seed/go-sdk/idempotency-headers/internal/retrier.go
new file mode 100644
index 000000000000..4efae1b4c286
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/retrier.go
@@ -0,0 +1,230 @@
+package internal
+
+import (
+ "crypto/rand"
+ "math/big"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+const (
+ defaultRetryAttempts = 2
+ minRetryDelay = 1000 * time.Millisecond
+ maxRetryDelay = 60000 * time.Millisecond
+)
+
+// RetryOption adapts the behavior the *Retrier.
+type RetryOption func(*retryOptions)
+
+// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
+type RetryFunc func(*http.Request) (*http.Response, error)
+
+// WithMaxAttempts configures the maximum number of attempts
+// of the *Retrier.
+func WithMaxAttempts(attempts uint) RetryOption {
+ return func(opts *retryOptions) {
+ opts.attempts = attempts
+ }
+}
+
+// Retrier retries failed requests a configurable number of times with an
+// exponential back-off between each retry.
+type Retrier struct {
+ attempts uint
+}
+
+// NewRetrier constructs a new *Retrier with the given options, if any.
+func NewRetrier(opts ...RetryOption) *Retrier {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ attempts := uint(defaultRetryAttempts)
+ if options.attempts > 0 {
+ attempts = options.attempts
+ }
+ return &Retrier{
+ attempts: attempts,
+ }
+}
+
+// Run issues the request and, upon failure, retries the request if possible.
+//
+// The request will be retried as long as the request is deemed retryable and the
+// number of retry attempts has not grown larger than the configured retry limit.
+func (r *Retrier) Run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ opts ...RetryOption,
+) (*http.Response, error) {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ maxRetryAttempts := r.attempts
+ if options.attempts > 0 {
+ maxRetryAttempts = options.attempts
+ }
+ var (
+ retryAttempt uint
+ previousError error
+ )
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt,
+ previousError,
+ )
+}
+
+func (r *Retrier) run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ maxRetryAttempts uint,
+ retryAttempt uint,
+ previousError error,
+) (*http.Response, error) {
+ if retryAttempt >= maxRetryAttempts {
+ return nil, previousError
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := request.Context().Err(); err != nil {
+ return nil, err
+ }
+
+ response, err := fn(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.shouldRetry(response) {
+ defer response.Body.Close()
+
+ delay, err := r.retryDelay(response, retryAttempt)
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(delay)
+
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt+1,
+ decodeError(response, errorDecoder),
+ )
+ }
+
+ return response, nil
+}
+
+// shouldRetry returns true if the request should be retried based on the given
+// response status code.
+func (r *Retrier) shouldRetry(response *http.Response) bool {
+ return response.StatusCode == http.StatusTooManyRequests ||
+ response.StatusCode == http.StatusRequestTimeout ||
+ response.StatusCode >= http.StatusInternalServerError
+}
+
+// retryDelay calculates the delay time based on response headers,
+// falling back to exponential backoff if no headers are present.
+func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
+ // Check for Retry-After header first (RFC 7231), applying no jitter
+ if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
+ // Parse as number of seconds...
+ if seconds, err := strconv.Atoi(retryAfter); err == nil {
+ delay := time.Duration(seconds) * time.Second
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+
+ // ...or as an HTTP date; both are valid
+ if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
+ delay := time.Until(retryTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+ }
+
+ // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
+ if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
+ if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
+ // Assume Unix timestamp in seconds
+ resetTime := time.Unix(resetTimestamp, 0)
+ delay := time.Until(resetTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return r.addPositiveJitter(delay)
+ }
+ }
+ }
+
+ // Fall back to exponential backoff
+ return r.exponentialBackoff(retryAttempt)
+}
+
+// exponentialBackoff calculates the delay time based on the retry attempt
+// and applies symmetric jitter (±10% around the delay).
+func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
+ if retryAttempt > 63 { // 2^63+ would overflow uint64
+ retryAttempt = 63
+ }
+
+ delay := minRetryDelay << retryAttempt
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+
+ return r.addSymmetricJitter(delay)
+}
+
+// addJitterWithRange applies jitter to the given delay.
+// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
+func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
+ jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
+ jitter, err := rand.Int(rand.Reader, jitterRange)
+ if err != nil {
+ return 0, err
+ }
+
+ jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
+ if jitteredDelay < minRetryDelay {
+ jitteredDelay = minRetryDelay
+ }
+ if jitteredDelay > maxRetryDelay {
+ jitteredDelay = maxRetryDelay
+ }
+ return jitteredDelay, nil
+}
+
+// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
+func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 100, 120)
+}
+
+// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
+func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 90, 110)
+}
+
+type retryOptions struct {
+ attempts uint
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/retrier_test.go b/seed/go-sdk/idempotency-headers/internal/retrier_test.go
new file mode 100644
index 000000000000..31768996da2b
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/retrier_test.go
@@ -0,0 +1,300 @@
+package internal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/idempotency-headers/fern/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type RetryTestCase struct {
+ description string
+
+ giveAttempts uint
+ giveStatusCodes []int
+ giveResponse *InternalTestResponse
+
+ wantResponse *InternalTestResponse
+ wantError *core.APIError
+}
+
+func TestRetrier(t *testing.T) {
+ tests := []*RetryTestCase{
+ {
+ description: "retry request succeeds after multiple failures",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ giveResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ },
+ {
+ description: "retry request fails if MaxAttempts is exceeded",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusOK,
+ },
+ wantError: &core.APIError{
+ StatusCode: http.StatusRequestTimeout,
+ },
+ },
+ {
+ description: "retry durations increase exponentially and stay within the min and max delay values",
+ giveAttempts: 4,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ },
+ {
+ description: "retry does not occur on status code 404",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
+ wantError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ },
+ {
+ description: "retries occur on status code 429",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 408",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 500",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.description, func(t *testing.T) {
+ var (
+ test = tc
+ server = newTestRetryServer(t, test)
+ client = server.Client()
+ )
+
+ t.Parallel()
+
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: test.giveAttempts,
+ ResponseIsOptional: true,
+ },
+ )
+
+ if test.wantError != nil {
+ require.IsType(t, err, &core.APIError{})
+ expectedErrorCode := test.wantError.StatusCode
+ actualErrorCode := err.(*core.APIError).StatusCode
+ assert.Equal(t, expectedErrorCode, actualErrorCode)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+// newTestRetryServer returns a new *httptest.Server configured with the
+// given test parameters, suitable for testing retries.
+func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
+ var index int
+ timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
+
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if index > 0 && index < len(expectedRetryDurations) {
+ // Ensure that the duration between retries increases exponentially,
+ // and that it is within the minimum and maximum retry delay values.
+ actualDuration := timestamps[index].Sub(timestamps[index-1])
+ expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
+ expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
+ assert.True(
+ t,
+ actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
+ "expected duration to be in range [%v, %v], got %v",
+ expectedDurationMin,
+ expectedDurationMax,
+ actualDuration,
+ )
+ assert.LessOrEqual(
+ t,
+ actualDuration,
+ maxRetryDelay,
+ "expected duration to be less than the maxRetryDelay (%v), got %v",
+ maxRetryDelay,
+ actualDuration,
+ )
+ assert.GreaterOrEqual(
+ t,
+ actualDuration,
+ minRetryDelay,
+ "expected duration to be greater than the minRetryDelay (%v), got %v",
+ minRetryDelay,
+ actualDuration,
+ )
+ }
+
+ request := new(InternalTestRequest)
+ bytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+ require.LessOrEqual(t, index, len(tc.giveStatusCodes))
+
+ statusCode := tc.giveStatusCodes[index]
+
+ w.WriteHeader(statusCode)
+
+ if tc.giveResponse != nil && statusCode == http.StatusOK {
+ bytes, err = json.Marshal(tc.giveResponse)
+ require.NoError(t, err)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ }
+
+ index++
+ },
+ ),
+ )
+}
+
+// expectedRetryDurations holds an array of calculated retry durations,
+// where the index of the array should correspond to the retry attempt.
+//
+// Values are calculated based off of `minRetryDelay * 2^i`.
+var expectedRetryDurations = []time.Duration{
+ 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
+ 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
+ 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
+ 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
+}
+
+func TestRetryDelayTiming(t *testing.T) {
+ tests := []struct {
+ name string
+ headerName string
+ headerValueFunc func() string
+ expectedMinMs int64
+ expectedMaxMs int64
+ }{
+ {
+ name: "retry-after with seconds value",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return "1"
+ },
+ expectedMinMs: 500,
+ expectedMaxMs: 1500,
+ },
+ {
+ name: "retry-after with HTTP date",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return time.Now().Add(3 * time.Second).Format(time.RFC1123)
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ {
+ name: "x-ratelimit-reset with future timestamp",
+ headerName: "x-ratelimit-reset",
+ headerValueFunc: func() string {
+ return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ var timestamps []time.Time
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if len(timestamps) == 1 {
+ // First request - return retryable error with header
+ w.Header().Set(tt.headerName, tt.headerValueFunc())
+ w.WriteHeader(http.StatusTooManyRequests)
+ } else {
+ // Second request - return success
+ w.WriteHeader(http.StatusOK)
+ response := &InternalTestResponse{Id: "success"}
+ bytes, _ := json.Marshal(response)
+ w.Write(bytes)
+ }
+ }))
+ defer server.Close()
+
+ caller := NewCaller(&CallerParams{
+ Client: server.Client(),
+ })
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: 2,
+ ResponseIsOptional: true,
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, timestamps, 2, "Expected exactly 2 requests")
+
+ actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
+
+ assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
+ "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
+ assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
+ "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
+ })
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/stringer.go b/seed/go-sdk/idempotency-headers/internal/stringer.go
new file mode 100644
index 000000000000..312801851e0e
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value interface{}) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-sdk/idempotency-headers/internal/time.go b/seed/go-sdk/idempotency-headers/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go b/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
new file mode 100644
index 000000000000..ca6d4d4b62c8
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
@@ -0,0 +1,24 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/idempotency-headers/fern/core"
+)
+
+// IdempotentRequestOption adapts the behavior of an individual request.
+type IdempotentRequestOption = core.IdempotentRequestOption
+
+// WithIdempotencyKey sets the idempotencyKey request header.
+func WithIdempotencyKey(idempotencyKey string) *core.IdempotencyKeyOption {
+ return &core.IdempotencyKeyOption{
+ IdempotencyKey: idempotencyKey,
+ }
+}
+
+// WithIdempotencyExpiration sets the idempotencyExpiration request header.
+func WithIdempotencyExpiration(idempotencyExpiration int) *core.IdempotencyExpirationOption {
+ return &core.IdempotencyExpirationOption{
+ IdempotencyExpiration: idempotencyExpiration,
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/option/request_option.go b/seed/go-sdk/idempotency-headers/option/request_option.go
new file mode 100644
index 000000000000..395b097fbc69
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/option/request_option.go
@@ -0,0 +1,71 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/idempotency-headers/fern/core"
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of an individual request.
+type RequestOption = core.RequestOption
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return &core.BaseURLOption{
+ BaseURL: baseURL,
+ }
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return &core.HTTPClientOption{
+ HTTPClient: httpClient,
+ }
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return &core.HTTPHeaderOption{
+ // Clone the headers so they can't be modified after the option call.
+ HTTPHeader: httpHeader.Clone(),
+ }
+}
+
+// WithBodyProperties adds the given body properties to the request.
+func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
+ copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
+ for key, value := range bodyProperties {
+ copiedBodyProperties[key] = value
+ }
+ return &core.BodyPropertiesOption{
+ BodyProperties: copiedBodyProperties,
+ }
+}
+
+// WithQueryParameters adds the given query parameters to the request.
+func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
+ copiedQueryParameters := make(url.Values, len(queryParameters))
+ for key, values := range queryParameters {
+ copiedQueryParameters[key] = values
+ }
+ return &core.QueryParametersOption{
+ QueryParameters: copiedQueryParameters,
+ }
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return &core.MaxAttemptsOption{
+ MaxAttempts: attempts,
+ }
+}
+
+// WithToken sets the 'Authorization: Bearer ' request header.
+func WithToken(token string) *core.TokenOption {
+ return &core.TokenOption{
+ Token: token,
+ }
+}
diff --git a/seed/go-sdk/idempotency-headers/payment/client.go b/seed/go-sdk/idempotency-headers/payment/client.go
new file mode 100644
index 000000000000..c254ebcafd77
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/payment/client.go
@@ -0,0 +1,66 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package payment
+
+import (
+ context "context"
+ uuid "github.com/google/uuid"
+ fern "github.com/idempotency-headers/fern"
+ core "github.com/idempotency-headers/fern/core"
+ internal "github.com/idempotency-headers/fern/internal"
+ option "github.com/idempotency-headers/fern/option"
+)
+
+type Client struct {
+ WithRawResponse *RawClient
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(options *core.RequestOptions) *Client {
+ return &Client{
+ WithRawResponse: NewRawClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (c *Client) Create(
+ ctx context.Context,
+ request *fern.CreatePaymentRequest,
+ opts ...option.IdempotentRequestOption,
+) (uuid.UUID, error) {
+ response, err := c.WithRawResponse.Create(
+ ctx,
+ request,
+ opts...,
+ )
+ if err != nil {
+ return uuid.UUID{}, err
+ }
+ return response.Body, nil
+}
+
+func (c *Client) Delete(
+ ctx context.Context,
+ paymentId string,
+ opts ...option.RequestOption,
+) error {
+ _, err := c.WithRawResponse.Delete(
+ ctx,
+ paymentId,
+ opts...,
+ )
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/seed/go-sdk/idempotency-headers/payment/raw_client.go b/seed/go-sdk/idempotency-headers/payment/raw_client.go
new file mode 100644
index 000000000000..f86f02e8c882
--- /dev/null
+++ b/seed/go-sdk/idempotency-headers/payment/raw_client.go
@@ -0,0 +1,114 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package payment
+
+import (
+ context "context"
+ uuid "github.com/google/uuid"
+ fern "github.com/idempotency-headers/fern"
+ core "github.com/idempotency-headers/fern/core"
+ internal "github.com/idempotency-headers/fern/internal"
+ option "github.com/idempotency-headers/fern/option"
+ http "net/http"
+)
+
+type RawClient struct {
+ baseURL string
+ caller *internal.Caller
+ options *core.RequestOptions
+}
+
+func NewRawClient(options *core.RequestOptions) *RawClient {
+ return &RawClient{
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (r *RawClient) Create(
+ ctx context.Context,
+ request *fern.CreatePaymentRequest,
+ opts ...option.IdempotentRequestOption,
+) (*core.Response[uuid.UUID], error) {
+ options := core.NewIdempotentRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ r.baseURL,
+ "",
+ )
+ endpointURL := baseURL + "/payment"
+ headers := internal.MergeHeaders(
+ r.options.ToHeader(),
+ options.ToHeader(),
+ )
+ var response uuid.UUID
+ raw, err := r.caller.Call(
+ ctx,
+ &internal.CallParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ Request: request,
+ Response: &response,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &core.Response[uuid.UUID]{
+ StatusCode: raw.StatusCode,
+ Header: raw.Header,
+ Body: response,
+ }, nil
+}
+
+func (r *RawClient) Delete(
+ ctx context.Context,
+ paymentId string,
+ opts ...option.RequestOption,
+) (*core.Response[any], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ r.baseURL,
+ "",
+ )
+ endpointURL := internal.EncodeURL(
+ baseURL+"/payment/%v",
+ paymentId,
+ )
+ headers := internal.MergeHeaders(
+ r.options.ToHeader(),
+ options.ToHeader(),
+ )
+ raw, err := r.caller.Call(
+ ctx,
+ &internal.CallParams{
+ URL: endpointURL,
+ Method: http.MethodDelete,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &core.Response[any]{
+ StatusCode: raw.StatusCode,
+ Header: raw.Header,
+ Body: nil,
+ }, nil
+}
diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query.go
+++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query_test.go b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query_test.go
+++ b/seed/go-sdk/imdb/deep-package-path/all/the/way/in/here/please/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/imdb/no-custom-config/internal/query.go b/seed/go-sdk/imdb/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/imdb/no-custom-config/internal/query.go
+++ b/seed/go-sdk/imdb/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/imdb/no-custom-config/internal/query_test.go b/seed/go-sdk/imdb/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/imdb/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/imdb/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/imdb/package-path/inhereplease/internal/query.go b/seed/go-sdk/imdb/package-path/inhereplease/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/imdb/package-path/inhereplease/internal/query.go
+++ b/seed/go-sdk/imdb/package-path/inhereplease/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/imdb/package-path/inhereplease/internal/query_test.go b/seed/go-sdk/imdb/package-path/inhereplease/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/imdb/package-path/inhereplease/internal/query_test.go
+++ b/seed/go-sdk/imdb/package-path/inhereplease/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/imdb/with-wiremock-tests/internal/query.go b/seed/go-sdk/imdb/with-wiremock-tests/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/imdb/with-wiremock-tests/internal/query.go
+++ b/seed/go-sdk/imdb/with-wiremock-tests/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/imdb/with-wiremock-tests/internal/query_test.go b/seed/go-sdk/imdb/with-wiremock-tests/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/imdb/with-wiremock-tests/internal/query_test.go
+++ b/seed/go-sdk/imdb/with-wiremock-tests/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/inferred-auth-explicit/internal/query.go b/seed/go-sdk/inferred-auth-explicit/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/inferred-auth-explicit/internal/query.go
+++ b/seed/go-sdk/inferred-auth-explicit/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/inferred-auth-explicit/internal/query_test.go b/seed/go-sdk/inferred-auth-explicit/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/inferred-auth-explicit/internal/query_test.go
+++ b/seed/go-sdk/inferred-auth-explicit/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query.go b/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query.go
+++ b/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query_test.go b/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query_test.go
+++ b/seed/go-sdk/inferred-auth-implicit-no-expiry/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/inferred-auth-implicit/internal/query.go b/seed/go-sdk/inferred-auth-implicit/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/inferred-auth-implicit/internal/query.go
+++ b/seed/go-sdk/inferred-auth-implicit/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/inferred-auth-implicit/internal/query_test.go b/seed/go-sdk/inferred-auth-implicit/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/inferred-auth-implicit/internal/query_test.go
+++ b/seed/go-sdk/inferred-auth-implicit/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/license/internal/query.go b/seed/go-sdk/license/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/license/internal/query.go
+++ b/seed/go-sdk/license/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/license/internal/query_test.go b/seed/go-sdk/license/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/license/internal/query_test.go
+++ b/seed/go-sdk/license/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/literals-unions/no-custom-config/internal/query.go b/seed/go-sdk/literals-unions/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/literals-unions/no-custom-config/internal/query.go
+++ b/seed/go-sdk/literals-unions/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/literals-unions/no-custom-config/internal/query_test.go b/seed/go-sdk/literals-unions/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/literals-unions/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/literals-unions/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/mixed-case/default-values/internal/query.go b/seed/go-sdk/mixed-case/default-values/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/mixed-case/default-values/internal/query.go
+++ b/seed/go-sdk/mixed-case/default-values/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/mixed-case/default-values/internal/query_test.go b/seed/go-sdk/mixed-case/default-values/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/mixed-case/default-values/internal/query_test.go
+++ b/seed/go-sdk/mixed-case/default-values/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/mixed-case/no-custom-config/internal/query.go b/seed/go-sdk/mixed-case/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/mixed-case/no-custom-config/internal/query.go
+++ b/seed/go-sdk/mixed-case/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/mixed-case/no-custom-config/internal/query_test.go b/seed/go-sdk/mixed-case/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/mixed-case/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/mixed-case/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/mixed-file-directory/internal/query.go b/seed/go-sdk/mixed-file-directory/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/mixed-file-directory/internal/query.go
+++ b/seed/go-sdk/mixed-file-directory/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/mixed-file-directory/internal/query_test.go b/seed/go-sdk/mixed-file-directory/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/mixed-file-directory/internal/query_test.go
+++ b/seed/go-sdk/mixed-file-directory/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/multi-line-docs/internal/query.go b/seed/go-sdk/multi-line-docs/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/multi-line-docs/internal/query.go
+++ b/seed/go-sdk/multi-line-docs/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/multi-line-docs/internal/query_test.go b/seed/go-sdk/multi-line-docs/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/multi-line-docs/internal/query_test.go
+++ b/seed/go-sdk/multi-line-docs/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/multi-url-environment-no-default/internal/query.go b/seed/go-sdk/multi-url-environment-no-default/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/multi-url-environment-no-default/internal/query.go
+++ b/seed/go-sdk/multi-url-environment-no-default/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/multi-url-environment-no-default/internal/query_test.go b/seed/go-sdk/multi-url-environment-no-default/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/multi-url-environment-no-default/internal/query_test.go
+++ b/seed/go-sdk/multi-url-environment-no-default/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/multi-url-environment/internal/query.go b/seed/go-sdk/multi-url-environment/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/multi-url-environment/internal/query.go
+++ b/seed/go-sdk/multi-url-environment/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/multi-url-environment/internal/query_test.go b/seed/go-sdk/multi-url-environment/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/multi-url-environment/internal/query_test.go
+++ b/seed/go-sdk/multi-url-environment/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/multiple-request-bodies/internal/query.go b/seed/go-sdk/multiple-request-bodies/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/multiple-request-bodies/internal/query.go
+++ b/seed/go-sdk/multiple-request-bodies/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/multiple-request-bodies/internal/query_test.go b/seed/go-sdk/multiple-request-bodies/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/multiple-request-bodies/internal/query_test.go
+++ b/seed/go-sdk/multiple-request-bodies/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/no-environment/internal/query.go b/seed/go-sdk/no-environment/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/no-environment/internal/query.go
+++ b/seed/go-sdk/no-environment/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/no-environment/internal/query_test.go b/seed/go-sdk/no-environment/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/no-environment/internal/query_test.go
+++ b/seed/go-sdk/no-environment/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/no-retries/internal/query.go b/seed/go-sdk/no-retries/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/no-retries/internal/query.go
+++ b/seed/go-sdk/no-retries/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/no-retries/internal/query_test.go b/seed/go-sdk/no-retries/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/no-retries/internal/query_test.go
+++ b/seed/go-sdk/no-retries/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/nullable-optional/internal/query.go b/seed/go-sdk/nullable-optional/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/nullable-optional/internal/query.go
+++ b/seed/go-sdk/nullable-optional/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/nullable-optional/internal/query_test.go b/seed/go-sdk/nullable-optional/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/nullable-optional/internal/query_test.go
+++ b/seed/go-sdk/nullable-optional/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query.go b/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query.go
+++ b/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query_test.go b/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query_test.go
+++ b/seed/go-sdk/nullable-request-body/dynamic-snippets-disabled/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/nullable/internal/query.go b/seed/go-sdk/nullable/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/nullable/internal/query.go
+++ b/seed/go-sdk/nullable/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/nullable/internal/query_test.go b/seed/go-sdk/nullable/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/nullable/internal/query_test.go
+++ b/seed/go-sdk/nullable/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials-custom/internal/query.go b/seed/go-sdk/oauth-client-credentials-custom/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials-custom/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials-custom/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials-custom/internal/query_test.go b/seed/go-sdk/oauth-client-credentials-custom/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials-custom/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials-custom/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials-default/internal/query.go b/seed/go-sdk/oauth-client-credentials-default/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials-default/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials-default/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials-default/internal/query_test.go b/seed/go-sdk/oauth-client-credentials-default/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials-default/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials-default/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query.go b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query_test.go b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials-nested-root/internal/query.go b/seed/go-sdk/oauth-client-credentials-nested-root/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials-nested-root/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials-nested-root/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials-nested-root/internal/query_test.go b/seed/go-sdk/oauth-client-credentials-nested-root/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials-nested-root/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials-nested-root/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials-with-variables/internal/query.go b/seed/go-sdk/oauth-client-credentials-with-variables/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials-with-variables/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials-with-variables/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials-with-variables/internal/query_test.go b/seed/go-sdk/oauth-client-credentials-with-variables/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials-with-variables/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials-with-variables/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/oauth-client-credentials/internal/query.go b/seed/go-sdk/oauth-client-credentials/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/oauth-client-credentials/internal/query.go
+++ b/seed/go-sdk/oauth-client-credentials/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/oauth-client-credentials/internal/query_test.go b/seed/go-sdk/oauth-client-credentials/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/oauth-client-credentials/internal/query_test.go
+++ b/seed/go-sdk/oauth-client-credentials/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/object/internal/query.go b/seed/go-sdk/object/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/object/internal/query.go
+++ b/seed/go-sdk/object/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/object/internal/query_test.go b/seed/go-sdk/object/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/object/internal/query_test.go
+++ b/seed/go-sdk/object/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/objects-with-imports/internal/query.go b/seed/go-sdk/objects-with-imports/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/objects-with-imports/internal/query.go
+++ b/seed/go-sdk/objects-with-imports/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/objects-with-imports/internal/query_test.go b/seed/go-sdk/objects-with-imports/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/objects-with-imports/internal/query_test.go
+++ b/seed/go-sdk/objects-with-imports/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/optional/internal/query.go b/seed/go-sdk/optional/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/optional/internal/query.go
+++ b/seed/go-sdk/optional/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/optional/internal/query_test.go b/seed/go-sdk/optional/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/optional/internal/query_test.go
+++ b/seed/go-sdk/optional/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/package-yml/no-custom-config/internal/query.go b/seed/go-sdk/package-yml/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/package-yml/no-custom-config/internal/query.go
+++ b/seed/go-sdk/package-yml/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/package-yml/no-custom-config/internal/query_test.go b/seed/go-sdk/package-yml/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/package-yml/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/package-yml/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/pagination/internal/query.go b/seed/go-sdk/pagination/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/pagination/internal/query.go
+++ b/seed/go-sdk/pagination/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/pagination/internal/query_test.go b/seed/go-sdk/pagination/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/pagination/internal/query_test.go
+++ b/seed/go-sdk/pagination/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/path-parameters/no-custom-config/internal/query.go b/seed/go-sdk/path-parameters/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/path-parameters/no-custom-config/internal/query.go
+++ b/seed/go-sdk/path-parameters/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/path-parameters/no-custom-config/internal/query_test.go b/seed/go-sdk/path-parameters/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/path-parameters/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/path-parameters/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/path-parameters/package-name/internal/query.go b/seed/go-sdk/path-parameters/package-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/path-parameters/package-name/internal/query.go
+++ b/seed/go-sdk/path-parameters/package-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/path-parameters/package-name/internal/query_test.go b/seed/go-sdk/path-parameters/package-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/path-parameters/package-name/internal/query_test.go
+++ b/seed/go-sdk/path-parameters/package-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/path-parameters/v0/internal/query.go b/seed/go-sdk/path-parameters/v0/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/path-parameters/v0/internal/query.go
+++ b/seed/go-sdk/path-parameters/v0/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/path-parameters/v0/internal/query_test.go b/seed/go-sdk/path-parameters/v0/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/path-parameters/v0/internal/query_test.go
+++ b/seed/go-sdk/path-parameters/v0/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/plain-text/internal/query.go b/seed/go-sdk/plain-text/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/plain-text/internal/query.go
+++ b/seed/go-sdk/plain-text/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/plain-text/internal/query_test.go b/seed/go-sdk/plain-text/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/plain-text/internal/query_test.go
+++ b/seed/go-sdk/plain-text/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/property-access/internal/query.go b/seed/go-sdk/property-access/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/property-access/internal/query.go
+++ b/seed/go-sdk/property-access/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/property-access/internal/query_test.go b/seed/go-sdk/property-access/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/property-access/internal/query_test.go
+++ b/seed/go-sdk/property-access/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/public-object/internal/query.go b/seed/go-sdk/public-object/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/public-object/internal/query.go
+++ b/seed/go-sdk/public-object/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/public-object/internal/query_test.go b/seed/go-sdk/public-object/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/public-object/internal/query_test.go
+++ b/seed/go-sdk/public-object/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/query-parameters-openapi-as-objects/internal/query.go b/seed/go-sdk/query-parameters-openapi-as-objects/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/query-parameters-openapi-as-objects/internal/query.go
+++ b/seed/go-sdk/query-parameters-openapi-as-objects/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/query-parameters-openapi-as-objects/internal/query_test.go b/seed/go-sdk/query-parameters-openapi-as-objects/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/query-parameters-openapi-as-objects/internal/query_test.go
+++ b/seed/go-sdk/query-parameters-openapi-as-objects/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/query-parameters-openapi/internal/query.go b/seed/go-sdk/query-parameters-openapi/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/query-parameters-openapi/internal/query.go
+++ b/seed/go-sdk/query-parameters-openapi/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/query-parameters-openapi/internal/query_test.go b/seed/go-sdk/query-parameters-openapi/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/query-parameters-openapi/internal/query_test.go
+++ b/seed/go-sdk/query-parameters-openapi/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/query-parameters/internal/query.go b/seed/go-sdk/query-parameters/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/query-parameters/internal/query.go
+++ b/seed/go-sdk/query-parameters/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/query-parameters/internal/query_test.go b/seed/go-sdk/query-parameters/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/query-parameters/internal/query_test.go
+++ b/seed/go-sdk/query-parameters/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/required-nullable/internal/query.go b/seed/go-sdk/required-nullable/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/required-nullable/internal/query.go
+++ b/seed/go-sdk/required-nullable/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/required-nullable/internal/query_test.go b/seed/go-sdk/required-nullable/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/required-nullable/internal/query_test.go
+++ b/seed/go-sdk/required-nullable/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/reserved-keywords/.fern/metadata.json b/seed/go-sdk/reserved-keywords/.fern/metadata.json
new file mode 100644
index 000000000000..13cf07b7fbfd
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/.fern/metadata.json
@@ -0,0 +1,8 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-sdk",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "enableWireTests": false
+ }
+}
\ No newline at end of file
diff --git a/seed/go-sdk/reserved-keywords/.github/workflows/ci.yml b/seed/go-sdk/reserved-keywords/.github/workflows/ci.yml
new file mode 100644
index 000000000000..56310d69624b
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on: [push]
+
+jobs:
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Compile
+ run: go build ./...
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Setup wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
+
+ - name: Test
+ run: go test ./...
+
+ - name: Teardown wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/reserved-keywords/README.md b/seed/go-sdk/reserved-keywords/README.md
new file mode 100644
index 000000000000..9bed84dad11e
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/README.md
@@ -0,0 +1,193 @@
+# Seed Go Library
+
+[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FGo)
+
+The Seed Go library provides convenient access to the Seed APIs from Go.
+
+## Table of Contents
+
+- [Reference](#reference)
+- [Usage](#usage)
+- [Environments](#environments)
+- [Errors](#errors)
+- [Request Options](#request-options)
+- [Advanced](#advanced)
+ - [Response Headers](#response-headers)
+ - [Retries](#retries)
+ - [Timeouts](#timeouts)
+ - [Explicit Null](#explicit-null)
+- [Contributing](#contributing)
+
+## Reference
+
+A full reference for this library is available [here](./reference.md).
+
+## Usage
+
+Instantiate and use the client with the following:
+
+```go
+package example
+
+import (
+ client "github.com/reserved-keywords/fern/client"
+ fern "github.com/reserved-keywords/fern"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient()
+ request := &fern.TestRequest{
+ For: "for",
+ }
+ client.Package.Test(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+## Environments
+
+You can choose between different environments by using the `option.WithBaseURL` option. You can configure any arbitrary base
+URL, which is particularly useful in test environments.
+
+```go
+client := client.NewClient(
+ option.WithBaseURL("https://example.com"),
+)
+```
+
+## Errors
+
+Structured error types are returned from API calls that return non-success status codes. These errors are compatible
+with the `errors.Is` and `errors.As` APIs, so you can access the error like so:
+
+```go
+response, err := client.Package.Test(...)
+if err != nil {
+ var apiError *core.APIError
+ if errors.As(err, apiError) {
+ // Do something with the API error ...
+ }
+ return err
+}
+```
+
+## Request Options
+
+A variety of request options are included to adapt the behavior of the library, which includes configuring
+authorization tokens, or providing your own instrumented `*http.Client`.
+
+These request options can either be
+specified on the client so that they're applied on every request, or for an individual request, like so:
+
+> Providing your own `*http.Client` is recommended. Otherwise, the `http.DefaultClient` will be used,
+> and your client will wait indefinitely for a response (unless the per-request, context-based timeout
+> is used).
+
+```go
+// Specify default options applied on every request.
+client := client.NewClient(
+ option.WithToken(""),
+ option.WithHTTPClient(
+ &http.Client{
+ Timeout: 5 * time.Second,
+ },
+ ),
+)
+
+// Specify options for an individual request.
+response, err := client.Package.Test(
+ ...,
+ option.WithToken(""),
+)
+```
+
+## Advanced
+
+### Response Headers
+
+You can access the raw HTTP response data by using the `WithRawResponse` field on the client. This is useful
+when you need to examine the response headers received from the API call. (When the endpoint is paginated,
+the raw HTTP response data will be included automatically in the Page response object.)
+
+```go
+response, err := client.Package.WithRawResponse.Test(...)
+if err != nil {
+ return err
+}
+fmt.Printf("Got response headers: %v", response.Header)
+fmt.Printf("Got status code: %d", response.StatusCode)
+```
+
+### Retries
+
+The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long
+as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
+retry limit (default: 2).
+
+A request is deemed retryable when any of the following HTTP status codes is returned:
+
+- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
+- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
+- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
+
+If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly
+over the default exponential backoff.
+
+Use the `option.WithMaxAttempts` option to configure this behavior for the entire client or an individual request:
+
+```go
+client := client.NewClient(
+ option.WithMaxAttempts(1),
+)
+
+response, err := client.Package.Test(
+ ...,
+ option.WithMaxAttempts(1),
+)
+```
+
+### Timeouts
+
+Setting a timeout for each individual request is as simple as using the standard context library. Setting a one second timeout for an individual API call looks like the following:
+
+```go
+ctx, cancel := context.WithTimeout(ctx, time.Second)
+defer cancel()
+
+response, err := client.Package.Test(ctx, ...)
+```
+
+### Explicit Null
+
+If you want to send the explicit `null` JSON value through an optional parameter, you can use the setters\
+that come with every object. Calling a setter method for a property will flip a bit in the `explicitFields`
+bitfield for that setter's object; during serialization, any property with a flipped bit will have its
+omittable status stripped, so zero or `nil` values will be sent explicitly rather than omitted altogether:
+
+```go
+type ExampleRequest struct {
+ // An optional string parameter.
+ Name *string `json:"name,omitempty" url:"-"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+request := &ExampleRequest{}
+request.SetName(nil)
+
+response, err := client.Package.Test(ctx, request, ...)
+```
+
+## Contributing
+
+While we value open-source contributions to this SDK, this library is generated programmatically.
+Additions made directly to this library would have to be moved over to our generation code,
+otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
+a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
+an issue first to discuss with us!
+
+On the other hand, contributions to the README are always very welcome!
\ No newline at end of file
diff --git a/seed/go-sdk/reserved-keywords/client/client.go b/seed/go-sdk/reserved-keywords/client/client.go
new file mode 100644
index 000000000000..a56eedc46fa1
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/client/client.go
@@ -0,0 +1,33 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ core "github.com/reserved-keywords/fern/core"
+ internal "github.com/reserved-keywords/fern/internal"
+ option "github.com/reserved-keywords/fern/option"
+ package_ "github.com/reserved-keywords/fern/package_"
+)
+
+type Client struct {
+ Package *package_.Client
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(opts ...option.RequestOption) *Client {
+ options := core.NewRequestOptions(opts...)
+ return &Client{
+ Package: package_.NewClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/client/client_test.go b/seed/go-sdk/reserved-keywords/client/client_test.go
new file mode 100644
index 000000000000..3fe8b404dd3c
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/client/client_test.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ option "github.com/reserved-keywords/fern/option"
+ assert "github.com/stretchr/testify/assert"
+ http "net/http"
+ testing "testing"
+ time "time"
+)
+
+func TestNewClient(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ c := NewClient()
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("base url", func(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("test.co"),
+ )
+ assert.Equal(t, "test.co", c.baseURL)
+ })
+
+ t.Run("http client", func(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ c := NewClient(
+ option.WithHTTPClient(httpClient),
+ )
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("http header", func(t *testing.T) {
+ header := make(http.Header)
+ header.Set("X-API-Tenancy", "test")
+ c := NewClient(
+ option.WithHTTPHeader(header),
+ )
+ assert.Empty(t, c.baseURL)
+ assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
+ })
+}
diff --git a/seed/go-sdk/reserved-keywords/core/api_error.go b/seed/go-sdk/reserved-keywords/core/api_error.go
new file mode 100644
index 000000000000..6168388541b4
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/core/api_error.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// APIError is a lightweight wrapper around the standard error
+// interface that preserves the status code from the RPC, if any.
+type APIError struct {
+ err error
+
+ StatusCode int `json:"-"`
+ Header http.Header `json:"-"`
+}
+
+// NewAPIError constructs a new API error.
+func NewAPIError(statusCode int, header http.Header, err error) *APIError {
+ return &APIError{
+ err: err,
+ Header: header,
+ StatusCode: statusCode,
+ }
+}
+
+// Unwrap returns the underlying error. This also makes the error compatible
+// with errors.As and errors.Is.
+func (a *APIError) Unwrap() error {
+ if a == nil {
+ return nil
+ }
+ return a.err
+}
+
+// Error returns the API error's message.
+func (a *APIError) Error() string {
+ if a == nil || (a.err == nil && a.StatusCode == 0) {
+ return ""
+ }
+ if a.err == nil {
+ return fmt.Sprintf("%d", a.StatusCode)
+ }
+ if a.StatusCode == 0 {
+ return a.err.Error()
+ }
+ return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
+}
diff --git a/seed/go-sdk/reserved-keywords/core/http.go b/seed/go-sdk/reserved-keywords/core/http.go
new file mode 100644
index 000000000000..92c435692940
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/core/http.go
@@ -0,0 +1,15 @@
+package core
+
+import "net/http"
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Response is an HTTP response from an HTTP client.
+type Response[T any] struct {
+ StatusCode int
+ Header http.Header
+ Body T
+}
diff --git a/seed/go-sdk/reserved-keywords/core/request_option.go b/seed/go-sdk/reserved-keywords/core/request_option.go
new file mode 100644
index 000000000000..46b8670ec9c2
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/core/request_option.go
@@ -0,0 +1,109 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of the client or an individual request.
+type RequestOption interface {
+ applyRequestOptions(*RequestOptions)
+}
+
+// RequestOptions defines all of the possible request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type RequestOptions struct {
+ BaseURL string
+ HTTPClient HTTPClient
+ HTTPHeader http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ MaxAttempts uint
+}
+
+// NewRequestOptions returns a new *RequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use RequestOption instead.
+func NewRequestOptions(opts ...RequestOption) *RequestOptions {
+ options := &RequestOptions{
+ HTTPHeader: make(http.Header),
+ BodyProperties: make(map[string]interface{}),
+ QueryParameters: make(url.Values),
+ }
+ for _, opt := range opts {
+ opt.applyRequestOptions(options)
+ }
+ return options
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request(s).
+func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() }
+
+func (r *RequestOptions) cloneHeader() http.Header {
+ headers := r.HTTPHeader.Clone()
+ headers.Set("X-Fern-Language", "Go")
+ headers.Set("X-Fern-SDK-Name", "github.com/reserved-keywords/fern")
+ headers.Set("X-Fern-SDK-Version", "v0.0.1")
+ headers.Set("User-Agent", "github.com/reserved-keywords/fern/0.0.1")
+ return headers
+}
+
+// BaseURLOption implements the RequestOption interface.
+type BaseURLOption struct {
+ BaseURL string
+}
+
+func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+// HTTPClientOption implements the RequestOption interface.
+type HTTPClientOption struct {
+ HTTPClient HTTPClient
+}
+
+func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+// HTTPHeaderOption implements the RequestOption interface.
+type HTTPHeaderOption struct {
+ HTTPHeader http.Header
+}
+
+func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+// BodyPropertiesOption implements the RequestOption interface.
+type BodyPropertiesOption struct {
+ BodyProperties map[string]interface{}
+}
+
+func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+// QueryParametersOption implements the RequestOption interface.
+type QueryParametersOption struct {
+ QueryParameters url.Values
+}
+
+func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+// MaxAttemptsOption implements the RequestOption interface.
+type MaxAttemptsOption struct {
+ MaxAttempts uint
+}
+
+func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
diff --git a/seed/go-sdk/reserved-keywords/dynamic-snippets/example0/snippet.go b/seed/go-sdk/reserved-keywords/dynamic-snippets/example0/snippet.go
new file mode 100644
index 000000000000..32e98950b231
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/dynamic-snippets/example0/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/reserved-keywords/fern/client"
+ option "github.com/reserved-keywords/fern/option"
+ fern "github.com/reserved-keywords/fern"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &fern.TestRequest{
+ For: "for",
+ }
+ client.Package.Test(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/reserved-keywords/error_codes.go b/seed/go-sdk/reserved-keywords/error_codes.go
new file mode 100644
index 000000000000..f8ff069fa84d
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/error_codes.go
@@ -0,0 +1,9 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package nurseryapi
+
+import (
+ internal "github.com/reserved-keywords/fern/internal"
+)
+
+var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{}
diff --git a/seed/go-sdk/reserved-keywords/file_param.go b/seed/go-sdk/reserved-keywords/file_param.go
new file mode 100644
index 000000000000..032be7485e01
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/file_param.go
@@ -0,0 +1,41 @@
+package nurseryapi
+
+import (
+ "io"
+)
+
+// FileParam is a file type suitable for multipart/form-data uploads.
+type FileParam struct {
+ io.Reader
+ filename string
+ contentType string
+}
+
+// FileParamOption adapts the behavior of the FileParam. No options are
+// implemented yet, but this interface allows for future extensibility.
+type FileParamOption interface {
+ apply()
+}
+
+// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file
+// upload endpoints accept a simple io.Reader, which is usually created by opening a file
+// via os.Open.
+//
+// However, some endpoints require additional metadata about the file such as a specific
+// Content-Type or custom filename. FileParam makes it easier to create the correct type
+// signature for these endpoints.
+func NewFileParam(
+ reader io.Reader,
+ filename string,
+ contentType string,
+ opts ...FileParamOption,
+) *FileParam {
+ return &FileParam{
+ Reader: reader,
+ filename: filename,
+ contentType: contentType,
+ }
+}
+
+func (f *FileParam) Name() string { return f.filename }
+func (f *FileParam) ContentType() string { return f.contentType }
diff --git a/seed/go-sdk/reserved-keywords/go.mod b/seed/go-sdk/reserved-keywords/go.mod
new file mode 100644
index 000000000000..1198523d0fa2
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/go.mod
@@ -0,0 +1,16 @@
+module github.com/reserved-keywords/fern
+
+go 1.21
+
+toolchain go1.23.8
+
+require github.com/google/uuid v1.6.0
+
+require github.com/stretchr/testify v1.8.4
+
+require gopkg.in/yaml.v3 v3.0.1 // indirect
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+)
diff --git a/seed/go-sdk/reserved-keywords/go.sum b/seed/go-sdk/reserved-keywords/go.sum
new file mode 100644
index 000000000000..fcca6d128057
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/go.sum
@@ -0,0 +1,12 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/seed/go-sdk/reserved-keywords/internal/caller.go b/seed/go-sdk/reserved-keywords/internal/caller.go
new file mode 100644
index 000000000000..de2a373090a3
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/caller.go
@@ -0,0 +1,250 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+
+ "github.com/reserved-keywords/fern/core"
+)
+
+const (
+ // contentType specifies the JSON Content-Type header value.
+ contentType = "application/json"
+ contentTypeHeader = "Content-Type"
+)
+
+// Caller calls APIs and deserializes their response, if any.
+type Caller struct {
+ client core.HTTPClient
+ retrier *Retrier
+}
+
+// CallerParams represents the parameters used to constrcut a new *Caller.
+type CallerParams struct {
+ Client core.HTTPClient
+ MaxAttempts uint
+}
+
+// NewCaller returns a new *Caller backed by the given parameters.
+func NewCaller(params *CallerParams) *Caller {
+ var httpClient core.HTTPClient = http.DefaultClient
+ if params.Client != nil {
+ httpClient = params.Client
+ }
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+ return &Caller{
+ client: httpClient,
+ retrier: NewRetrier(retryOptions...),
+ }
+}
+
+// CallParams represents the parameters used to issue an API call.
+type CallParams struct {
+ URL string
+ Method string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client core.HTTPClient
+ Request interface{}
+ Response interface{}
+ ResponseIsOptional bool
+ ErrorDecoder ErrorDecoder
+}
+
+// CallResponse is a parsed HTTP response from an API call.
+type CallResponse struct {
+ StatusCode int
+ Header http.Header
+}
+
+// Call issues an API call according to the given call parameters.
+func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := c.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := c.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Close the response body after we're done.
+ defer resp.Body.Close()
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ // Mutate the response parameter in-place.
+ if params.Response != nil {
+ if writer, ok := params.Response.(io.Writer); ok {
+ _, err = io.Copy(writer, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(params.Response)
+ }
+ if err != nil {
+ if err == io.EOF {
+ if params.ResponseIsOptional {
+ // The response is optional, so we should ignore the
+ // io.EOF error
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+ }
+ return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
+ }
+ return nil, err
+ }
+ }
+
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+}
+
+// buildURL constructs the final URL by appending the given query parameters (if any).
+func buildURL(
+ url string,
+ queryParameters url.Values,
+) string {
+ if len(queryParameters) == 0 {
+ return url
+ }
+ if strings.ContainsRune(url, '?') {
+ url += "&"
+ } else {
+ url += "?"
+ }
+ url += queryParameters.Encode()
+ return url
+}
+
+// newRequest returns a new *http.Request with all of the fields
+// required to issue the call.
+func newRequest(
+ ctx context.Context,
+ url string,
+ method string,
+ endpointHeaders http.Header,
+ request interface{},
+ bodyProperties map[string]interface{},
+) (*http.Request, error) {
+ requestBody, err := newRequestBody(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ req.Header.Set(contentTypeHeader, contentType)
+ for name, values := range endpointHeaders {
+ req.Header[name] = values
+ }
+ return req, nil
+}
+
+// newRequestBody returns a new io.Reader that represents the HTTP request body.
+func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
+ if isNil(request) {
+ if len(bodyProperties) == 0 {
+ return nil, nil
+ }
+ requestBytes, err := json.Marshal(bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+ }
+ if body, ok := request.(io.Reader); ok {
+ return body, nil
+ }
+ requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+}
+
+// decodeError decodes the error from the given HTTP response. Note that
+// it's the caller's responsibility to close the response body.
+func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
+ if errorDecoder != nil {
+ // This endpoint has custom errors, so we'll
+ // attempt to unmarshal the error into a structured
+ // type based on the status code.
+ return errorDecoder(response.StatusCode, response.Header, response.Body)
+ }
+ // This endpoint doesn't have any custom error
+ // types, so we just read the body as-is, and
+ // put it into a normal error.
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if err == io.EOF {
+ // The error didn't have a response body,
+ // so all we can do is return an error
+ // with the status code.
+ return core.NewAPIError(response.StatusCode, response.Header, nil)
+ }
+ return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
+}
+
+// isNil is used to determine if the request value is equal to nil (i.e. an interface
+// value that holds a nil concrete value is itself non-nil).
+func isNil(value interface{}) bool {
+ return value == nil || reflect.ValueOf(value).IsNil()
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/caller_test.go b/seed/go-sdk/reserved-keywords/internal/caller_test.go
new file mode 100644
index 000000000000..aec251a0c873
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/caller_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/reserved-keywords/fern/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// InternalTestCase represents a single test case.
+type InternalTestCase struct {
+ description string
+
+ // Server-side assertions.
+ givePathSuffix string
+ giveMethod string
+ giveResponseIsOptional bool
+ giveHeader http.Header
+ giveErrorDecoder ErrorDecoder
+ giveRequest *InternalTestRequest
+ giveQueryParams url.Values
+ giveBodyProperties map[string]interface{}
+
+ // Client-side assertions.
+ wantResponse *InternalTestResponse
+ wantHeaders http.Header
+ wantError error
+}
+
+// InternalTestRequest a simple request body.
+type InternalTestRequest struct {
+ Id string `json:"id"`
+}
+
+// InternalTestResponse a simple response body.
+type InternalTestResponse struct {
+ Id string `json:"id"`
+ ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
+ QueryParameters url.Values `json:"queryParameters,omitempty"`
+}
+
+// InternalTestNotFoundError represents a 404.
+type InternalTestNotFoundError struct {
+ *core.APIError
+
+ Message string `json:"message"`
+}
+
+func TestCall(t *testing.T) {
+ tests := []*InternalTestCase{
+ {
+ description: "GET success",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ },
+ },
+ {
+ description: "GET success with query",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ },
+ },
+ },
+ {
+ description: "GET not found",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusNotFound),
+ },
+ giveErrorDecoder: newTestErrorDecoder(t),
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(
+ http.StatusNotFound,
+ http.Header{},
+ errors.New(`{"message":"ID \"404\" not found"}`),
+ ),
+ },
+ },
+ {
+ description: "POST empty body",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: nil,
+ wantError: core.NewAPIError(
+ http.StatusBadRequest,
+ http.Header{},
+ errors.New("invalid request"),
+ ),
+ },
+ {
+ description: "POST optional response",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveResponseIsOptional: true,
+ },
+ {
+ description: "POST API error",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusInternalServerError),
+ },
+ wantError: core.NewAPIError(
+ http.StatusInternalServerError,
+ http.Header{},
+ errors.New("failed to process request"),
+ ),
+ },
+ {
+ description: "POST extra properties",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: new(InternalTestRequest),
+ giveBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ wantResponse: &InternalTestResponse{
+ ExtraBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ },
+ },
+ {
+ description: "GET extra query parameters",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "extra": []string{"true"},
+ },
+ },
+ },
+ {
+ description: "GET merge extra query parameters",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ "extra": []string{"true"},
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ var (
+ server = newTestServer(t, test)
+ client = server.Client()
+ )
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL + test.givePathSuffix,
+ Method: test.giveMethod,
+ Headers: test.giveHeader,
+ BodyProperties: test.giveBodyProperties,
+ QueryParameters: test.giveQueryParams,
+ Request: test.giveRequest,
+ Response: &response,
+ ResponseIsOptional: test.giveResponseIsOptional,
+ ErrorDecoder: test.giveErrorDecoder,
+ },
+ )
+ if test.wantError != nil {
+ assert.EqualError(t, err, test.wantError.Error())
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+func TestMergeHeaders(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ merged := MergeHeaders(make(http.Header), make(http.Header))
+ assert.Empty(t, merged)
+ })
+
+ t.Run("empty left", func(t *testing.T) {
+ left := make(http.Header)
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("empty right", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.1")
+
+ right := make(http.Header)
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("single value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.0")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+
+ t.Run("multiple value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Versions", "0.0.0")
+
+ right := make(http.Header)
+ right.Add("X-API-Versions", "0.0.1")
+ right.Add("X-API-Versions", "0.0.2")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
+ })
+
+ t.Run("disjoint merge", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Tenancy", "test")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+}
+
+// newTestServer returns a new *httptest.Server configured with the
+// given test parameters.
+func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tc.giveMethod, r.Method)
+ assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
+ for header, value := range tc.giveHeader {
+ assert.Equal(t, value, r.Header.Values(header))
+ }
+
+ request := new(InternalTestRequest)
+
+ bytes, err := io.ReadAll(r.Body)
+ if tc.giveRequest == nil {
+ require.Empty(t, bytes)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err = w.Write([]byte("invalid request"))
+ require.NoError(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+
+ switch request.Id {
+ case strconv.Itoa(http.StatusNotFound):
+ notFoundError := &InternalTestNotFoundError{
+ APIError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ Message: fmt.Sprintf("ID %q not found", request.Id),
+ }
+ bytes, err = json.Marshal(notFoundError)
+ require.NoError(t, err)
+
+ w.WriteHeader(http.StatusNotFound)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ return
+
+ case strconv.Itoa(http.StatusInternalServerError):
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = w.Write([]byte("failed to process request"))
+ require.NoError(t, err)
+ return
+ }
+
+ if tc.giveResponseIsOptional {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ extraBodyProperties := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
+ delete(extraBodyProperties, "id")
+
+ response := &InternalTestResponse{
+ Id: request.Id,
+ ExtraBodyProperties: extraBodyProperties,
+ QueryParameters: r.URL.Query(),
+ }
+ bytes, err = json.Marshal(response)
+ require.NoError(t, err)
+
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ },
+ ),
+ )
+}
+
+// newTestErrorDecoder returns an error decoder suitable for tests.
+func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ require.NoError(t, err)
+
+ var (
+ apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
+ decoder = json.NewDecoder(bytes.NewReader(raw))
+ )
+ if statusCode == http.StatusNotFound {
+ value := new(InternalTestNotFoundError)
+ value.APIError = apiError
+ require.NoError(t, decoder.Decode(value))
+
+ return value
+ }
+ return apiError
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/error_decoder.go b/seed/go-sdk/reserved-keywords/internal/error_decoder.go
new file mode 100644
index 000000000000..139482123397
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/error_decoder.go
@@ -0,0 +1,64 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/reserved-keywords/fern/core"
+)
+
+// ErrorCodes maps HTTP status codes to error constructors.
+type ErrorCodes map[int]func(*core.APIError) error
+
+// ErrorDecoder decodes *http.Response errors and returns a
+// typed API error (e.g. *core.APIError).
+type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
+
+// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
+// errorCodesOverrides is optional and will be merged with the default error codes,
+// with overrides taking precedence.
+func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
+ // Merge default error codes with overrides
+ mergedErrorCodes := make(ErrorCodes)
+
+ // Start with default error codes
+ for statusCode, errorFunc := range errorCodes {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+
+ // Apply overrides if provided
+ if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
+ for statusCode, errorFunc := range errorCodesOverrides[0] {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+ }
+
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ if err != nil {
+ return fmt.Errorf("failed to read error from response body: %w", err)
+ }
+ apiError := core.NewAPIError(
+ statusCode,
+ header,
+ errors.New(string(raw)),
+ )
+ newErrorFunc, ok := mergedErrorCodes[statusCode]
+ if !ok {
+ // This status code isn't recognized, so we return
+ // the API error as-is.
+ return apiError
+ }
+ customError := newErrorFunc(apiError)
+ if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
+ // If we fail to decode the error, we return the
+ // API error as-is.
+ return apiError
+ }
+ return customError
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/error_decoder_test.go b/seed/go-sdk/reserved-keywords/internal/error_decoder_test.go
new file mode 100644
index 000000000000..cd936d151b2d
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/error_decoder_test.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/reserved-keywords/fern/core"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorDecoder(t *testing.T) {
+ decoder := NewErrorDecoder(
+ ErrorCodes{
+ http.StatusNotFound: func(apiError *core.APIError) error {
+ return &InternalTestNotFoundError{APIError: apiError}
+ },
+ })
+
+ tests := []struct {
+ description string
+ giveStatusCode int
+ giveHeader http.Header
+ giveBody string
+ wantError error
+ }{
+ {
+ description: "unrecognized status code",
+ giveStatusCode: http.StatusInternalServerError,
+ giveHeader: http.Header{},
+ giveBody: "Internal Server Error",
+ wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
+ },
+ {
+ description: "not found with valid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `{"message": "Resource not found"}`,
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
+ Message: "Resource not found",
+ },
+ },
+ {
+ description: "not found with invalid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `Resource not found`,
+ wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
+ })
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/explicit_fields.go b/seed/go-sdk/reserved-keywords/internal/explicit_fields.go
new file mode 100644
index 000000000000..4bdf34fc2b7c
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/explicit_fields.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+ "math/big"
+ "reflect"
+ "strings"
+)
+
+// HandleExplicitFields processes a struct to remove `omitempty` from
+// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
+// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
+// Returns an interface{} that can be passed to json.Marshal.
+func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
+ val := reflect.ValueOf(marshaler)
+ typ := reflect.TypeOf(marshaler)
+
+ // Handle pointer types
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil
+ }
+ val = val.Elem()
+ typ = typ.Elem()
+ }
+
+ // Only handle struct types
+ if val.Kind() != reflect.Struct {
+ return marshaler
+ }
+
+ // Handle embedded struct pattern
+ var sourceVal reflect.Value
+ var sourceType reflect.Type
+
+ // Check if this is an embedded struct pattern
+ if typ.NumField() == 1 && typ.Field(0).Anonymous {
+ // This is likely an embedded struct, get the embedded value
+ embeddedField := val.Field(0)
+ sourceVal = embeddedField
+ sourceType = embeddedField.Type()
+ } else {
+ // Regular struct
+ sourceVal = val
+ sourceType = typ
+ }
+
+ // If no explicit fields set, use standard marshaling
+ if explicitFields == nil || explicitFields.Sign() == 0 {
+ return marshaler
+ }
+
+ // Create a new struct type with modified tags
+ fields := make([]reflect.StructField, 0, sourceType.NumField())
+
+ for i := 0; i < sourceType.NumField(); i++ {
+ field := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !field.IsExported() || field.Name == "explicitFields" {
+ continue
+ }
+
+ // Check if this field has been explicitly set
+ fieldBit := big.NewInt(1)
+ fieldBit.Lsh(fieldBit, uint(i))
+ if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
+ // Remove omitempty from the json tag
+ tag := field.Tag.Get("json")
+ if tag != "" && tag != "-" {
+ // Parse the json tag, remove omitempty from options
+ parts := strings.Split(tag, ",")
+ if len(parts) > 1 {
+ var newParts []string
+ newParts = append(newParts, parts[0]) // Keep the field name
+ for _, part := range parts[1:] {
+ if strings.TrimSpace(part) != "omitempty" {
+ newParts = append(newParts, part)
+ }
+ }
+ tag = strings.Join(newParts, ",")
+ }
+
+ // Reconstruct the struct tag
+ newTag := `json:"` + tag + `"`
+ if urlTag := field.Tag.Get("url"); urlTag != "" {
+ newTag += ` url:"` + urlTag + `"`
+ }
+
+ field.Tag = reflect.StructTag(newTag)
+ }
+ }
+
+ fields = append(fields, field)
+ }
+
+ // Create new struct type with modified tags
+ newType := reflect.StructOf(fields)
+ newVal := reflect.New(newType).Elem()
+
+ // Copy field values from original struct to new struct
+ fieldIndex := 0
+ for i := 0; i < sourceType.NumField(); i++ {
+ originalField := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !originalField.IsExported() || originalField.Name == "explicitFields" {
+ continue
+ }
+
+ originalValue := sourceVal.Field(i)
+ newVal.Field(fieldIndex).Set(originalValue)
+ fieldIndex++
+ }
+
+ return newVal.Interface()
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/explicit_fields_test.go b/seed/go-sdk/reserved-keywords/internal/explicit_fields_test.go
new file mode 100644
index 000000000000..3d05e88a2ce9
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/explicit_fields_test.go
@@ -0,0 +1,497 @@
+package internal
+
+import (
+ "encoding/json"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testExplicitFieldsStruct struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+ Count *int `json:"count,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ //lint:ignore unused this field is intentionally unused for testing
+ unexported string `json:"-"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ testFieldName = big.NewInt(1 << 0)
+ testFieldCode = big.NewInt(1 << 1)
+ testFieldCount = big.NewInt(1 << 2)
+ testFieldEnabled = big.NewInt(1 << 3)
+ testFieldTags = big.NewInt(1 << 4)
+)
+
+func (t *testExplicitFieldsStruct) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+func (t *testExplicitFieldsStruct) SetName(name *string) {
+ t.Name = name
+ t.require(testFieldName)
+}
+
+func (t *testExplicitFieldsStruct) SetCode(code *string) {
+ t.Code = code
+ t.require(testFieldCode)
+}
+
+func (t *testExplicitFieldsStruct) SetCount(count *int) {
+ t.Count = count
+ t.require(testFieldCount)
+}
+
+func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
+ t.Enabled = enabled
+ t.require(testFieldEnabled)
+}
+
+func (t *testExplicitFieldsStruct) SetTags(tags []string) {
+ t.Tags = tags
+ t.require(testFieldTags)
+}
+
+func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*t),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
+}
+
+type testStructWithoutExplicitFields struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+}
+
+func TestHandleExplicitFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveInput interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "nil input",
+ giveInput: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "non-struct input",
+ giveInput: "string",
+ wantBytes: []byte(`"string"`),
+ },
+ {
+ desc: "slice input",
+ giveInput: []string{"a", "b"},
+ wantBytes: []byte(`["a","b"]`),
+ },
+ {
+ desc: "map input",
+ giveInput: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "struct without explicitFields field",
+ giveInput: &testStructWithoutExplicitFields{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with no explicit fields set",
+ giveInput: &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with explicit nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null}`),
+ },
+ {
+ desc: "struct with explicit non-nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("explicit"))
+ s.SetCode(stringPtr("also-explicit"))
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
+ },
+ {
+ desc: "struct with mixed explicit and implicit fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Count: intPtr(42),
+ }
+ s.SetCode(nil) // explicit nil
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
+ },
+ {
+ desc: "struct with multiple explicit nil fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ s.SetCount(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
+ },
+ {
+ desc: "struct with slice field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Tags: []string{"tag1", "tag2"},
+ }
+ s.SetTags(nil) // explicit nil slice
+ return s
+ }(),
+ wantBytes: []byte(`{"tags":null}`),
+ },
+ {
+ desc: "struct with boolean field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetEnabled(boolPtr(false)) // explicit false
+ return s
+ }(),
+ wantBytes: []byte(`{"enabled":false}`),
+ },
+ {
+ desc: "struct with all fields explicit",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("test"))
+ s.SetCode(nil)
+ s.SetCount(intPtr(0))
+ s.SetEnabled(boolPtr(false))
+ s.SetTags([]string{})
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ var explicitFields *big.Int
+ if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
+ explicitFields = s.explicitFields
+ }
+ bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
+ t.Run("custom marshaler with explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
+ t.Run("nil pointer", func(t *testing.T) {
+ var s *testExplicitFieldsStruct
+ bytes, err := json.Marshal(HandleExplicitFields(s, nil))
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`null`), bytes)
+ })
+
+ t.Run("pointer to struct", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
+ t.Run("embedded struct with explicit fields", func(t *testing.T) {
+ // Create a struct similar to what MarshalJSON creates
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include both explicit fields (name as null, code as "test-code")
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should only include non-nil fields (omitempty behavior)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with mixed fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Count: intPtr(42), // implicit field
+ }
+ s.SetName(nil) // explicit nil
+ s.SetCode(stringPtr("explicit")) // explicit value
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include explicit null, explicit value, and implicit value
+ assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsTagHandling(t *testing.T) {
+ type testStructWithComplexTags struct {
+ Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
+ Field2 *string `json:"field2,omitempty,string" url:"field2"`
+ Field3 *string `json:"-"`
+ Field4 *string `json:"field4"`
+ explicitFields *big.Int `json:"-"`
+ }
+
+ s := &testStructWithComplexTags{
+ Field1: stringPtr("test1"),
+ Field4: stringPtr("test4"),
+ explicitFields: big.NewInt(1), // Only first field is explicit
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+
+ // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
+ assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
+}
+
+// Test types for nested struct explicit fields testing
+type testNestedStruct struct {
+ NestedName *string `json:"nested_name,omitempty"`
+ NestedCode *string `json:"nested_code,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+type testParentStruct struct {
+ ParentName *string `json:"parent_name,omitempty"`
+ Nested *testNestedStruct `json:"nested,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ nestedFieldName = big.NewInt(1 << 0)
+ nestedFieldCode = big.NewInt(1 << 1)
+)
+
+var (
+ parentFieldName = big.NewInt(1 << 0)
+ parentFieldNested = big.NewInt(1 << 1)
+)
+
+func (n *testNestedStruct) require(field *big.Int) {
+ if n.explicitFields == nil {
+ n.explicitFields = big.NewInt(0)
+ }
+ n.explicitFields.Or(n.explicitFields, field)
+}
+
+func (n *testNestedStruct) SetNestedName(name *string) {
+ n.NestedName = name
+ n.require(nestedFieldName)
+}
+
+func (n *testNestedStruct) SetNestedCode(code *string) {
+ n.NestedCode = code
+ n.require(nestedFieldCode)
+}
+
+func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
+ type embed testNestedStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*n),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
+}
+
+func (p *testParentStruct) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+func (p *testParentStruct) SetParentName(name *string) {
+ p.ParentName = name
+ p.require(parentFieldName)
+}
+
+func (p *testParentStruct) SetNested(nested *testNestedStruct) {
+ p.Nested = nested
+ p.require(parentFieldNested)
+}
+
+func (p *testParentStruct) MarshalJSON() ([]byte, error) {
+ type embed testParentStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
+}
+
+func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
+ tests := []struct {
+ desc string
+ setupFunc func() *testParentStruct
+ wantBytes []byte
+ }{
+ {
+ desc: "nested struct with explicit nil in nested object",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{
+ NestedName: stringPtr("implicit-nested"),
+ }
+ nested.SetNestedCode(nil) // explicit nil
+
+ return &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ Nested: nested,
+ }
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
+ },
+ {
+ desc: "parent with explicit nil nested struct",
+ setupFunc: func() *testParentStruct {
+ parent := &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ }
+ parent.SetNested(nil) // explicit nil nested struct
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
+ },
+ {
+ desc: "all explicit fields in nested structure",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{}
+ nested.SetNestedName(stringPtr("explicit-nested"))
+ nested.SetNestedCode(nil) // explicit nil
+
+ parent := &testParentStruct{}
+ parent.SetParentName(nil) // explicit nil
+ parent.SetNested(nested) // explicit nested struct
+
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ parent := tt.setupFunc()
+ bytes, err := parent.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/extra_properties.go b/seed/go-sdk/reserved-keywords/internal/extra_properties.go
new file mode 100644
index 000000000000..540c3fd89eeb
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]interface{}
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value interface{}) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/extra_properties_test.go b/seed/go-sdk/reserved-keywords/internal/extra_properties_test.go
new file mode 100644
index 000000000000..aa2510ee5121
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler interface{}
+ giveExtraProperties map[string]interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]interface{}{42: "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]interface{}{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/http.go b/seed/go-sdk/reserved-keywords/internal/http.go
new file mode 100644
index 000000000000..77863752bb58
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/http.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+)
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// ResolveBaseURL resolves the base URL from the given arguments,
+// preferring the first non-empty value.
+func ResolveBaseURL(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// EncodeURL encodes the given arguments into the URL, escaping
+// values as needed. Pointer arguments are dereferenced before processing.
+func EncodeURL(urlFormat string, args ...interface{}) string {
+ escapedArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ // Dereference the argument if it's a pointer
+ value := dereferenceArg(arg)
+ escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
+ }
+ return fmt.Sprintf(urlFormat, escapedArgs...)
+}
+
+// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
+// If the argument is not a pointer or is nil, it returns the argument as-is.
+func dereferenceArg(arg interface{}) interface{} {
+ if arg == nil {
+ return arg
+ }
+
+ v := reflect.ValueOf(arg)
+
+ // Keep dereferencing until we get to a non-pointer value or hit nil
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return nil
+ }
+ v = v.Elem()
+ }
+
+ return v.Interface()
+}
+
+// MergeHeaders merges the given headers together, where the right
+// takes precedence over the left.
+func MergeHeaders(left, right http.Header) http.Header {
+ for key, values := range right {
+ if len(values) > 1 {
+ left[key] = values
+ continue
+ }
+ if value := right.Get(key); value != "" {
+ left.Set(key, value)
+ }
+ }
+ return left
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/query.go b/seed/go-sdk/reserved-keywords/internal/query.go
new file mode 100644
index 000000000000..1cbaf7fe1c02
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/query.go
@@ -0,0 +1,353 @@
+package internal
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+var (
+ bytesType = reflect.TypeOf([]byte{})
+ queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ uuidType = reflect.TypeOf(uuid.UUID{})
+)
+
+// QueryEncoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type QueryEncoder interface {
+ EncodeQueryValues(key string, v *url.Values) error
+}
+
+// prepareValue handles common validation and unwrapping logic for both functions
+func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return reflect.Value{}, values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return reflect.Value{}, values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ if err != nil {
+ return reflect.Value{}, nil, err
+ }
+
+ return val, values, nil
+}
+
+// QueryValues encodes url.Values from request objects.
+//
+// Note: This type is inspired by Google's query encoding library, but
+// supports far less customization and is tailored to fit this SDK's use case.
+//
+// Ref: https://github.com/google/go-querystring
+func QueryValues(v interface{}) (url.Values, error) {
+ _, values, err := prepareValue(v)
+ return values, err
+}
+
+// QueryValuesWithDefaults encodes url.Values from request objects
+// and default values, merging the defaults into the request.
+// It's expected that the values of defaults are wire names.
+func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
+ val, values, err := prepareValue(v)
+ if err != nil {
+ return values, err
+ }
+ if !val.IsValid() {
+ return values, nil
+ }
+
+ // apply defaults to zero-value fields directly on the original struct
+ valType := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Field(i)
+ fieldType := valType.Field(i)
+ fieldName := fieldType.Name
+
+ if fieldType.PkgPath != "" && !fieldType.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ // check if field is zero value and we have a default for it
+ if field.CanSet() && field.IsZero() {
+ tag := fieldType.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+ wireName, _ := parseTag(tag)
+ if wireName == "" {
+ wireName = fieldName
+ }
+ if defaultVal, exists := defaults[wireName]; exists {
+ values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
+ }
+ }
+ }
+
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+
+ name, opts := parseTag(tag)
+ if name == "" {
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(queryEncoderType) {
+ // If sv is a nil pointer and the custom encoder is defined on a non-pointer
+ // method receiver, set sv to the zero value of the underlying type
+ if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(QueryEncoder)
+ if err := m.EncodeQueryValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ // Recursively dereference pointers, but stop at nil pointers.
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts, sf))
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ if sv.Len() == 0 {
+ // Skip if slice or array is empty.
+ continue
+ }
+ for i := 0; i < sv.Len(); i++ {
+ value := sv.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), name); err != nil {
+ return err
+ }
+ } else {
+ values.Add(name, valueString(value, opts, sf))
+ }
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Map {
+ if err := reflectMap(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ if err := reflectValue(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts, sf))
+ }
+
+ return nil
+}
+
+// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
+func reflectMap(values url.Values, val reflect.Value, scope string) error {
+ if val.IsNil() {
+ return nil
+ }
+
+ iter := val.MapRange()
+ for iter.Next() {
+ k := iter.Key()
+ v := iter.Value()
+
+ key := fmt.Sprint(k.Interface())
+ paramName := scope + "[" + key + "]"
+
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ break
+ }
+ v = v.Elem()
+ }
+
+ for v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Map {
+ if err := reflectMap(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Struct {
+ if err := reflectValue(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ if v.Len() == 0 {
+ continue
+ }
+ for i := 0; i < v.Len(); i++ {
+ value := v.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), paramName); err != nil {
+ return err
+ }
+ } else {
+ values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
+ }
+ }
+ continue
+ }
+
+ values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if format := sf.Tag.Get("format"); format == "date" {
+ return t.Format("2006-01-02")
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ if v.Type() == uuidType {
+ u := v.Interface().(uuid.UUID)
+ return u.String()
+ }
+
+ if v.Type() == bytesType {
+ b := v.Interface().([]byte)
+ return base64.StdEncoding.EncodeToString(b)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ type zeroable interface {
+ IsZero() bool
+ }
+
+ if !v.IsZero() {
+ if z, ok := v.Interface().(zeroable); ok {
+ return z.IsZero()
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
+ return false
+ }
+
+ return false
+}
+
+// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
+func isStructPointer(v reflect.Value) bool {
+ return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/query_test.go b/seed/go-sdk/reserved-keywords/internal/query_test.go
new file mode 100644
index 000000000000..2c28cb8acf68
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/query_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryValues(t *testing.T) {
+ t.Run("empty optional", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+
+ t.Run("empty required", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Equal(t, "required=", values.Encode())
+ })
+
+ t.Run("allow multiple", func(t *testing.T) {
+ type example struct {
+ Values []string `json:"values" url:"values"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Values: []string{"foo", "bar", "baz"},
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
+ })
+
+ t.Run("nested object", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ nestedValue := "nestedValue"
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ Nested: &nested{
+ Value: &nestedValue,
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
+ })
+
+ t.Run("url unspecified", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("url ignored", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound" url:"-"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("datetime", func(t *testing.T) {
+ type example struct {
+ DateTime time.Time `json:"dateTime" url:"dateTime"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
+ })
+
+ t.Run("date", func(t *testing.T) {
+ type example struct {
+ Date time.Time `json:"date" url:"date" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "date=1994-03-16", values.Encode())
+ })
+
+ t.Run("optional time", func(t *testing.T) {
+ type example struct {
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
+ type enum string
+
+ type example struct {
+ Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("object array", func(t *testing.T) {
+ type object struct {
+ Key string `json:"key" url:"key"`
+ Value string `json:"value" url:"value"`
+ }
+ type example struct {
+ Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Objects: []*object{
+ {
+ Key: "hello",
+ Value: "world",
+ },
+ {
+ Key: "foo",
+ Value: "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
+ })
+
+ t.Run("map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "foo": "bar",
+ "baz": "qux",
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map array", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": []string{
+ "one",
+ "two",
+ "three",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
+ })
+}
+
+func TestQueryValuesWithDefaults(t *testing.T) {
+ t.Run("apply defaults to zero values", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ })
+
+ t.Run("preserve non-zero values over defaults", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Name: "actual-name",
+ Age: 30,
+ // Enabled remains false (zero value), should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
+ })
+
+ t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "nonexistent": "should-be-ignored",
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&name=default-name", values.Encode())
+ })
+
+ t.Run("type conversion for compatible defaults", func(t *testing.T) {
+ type example struct {
+ Count int64 `json:"count" url:"count"`
+ Rate float64 `json:"rate" url:"rate"`
+ Message string `json:"message" url:"message"`
+ }
+
+ defaults := map[string]interface{}{
+ "count": int(100), // int -> int64 conversion
+ "rate": float32(2.5), // float32 -> float64 conversion
+ "message": "hello", // string -> string (no conversion needed)
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
+ })
+
+ t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
+ Count int `json:"count,omitempty" url:"count,omitempty"`
+ }
+
+ defaultOptional := "default-optional"
+ defaults := map[string]interface{}{
+ "required": "default-required",
+ "optional": &defaultOptional, // pointer type
+ "count": 42,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Required: "custom-required", // should override default
+ // Optional is nil, should get default
+ // Count is 0, should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
+ })
+
+ t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
+ type example struct {
+ Name *string `json:"name" url:"name"`
+ Age *int `json:"age" url:"age"`
+ Enabled *bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ // first, test that a properly empty request is overridden:
+ {
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ }
+
+ // second, test that a request that contains zeros is not overridden:
+ var (
+ name = ""
+ age = 0
+ enabled = false
+ )
+ values, err := QueryValuesWithDefaults(&example{
+ Name: &name, // explicit empty string should override default
+ Age: &age, // explicit zero should override default
+ Enabled: &enabled, // explicit false should override default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
+ })
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/retrier.go b/seed/go-sdk/reserved-keywords/internal/retrier.go
new file mode 100644
index 000000000000..4efae1b4c286
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/retrier.go
@@ -0,0 +1,230 @@
+package internal
+
+import (
+ "crypto/rand"
+ "math/big"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+const (
+ defaultRetryAttempts = 2
+ minRetryDelay = 1000 * time.Millisecond
+ maxRetryDelay = 60000 * time.Millisecond
+)
+
+// RetryOption adapts the behavior the *Retrier.
+type RetryOption func(*retryOptions)
+
+// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
+type RetryFunc func(*http.Request) (*http.Response, error)
+
+// WithMaxAttempts configures the maximum number of attempts
+// of the *Retrier.
+func WithMaxAttempts(attempts uint) RetryOption {
+ return func(opts *retryOptions) {
+ opts.attempts = attempts
+ }
+}
+
+// Retrier retries failed requests a configurable number of times with an
+// exponential back-off between each retry.
+type Retrier struct {
+ attempts uint
+}
+
+// NewRetrier constructs a new *Retrier with the given options, if any.
+func NewRetrier(opts ...RetryOption) *Retrier {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ attempts := uint(defaultRetryAttempts)
+ if options.attempts > 0 {
+ attempts = options.attempts
+ }
+ return &Retrier{
+ attempts: attempts,
+ }
+}
+
+// Run issues the request and, upon failure, retries the request if possible.
+//
+// The request will be retried as long as the request is deemed retryable and the
+// number of retry attempts has not grown larger than the configured retry limit.
+func (r *Retrier) Run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ opts ...RetryOption,
+) (*http.Response, error) {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ maxRetryAttempts := r.attempts
+ if options.attempts > 0 {
+ maxRetryAttempts = options.attempts
+ }
+ var (
+ retryAttempt uint
+ previousError error
+ )
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt,
+ previousError,
+ )
+}
+
+func (r *Retrier) run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ maxRetryAttempts uint,
+ retryAttempt uint,
+ previousError error,
+) (*http.Response, error) {
+ if retryAttempt >= maxRetryAttempts {
+ return nil, previousError
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := request.Context().Err(); err != nil {
+ return nil, err
+ }
+
+ response, err := fn(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.shouldRetry(response) {
+ defer response.Body.Close()
+
+ delay, err := r.retryDelay(response, retryAttempt)
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(delay)
+
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt+1,
+ decodeError(response, errorDecoder),
+ )
+ }
+
+ return response, nil
+}
+
+// shouldRetry returns true if the request should be retried based on the given
+// response status code.
+func (r *Retrier) shouldRetry(response *http.Response) bool {
+ return response.StatusCode == http.StatusTooManyRequests ||
+ response.StatusCode == http.StatusRequestTimeout ||
+ response.StatusCode >= http.StatusInternalServerError
+}
+
+// retryDelay calculates the delay time based on response headers,
+// falling back to exponential backoff if no headers are present.
+func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
+ // Check for Retry-After header first (RFC 7231), applying no jitter
+ if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
+ // Parse as number of seconds...
+ if seconds, err := strconv.Atoi(retryAfter); err == nil {
+ delay := time.Duration(seconds) * time.Second
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+
+ // ...or as an HTTP date; both are valid
+ if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
+ delay := time.Until(retryTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+ }
+
+ // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
+ if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
+ if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
+ // Assume Unix timestamp in seconds
+ resetTime := time.Unix(resetTimestamp, 0)
+ delay := time.Until(resetTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return r.addPositiveJitter(delay)
+ }
+ }
+ }
+
+ // Fall back to exponential backoff
+ return r.exponentialBackoff(retryAttempt)
+}
+
+// exponentialBackoff calculates the delay time based on the retry attempt
+// and applies symmetric jitter (±10% around the delay).
+func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
+ if retryAttempt > 63 { // 2^63+ would overflow uint64
+ retryAttempt = 63
+ }
+
+ delay := minRetryDelay << retryAttempt
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+
+ return r.addSymmetricJitter(delay)
+}
+
+// addJitterWithRange applies jitter to the given delay.
+// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
+func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
+ jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
+ jitter, err := rand.Int(rand.Reader, jitterRange)
+ if err != nil {
+ return 0, err
+ }
+
+ jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
+ if jitteredDelay < minRetryDelay {
+ jitteredDelay = minRetryDelay
+ }
+ if jitteredDelay > maxRetryDelay {
+ jitteredDelay = maxRetryDelay
+ }
+ return jitteredDelay, nil
+}
+
+// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
+func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 100, 120)
+}
+
+// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
+func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 90, 110)
+}
+
+type retryOptions struct {
+ attempts uint
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/retrier_test.go b/seed/go-sdk/reserved-keywords/internal/retrier_test.go
new file mode 100644
index 000000000000..66f91ef996f8
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/retrier_test.go
@@ -0,0 +1,300 @@
+package internal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/reserved-keywords/fern/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type RetryTestCase struct {
+ description string
+
+ giveAttempts uint
+ giveStatusCodes []int
+ giveResponse *InternalTestResponse
+
+ wantResponse *InternalTestResponse
+ wantError *core.APIError
+}
+
+func TestRetrier(t *testing.T) {
+ tests := []*RetryTestCase{
+ {
+ description: "retry request succeeds after multiple failures",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ giveResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ },
+ {
+ description: "retry request fails if MaxAttempts is exceeded",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusOK,
+ },
+ wantError: &core.APIError{
+ StatusCode: http.StatusRequestTimeout,
+ },
+ },
+ {
+ description: "retry durations increase exponentially and stay within the min and max delay values",
+ giveAttempts: 4,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ },
+ {
+ description: "retry does not occur on status code 404",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
+ wantError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ },
+ {
+ description: "retries occur on status code 429",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 408",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 500",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.description, func(t *testing.T) {
+ var (
+ test = tc
+ server = newTestRetryServer(t, test)
+ client = server.Client()
+ )
+
+ t.Parallel()
+
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: test.giveAttempts,
+ ResponseIsOptional: true,
+ },
+ )
+
+ if test.wantError != nil {
+ require.IsType(t, err, &core.APIError{})
+ expectedErrorCode := test.wantError.StatusCode
+ actualErrorCode := err.(*core.APIError).StatusCode
+ assert.Equal(t, expectedErrorCode, actualErrorCode)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+// newTestRetryServer returns a new *httptest.Server configured with the
+// given test parameters, suitable for testing retries.
+func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
+ var index int
+ timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
+
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if index > 0 && index < len(expectedRetryDurations) {
+ // Ensure that the duration between retries increases exponentially,
+ // and that it is within the minimum and maximum retry delay values.
+ actualDuration := timestamps[index].Sub(timestamps[index-1])
+ expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
+ expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
+ assert.True(
+ t,
+ actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
+ "expected duration to be in range [%v, %v], got %v",
+ expectedDurationMin,
+ expectedDurationMax,
+ actualDuration,
+ )
+ assert.LessOrEqual(
+ t,
+ actualDuration,
+ maxRetryDelay,
+ "expected duration to be less than the maxRetryDelay (%v), got %v",
+ maxRetryDelay,
+ actualDuration,
+ )
+ assert.GreaterOrEqual(
+ t,
+ actualDuration,
+ minRetryDelay,
+ "expected duration to be greater than the minRetryDelay (%v), got %v",
+ minRetryDelay,
+ actualDuration,
+ )
+ }
+
+ request := new(InternalTestRequest)
+ bytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+ require.LessOrEqual(t, index, len(tc.giveStatusCodes))
+
+ statusCode := tc.giveStatusCodes[index]
+
+ w.WriteHeader(statusCode)
+
+ if tc.giveResponse != nil && statusCode == http.StatusOK {
+ bytes, err = json.Marshal(tc.giveResponse)
+ require.NoError(t, err)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ }
+
+ index++
+ },
+ ),
+ )
+}
+
+// expectedRetryDurations holds an array of calculated retry durations,
+// where the index of the array should correspond to the retry attempt.
+//
+// Values are calculated based off of `minRetryDelay * 2^i`.
+var expectedRetryDurations = []time.Duration{
+ 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
+ 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
+ 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
+ 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
+}
+
+func TestRetryDelayTiming(t *testing.T) {
+ tests := []struct {
+ name string
+ headerName string
+ headerValueFunc func() string
+ expectedMinMs int64
+ expectedMaxMs int64
+ }{
+ {
+ name: "retry-after with seconds value",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return "1"
+ },
+ expectedMinMs: 500,
+ expectedMaxMs: 1500,
+ },
+ {
+ name: "retry-after with HTTP date",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return time.Now().Add(3 * time.Second).Format(time.RFC1123)
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ {
+ name: "x-ratelimit-reset with future timestamp",
+ headerName: "x-ratelimit-reset",
+ headerValueFunc: func() string {
+ return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ var timestamps []time.Time
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if len(timestamps) == 1 {
+ // First request - return retryable error with header
+ w.Header().Set(tt.headerName, tt.headerValueFunc())
+ w.WriteHeader(http.StatusTooManyRequests)
+ } else {
+ // Second request - return success
+ w.WriteHeader(http.StatusOK)
+ response := &InternalTestResponse{Id: "success"}
+ bytes, _ := json.Marshal(response)
+ w.Write(bytes)
+ }
+ }))
+ defer server.Close()
+
+ caller := NewCaller(&CallerParams{
+ Client: server.Client(),
+ })
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: 2,
+ ResponseIsOptional: true,
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, timestamps, 2, "Expected exactly 2 requests")
+
+ actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
+
+ assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
+ "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
+ assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
+ "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
+ })
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/stringer.go b/seed/go-sdk/reserved-keywords/internal/stringer.go
new file mode 100644
index 000000000000..312801851e0e
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value interface{}) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-sdk/reserved-keywords/internal/time.go b/seed/go-sdk/reserved-keywords/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-sdk/reserved-keywords/option/request_option.go b/seed/go-sdk/reserved-keywords/option/request_option.go
new file mode 100644
index 000000000000..9de03c7dbde8
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/option/request_option.go
@@ -0,0 +1,64 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/reserved-keywords/fern/core"
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of an individual request.
+type RequestOption = core.RequestOption
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return &core.BaseURLOption{
+ BaseURL: baseURL,
+ }
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return &core.HTTPClientOption{
+ HTTPClient: httpClient,
+ }
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return &core.HTTPHeaderOption{
+ // Clone the headers so they can't be modified after the option call.
+ HTTPHeader: httpHeader.Clone(),
+ }
+}
+
+// WithBodyProperties adds the given body properties to the request.
+func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
+ copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
+ for key, value := range bodyProperties {
+ copiedBodyProperties[key] = value
+ }
+ return &core.BodyPropertiesOption{
+ BodyProperties: copiedBodyProperties,
+ }
+}
+
+// WithQueryParameters adds the given query parameters to the request.
+func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
+ copiedQueryParameters := make(url.Values, len(queryParameters))
+ for key, values := range queryParameters {
+ copiedQueryParameters[key] = values
+ }
+ return &core.QueryParametersOption{
+ QueryParameters: copiedQueryParameters,
+ }
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return &core.MaxAttemptsOption{
+ MaxAttempts: attempts,
+ }
+}
diff --git a/seed/go-sdk/reserved-keywords/package.go b/seed/go-sdk/reserved-keywords/package.go
new file mode 100644
index 000000000000..bb39c1453582
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/package.go
@@ -0,0 +1,207 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package nurseryapi
+
+import (
+ json "encoding/json"
+ fmt "fmt"
+ internal "github.com/reserved-keywords/fern/internal"
+ big "math/big"
+)
+
+var (
+ testRequestFieldFor = big.NewInt(1 << 0)
+)
+
+type TestRequest struct {
+ For string `json:"-" url:"for"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+func (t *TestRequest) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+// SetFor sets the For field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (t *TestRequest) SetFor(for_ string) {
+ t.For = for_
+ t.require(testRequestFieldFor)
+}
+
+var (
+ packageFieldName = big.NewInt(1 << 0)
+)
+
+type Package struct {
+ Name string `json:"name" url:"name"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+
+ extraProperties map[string]interface{}
+ rawJSON json.RawMessage
+}
+
+func (p *Package) GetName() string {
+ if p == nil {
+ return ""
+ }
+ return p.Name
+}
+
+func (p *Package) GetExtraProperties() map[string]interface{} {
+ return p.extraProperties
+}
+
+func (p *Package) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+// SetName sets the Name field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (p *Package) SetName(name string) {
+ p.Name = name
+ p.require(packageFieldName)
+}
+
+func (p *Package) UnmarshalJSON(data []byte) error {
+ type unmarshaler Package
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *p = Package(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *p)
+ if err != nil {
+ return err
+ }
+ p.extraProperties = extraProperties
+ p.rawJSON = json.RawMessage(data)
+ return nil
+}
+
+func (p *Package) MarshalJSON() ([]byte, error) {
+ type embed Package
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ explicitMarshaler := internal.HandleExplicitFields(marshaler, p.explicitFields)
+ return json.Marshal(explicitMarshaler)
+}
+
+func (p *Package) String() string {
+ if len(p.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(p.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(p); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", p)
+}
+
+var (
+ recordFieldFoo = big.NewInt(1 << 0)
+ recordFieldField3D = big.NewInt(1 << 1)
+)
+
+type Record struct {
+ Foo map[string]string `json:"foo" url:"foo"`
+ Field3D int `json:"3d" url:"3d"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+
+ extraProperties map[string]interface{}
+ rawJSON json.RawMessage
+}
+
+func (r *Record) GetFoo() map[string]string {
+ if r == nil {
+ return nil
+ }
+ return r.Foo
+}
+
+func (r *Record) GetField3D() int {
+ if r == nil {
+ return 0
+ }
+ return r.Field3D
+}
+
+func (r *Record) GetExtraProperties() map[string]interface{} {
+ return r.extraProperties
+}
+
+func (r *Record) require(field *big.Int) {
+ if r.explicitFields == nil {
+ r.explicitFields = big.NewInt(0)
+ }
+ r.explicitFields.Or(r.explicitFields, field)
+}
+
+// SetFoo sets the Foo field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (r *Record) SetFoo(foo map[string]string) {
+ r.Foo = foo
+ r.require(recordFieldFoo)
+}
+
+// SetField3D sets the Field3D field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (r *Record) SetField3D(_3D int) {
+ r.Field3D = _3D
+ r.require(recordFieldField3D)
+}
+
+func (r *Record) UnmarshalJSON(data []byte) error {
+ type unmarshaler Record
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *r = Record(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *r)
+ if err != nil {
+ return err
+ }
+ r.extraProperties = extraProperties
+ r.rawJSON = json.RawMessage(data)
+ return nil
+}
+
+func (r *Record) MarshalJSON() ([]byte, error) {
+ type embed Record
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*r),
+ }
+ explicitMarshaler := internal.HandleExplicitFields(marshaler, r.explicitFields)
+ return json.Marshal(explicitMarshaler)
+}
+
+func (r *Record) String() string {
+ if len(r.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(r.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(r); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", r)
+}
diff --git a/seed/go-sdk/reserved-keywords/package_/client.go b/seed/go-sdk/reserved-keywords/package_/client.go
new file mode 100644
index 000000000000..56ef70dabff7
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/package_/client.go
@@ -0,0 +1,49 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package package_
+
+import (
+ context "context"
+ fern "github.com/reserved-keywords/fern"
+ core "github.com/reserved-keywords/fern/core"
+ internal "github.com/reserved-keywords/fern/internal"
+ option "github.com/reserved-keywords/fern/option"
+)
+
+type Client struct {
+ WithRawResponse *RawClient
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(options *core.RequestOptions) *Client {
+ return &Client{
+ WithRawResponse: NewRawClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (c *Client) Test(
+ ctx context.Context,
+ request *fern.TestRequest,
+ opts ...option.RequestOption,
+) error {
+ _, err := c.WithRawResponse.Test(
+ ctx,
+ request,
+ opts...,
+ )
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/seed/go-sdk/reserved-keywords/package_/raw_client.go b/seed/go-sdk/reserved-keywords/package_/raw_client.go
new file mode 100644
index 000000000000..0508a4058d78
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/package_/raw_client.go
@@ -0,0 +1,76 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package package_
+
+import (
+ context "context"
+ fern "github.com/reserved-keywords/fern"
+ core "github.com/reserved-keywords/fern/core"
+ internal "github.com/reserved-keywords/fern/internal"
+ option "github.com/reserved-keywords/fern/option"
+ http "net/http"
+)
+
+type RawClient struct {
+ baseURL string
+ caller *internal.Caller
+ options *core.RequestOptions
+}
+
+func NewRawClient(options *core.RequestOptions) *RawClient {
+ return &RawClient{
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (r *RawClient) Test(
+ ctx context.Context,
+ request *fern.TestRequest,
+ opts ...option.RequestOption,
+) (*core.Response[any], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ r.baseURL,
+ "",
+ )
+ endpointURL := baseURL
+ queryParams, err := internal.QueryValues(request)
+ if err != nil {
+ return nil, err
+ }
+ if len(queryParams) > 0 {
+ endpointURL += "?" + queryParams.Encode()
+ }
+ headers := internal.MergeHeaders(
+ r.options.ToHeader(),
+ options.ToHeader(),
+ )
+ raw, err := r.caller.Call(
+ ctx,
+ &internal.CallParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &core.Response[any]{
+ StatusCode: raw.StatusCode,
+ Header: raw.Header,
+ Body: nil,
+ }, nil
+}
diff --git a/seed/go-sdk/reserved-keywords/pointer.go b/seed/go-sdk/reserved-keywords/pointer.go
new file mode 100644
index 000000000000..aab57aa23ffe
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/pointer.go
@@ -0,0 +1,132 @@
+package nurseryapi
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// Bool returns a pointer to the given bool value.
+func Bool(b bool) *bool {
+ return &b
+}
+
+// Byte returns a pointer to the given byte value.
+func Byte(b byte) *byte {
+ return &b
+}
+
+// Complex64 returns a pointer to the given complex64 value.
+func Complex64(c complex64) *complex64 {
+ return &c
+}
+
+// Complex128 returns a pointer to the given complex128 value.
+func Complex128(c complex128) *complex128 {
+ return &c
+}
+
+// Float32 returns a pointer to the given float32 value.
+func Float32(f float32) *float32 {
+ return &f
+}
+
+// Float64 returns a pointer to the given float64 value.
+func Float64(f float64) *float64 {
+ return &f
+}
+
+// Int returns a pointer to the given int value.
+func Int(i int) *int {
+ return &i
+}
+
+// Int8 returns a pointer to the given int8 value.
+func Int8(i int8) *int8 {
+ return &i
+}
+
+// Int16 returns a pointer to the given int16 value.
+func Int16(i int16) *int16 {
+ return &i
+}
+
+// Int32 returns a pointer to the given int32 value.
+func Int32(i int32) *int32 {
+ return &i
+}
+
+// Int64 returns a pointer to the given int64 value.
+func Int64(i int64) *int64 {
+ return &i
+}
+
+// Rune returns a pointer to the given rune value.
+func Rune(r rune) *rune {
+ return &r
+}
+
+// String returns a pointer to the given string value.
+func String(s string) *string {
+ return &s
+}
+
+// Uint returns a pointer to the given uint value.
+func Uint(u uint) *uint {
+ return &u
+}
+
+// Uint8 returns a pointer to the given uint8 value.
+func Uint8(u uint8) *uint8 {
+ return &u
+}
+
+// Uint16 returns a pointer to the given uint16 value.
+func Uint16(u uint16) *uint16 {
+ return &u
+}
+
+// Uint32 returns a pointer to the given uint32 value.
+func Uint32(u uint32) *uint32 {
+ return &u
+}
+
+// Uint64 returns a pointer to the given uint64 value.
+func Uint64(u uint64) *uint64 {
+ return &u
+}
+
+// Uintptr returns a pointer to the given uintptr value.
+func Uintptr(u uintptr) *uintptr {
+ return &u
+}
+
+// UUID returns a pointer to the given uuid.UUID value.
+func UUID(u uuid.UUID) *uuid.UUID {
+ return &u
+}
+
+// Time returns a pointer to the given time.Time value.
+func Time(t time.Time) *time.Time {
+ return &t
+}
+
+// MustParseDate attempts to parse the given string as a
+// date time.Time, and panics upon failure.
+func MustParseDate(date string) time.Time {
+ t, err := time.Parse("2006-01-02", date)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
+
+// MustParseDateTime attempts to parse the given string as a
+// datetime time.Time, and panics upon failure.
+func MustParseDateTime(datetime string) time.Time {
+ t, err := time.Parse(time.RFC3339, datetime)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
diff --git a/seed/go-sdk/reserved-keywords/reference.md b/seed/go-sdk/reserved-keywords/reference.md
new file mode 100644
index 000000000000..47695584154c
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/reference.md
@@ -0,0 +1,48 @@
+# Reference
+## Package
+client.Package.Test() -> error
+
+-
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```go
+request := &fern.TestRequest{
+ For: "for",
+ }
+client.Package.Test(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**for_:** `string`
+
+
+
+
+
+
+
+
+
+
diff --git a/seed/go-sdk/reserved-keywords/snippet.json b/seed/go-sdk/reserved-keywords/snippet.json
new file mode 100644
index 000000000000..0bbbb0c1f076
--- /dev/null
+++ b/seed/go-sdk/reserved-keywords/snippet.json
@@ -0,0 +1,15 @@
+{
+ "endpoints": [
+ {
+ "id": {
+ "path": "/",
+ "method": "POST",
+ "identifier_override": "endpoint_package.test"
+ },
+ "snippet": {
+ "type": "go",
+ "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/reserved-keywords/fern\"\n\tfernclient \"github.com/reserved-keywords/fern/client\"\n)\n\nclient := fernclient.NewClient()\nerr := client.Package.Test(\n\tcontext.TODO(),\n\t\u0026fern.TestRequest{\n\t\tFor: \"for\",\n\t},\n)\n"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/seed/go-sdk/response-property/internal/query.go b/seed/go-sdk/response-property/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/response-property/internal/query.go
+++ b/seed/go-sdk/response-property/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/response-property/internal/query_test.go b/seed/go-sdk/response-property/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/response-property/internal/query_test.go
+++ b/seed/go-sdk/response-property/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/.fern/metadata.json b/seed/go-sdk/server-sent-event-examples/with-wire-tests/.fern/metadata.json
new file mode 100644
index 000000000000..93eede3d8336
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/.fern/metadata.json
@@ -0,0 +1,12 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-sdk",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "enableWireTests": true,
+ "packageName": "sse",
+ "module": {
+ "path": "github.com/fern-api/sse-examples-go"
+ }
+ }
+}
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/.github/workflows/ci.yml b/seed/go-sdk/server-sent-event-examples/with-wire-tests/.github/workflows/ci.yml
new file mode 100644
index 000000000000..56310d69624b
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on: [push]
+
+jobs:
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Compile
+ run: go build ./...
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Setup wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
+
+ - name: Test
+ run: go test ./...
+
+ - name: Teardown wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/README.md b/seed/go-sdk/server-sent-event-examples/with-wire-tests/README.md
new file mode 100644
index 000000000000..abc08e84b594
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/README.md
@@ -0,0 +1,193 @@
+# Seed Go Library
+
+[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FGo)
+
+The Seed Go library provides convenient access to the Seed APIs from Go.
+
+## Table of Contents
+
+- [Reference](#reference)
+- [Usage](#usage)
+- [Environments](#environments)
+- [Errors](#errors)
+- [Request Options](#request-options)
+- [Advanced](#advanced)
+ - [Response Headers](#response-headers)
+ - [Retries](#retries)
+ - [Timeouts](#timeouts)
+ - [Explicit Null](#explicit-null)
+- [Contributing](#contributing)
+
+## Reference
+
+A full reference for this library is available [here](./reference.md).
+
+## Usage
+
+Instantiate and use the client with the following:
+
+```go
+package example
+
+import (
+ client "github.com/fern-api/sse-examples-go/client"
+ sse "github.com/fern-api/sse-examples-go"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient()
+ request := &sse.StreamCompletionRequest{
+ Query: "foo",
+ }
+ client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+## Environments
+
+You can choose between different environments by using the `option.WithBaseURL` option. You can configure any arbitrary base
+URL, which is particularly useful in test environments.
+
+```go
+client := client.NewClient(
+ option.WithBaseURL("https://example.com"),
+)
+```
+
+## Errors
+
+Structured error types are returned from API calls that return non-success status codes. These errors are compatible
+with the `errors.Is` and `errors.As` APIs, so you can access the error like so:
+
+```go
+response, err := client.Completions.Stream(...)
+if err != nil {
+ var apiError *core.APIError
+ if errors.As(err, apiError) {
+ // Do something with the API error ...
+ }
+ return err
+}
+```
+
+## Request Options
+
+A variety of request options are included to adapt the behavior of the library, which includes configuring
+authorization tokens, or providing your own instrumented `*http.Client`.
+
+These request options can either be
+specified on the client so that they're applied on every request, or for an individual request, like so:
+
+> Providing your own `*http.Client` is recommended. Otherwise, the `http.DefaultClient` will be used,
+> and your client will wait indefinitely for a response (unless the per-request, context-based timeout
+> is used).
+
+```go
+// Specify default options applied on every request.
+client := client.NewClient(
+ option.WithToken(""),
+ option.WithHTTPClient(
+ &http.Client{
+ Timeout: 5 * time.Second,
+ },
+ ),
+)
+
+// Specify options for an individual request.
+response, err := client.Completions.Stream(
+ ...,
+ option.WithToken(""),
+)
+```
+
+## Advanced
+
+### Response Headers
+
+You can access the raw HTTP response data by using the `WithRawResponse` field on the client. This is useful
+when you need to examine the response headers received from the API call. (When the endpoint is paginated,
+the raw HTTP response data will be included automatically in the Page response object.)
+
+```go
+response, err := client.Completions.WithRawResponse.Stream(...)
+if err != nil {
+ return err
+}
+fmt.Printf("Got response headers: %v", response.Header)
+fmt.Printf("Got status code: %d", response.StatusCode)
+```
+
+### Retries
+
+The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long
+as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
+retry limit (default: 2).
+
+A request is deemed retryable when any of the following HTTP status codes is returned:
+
+- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
+- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
+- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
+
+If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly
+over the default exponential backoff.
+
+Use the `option.WithMaxAttempts` option to configure this behavior for the entire client or an individual request:
+
+```go
+client := client.NewClient(
+ option.WithMaxAttempts(1),
+)
+
+response, err := client.Completions.Stream(
+ ...,
+ option.WithMaxAttempts(1),
+)
+```
+
+### Timeouts
+
+Setting a timeout for each individual request is as simple as using the standard context library. Setting a one second timeout for an individual API call looks like the following:
+
+```go
+ctx, cancel := context.WithTimeout(ctx, time.Second)
+defer cancel()
+
+response, err := client.Completions.Stream(ctx, ...)
+```
+
+### Explicit Null
+
+If you want to send the explicit `null` JSON value through an optional parameter, you can use the setters\
+that come with every object. Calling a setter method for a property will flip a bit in the `explicitFields`
+bitfield for that setter's object; during serialization, any property with a flipped bit will have its
+omittable status stripped, so zero or `nil` values will be sent explicitly rather than omitted altogether:
+
+```go
+type ExampleRequest struct {
+ // An optional string parameter.
+ Name *string `json:"name,omitempty" url:"-"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+request := &ExampleRequest{}
+request.SetName(nil)
+
+response, err := client.Completions.Stream(ctx, request, ...)
+```
+
+## Contributing
+
+While we value open-source contributions to this SDK, this library is generated programmatically.
+Additions made directly to this library would have to be moved over to our generation code,
+otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
+a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
+an issue first to discuss with us!
+
+On the other hand, contributions to the README are always very welcome!
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client.go
new file mode 100644
index 000000000000..089696a9ab23
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client.go
@@ -0,0 +1,33 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ completions "github.com/fern-api/sse-examples-go/completions"
+ core "github.com/fern-api/sse-examples-go/core"
+ internal "github.com/fern-api/sse-examples-go/internal"
+ option "github.com/fern-api/sse-examples-go/option"
+)
+
+type Client struct {
+ Completions *completions.Client
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(opts ...option.RequestOption) *Client {
+ options := core.NewRequestOptions(opts...)
+ return &Client{
+ Completions: completions.NewClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client_test.go
new file mode 100644
index 000000000000..50c877227797
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/client/client_test.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ option "github.com/fern-api/sse-examples-go/option"
+ assert "github.com/stretchr/testify/assert"
+ http "net/http"
+ testing "testing"
+ time "time"
+)
+
+func TestNewClient(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ c := NewClient()
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("base url", func(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("test.co"),
+ )
+ assert.Equal(t, "test.co", c.baseURL)
+ })
+
+ t.Run("http client", func(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ c := NewClient(
+ option.WithHTTPClient(httpClient),
+ )
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("http header", func(t *testing.T) {
+ header := make(http.Header)
+ header.Set("X-API-Tenancy", "test")
+ c := NewClient(
+ option.WithHTTPHeader(header),
+ )
+ assert.Empty(t, c.baseURL)
+ assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
+ })
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions.go
new file mode 100644
index 000000000000..a614a561f443
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions.go
@@ -0,0 +1,129 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package sse
+
+import (
+ json "encoding/json"
+ fmt "fmt"
+ internal "github.com/fern-api/sse-examples-go/internal"
+ big "math/big"
+)
+
+var (
+ streamCompletionRequestFieldQuery = big.NewInt(1 << 0)
+)
+
+type StreamCompletionRequest struct {
+ Query string `json:"query" url:"-"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+func (s *StreamCompletionRequest) require(field *big.Int) {
+ if s.explicitFields == nil {
+ s.explicitFields = big.NewInt(0)
+ }
+ s.explicitFields.Or(s.explicitFields, field)
+}
+
+// SetQuery sets the Query field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamCompletionRequest) SetQuery(query string) {
+ s.Query = query
+ s.require(streamCompletionRequestFieldQuery)
+}
+
+var (
+ streamedCompletionFieldDelta = big.NewInt(1 << 0)
+ streamedCompletionFieldTokens = big.NewInt(1 << 1)
+)
+
+type StreamedCompletion struct {
+ Delta string `json:"delta" url:"delta"`
+ Tokens *int `json:"tokens,omitempty" url:"tokens,omitempty"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+
+ extraProperties map[string]interface{}
+ rawJSON json.RawMessage
+}
+
+func (s *StreamedCompletion) GetDelta() string {
+ if s == nil {
+ return ""
+ }
+ return s.Delta
+}
+
+func (s *StreamedCompletion) GetTokens() *int {
+ if s == nil {
+ return nil
+ }
+ return s.Tokens
+}
+
+func (s *StreamedCompletion) GetExtraProperties() map[string]interface{} {
+ return s.extraProperties
+}
+
+func (s *StreamedCompletion) require(field *big.Int) {
+ if s.explicitFields == nil {
+ s.explicitFields = big.NewInt(0)
+ }
+ s.explicitFields.Or(s.explicitFields, field)
+}
+
+// SetDelta sets the Delta field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamedCompletion) SetDelta(delta string) {
+ s.Delta = delta
+ s.require(streamedCompletionFieldDelta)
+}
+
+// SetTokens sets the Tokens field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamedCompletion) SetTokens(tokens *int) {
+ s.Tokens = tokens
+ s.require(streamedCompletionFieldTokens)
+}
+
+func (s *StreamedCompletion) UnmarshalJSON(data []byte) error {
+ type unmarshaler StreamedCompletion
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *s = StreamedCompletion(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *s)
+ if err != nil {
+ return err
+ }
+ s.extraProperties = extraProperties
+ s.rawJSON = json.RawMessage(data)
+ return nil
+}
+
+func (s *StreamedCompletion) MarshalJSON() ([]byte, error) {
+ type embed StreamedCompletion
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+ explicitMarshaler := internal.HandleExplicitFields(marshaler, s.explicitFields)
+ return json.Marshal(explicitMarshaler)
+}
+
+func (s *StreamedCompletion) String() string {
+ if len(s.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(s.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(s); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", s)
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/client.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/client.go
new file mode 100644
index 000000000000..1ca0fe3f49b9
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/client.go
@@ -0,0 +1,71 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions
+
+import (
+ context "context"
+ sse "github.com/fern-api/sse-examples-go"
+ core "github.com/fern-api/sse-examples-go/core"
+ internal "github.com/fern-api/sse-examples-go/internal"
+ option "github.com/fern-api/sse-examples-go/option"
+ http "net/http"
+)
+
+type Client struct {
+ WithRawResponse *RawClient
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(options *core.RequestOptions) *Client {
+ return &Client{
+ WithRawResponse: NewRawClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (c *Client) Stream(
+ ctx context.Context,
+ request *sse.StreamCompletionRequest,
+ opts ...option.RequestOption,
+) (*core.Stream[sse.StreamedCompletion], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ c.baseURL,
+ "",
+ )
+ endpointURL := baseURL + "/stream"
+ headers := internal.MergeHeaders(
+ c.options.ToHeader(),
+ options.ToHeader(),
+ )
+ headers.Add("Accept", "text/event-stream")
+ streamer := internal.NewStreamer[sse.StreamedCompletion](c.caller)
+ return streamer.Stream(
+ ctx,
+ &internal.StreamParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ Prefix: internal.DefaultSSEDataPrefix,
+ Terminator: "[[DONE]]",
+ Format: core.StreamFormatSSE,
+ Request: request,
+ ErrorDecoder: internal.NewErrorDecoder(sse.ErrorCodes),
+ },
+ )
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/completions_test/completions_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/completions_test/completions_test.go
new file mode 100644
index 000000000000..6454372eb37f
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/completions_test/completions_test.go
@@ -0,0 +1,84 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions_test
+
+import (
+ bytes "bytes"
+ context "context"
+ json "encoding/json"
+ sse "github.com/fern-api/sse-examples-go"
+ client "github.com/fern-api/sse-examples-go/client"
+ option "github.com/fern-api/sse-examples-go/option"
+ require "github.com/stretchr/testify/require"
+ http "net/http"
+ testing "testing"
+)
+
+func ResetWireMockRequests(
+ t *testing.T,
+) {
+ WiremockAdminURL := "http://localhost:8080/__admin"
+ _, err := http.Post(WiremockAdminURL+"/requests/reset", "application/json", nil)
+ require.NoError(t, err)
+}
+
+func VerifyRequestCount(
+ t *testing.T,
+ method string,
+ urlPath string,
+ queryParams map[string]string,
+ expected int,
+) {
+ WiremockAdminURL := "http://localhost:8080/__admin"
+ var reqBody bytes.Buffer
+ reqBody.WriteString(`{"method":"`)
+ reqBody.WriteString(method)
+ reqBody.WriteString(`","urlPath":"`)
+ reqBody.WriteString(urlPath)
+ reqBody.WriteString(`"}`)
+ if len(queryParams) > 0 {
+ reqBody.WriteString(`,"queryParameters":{`)
+ first := true
+ for key, value := range queryParams {
+ if !first {
+ reqBody.WriteString(",")
+ }
+ reqBody.WriteString(`"`)
+ reqBody.WriteString(key)
+ reqBody.WriteString(`":{"equalTo":"`)
+ reqBody.WriteString(value)
+ reqBody.WriteString(`"}`)
+ first = false
+ }
+ reqBody.WriteString("}")
+ }
+ resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody)
+ require.NoError(t, err)
+ var result struct {
+ Requests []interface{} `json:"requests"`
+ }
+ json.NewDecoder(resp.Body).Decode(&result)
+ require.Equal(t, expected, len(result.Requests))
+}
+
+func TestCompletionsStreamWithWireMock(
+ t *testing.T,
+) {
+ ResetWireMockRequests(t)
+ WireMockBaseURL := "http://localhost:8080"
+ client := client.NewClient(
+ option.WithBaseURL(
+ WireMockBaseURL,
+ ),
+ )
+ request := &sse.StreamCompletionRequest{
+ Query: "foo",
+ }
+ _, invocationErr := client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+
+ require.NoError(t, invocationErr, "Client method call should succeed")
+ VerifyRequestCount(t, "POST", "/stream", nil, 1)
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/raw_client.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/raw_client.go
new file mode 100644
index 000000000000..195699bb35c9
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/completions/raw_client.go
@@ -0,0 +1,27 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions
+
+import (
+ core "github.com/fern-api/sse-examples-go/core"
+ internal "github.com/fern-api/sse-examples-go/internal"
+)
+
+type RawClient struct {
+ baseURL string
+ caller *internal.Caller
+ options *core.RequestOptions
+}
+
+func NewRawClient(options *core.RequestOptions) *RawClient {
+ return &RawClient{
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/api_error.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/api_error.go
new file mode 100644
index 000000000000..6168388541b4
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/api_error.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// APIError is a lightweight wrapper around the standard error
+// interface that preserves the status code from the RPC, if any.
+type APIError struct {
+ err error
+
+ StatusCode int `json:"-"`
+ Header http.Header `json:"-"`
+}
+
+// NewAPIError constructs a new API error.
+func NewAPIError(statusCode int, header http.Header, err error) *APIError {
+ return &APIError{
+ err: err,
+ Header: header,
+ StatusCode: statusCode,
+ }
+}
+
+// Unwrap returns the underlying error. This also makes the error compatible
+// with errors.As and errors.Is.
+func (a *APIError) Unwrap() error {
+ if a == nil {
+ return nil
+ }
+ return a.err
+}
+
+// Error returns the API error's message.
+func (a *APIError) Error() string {
+ if a == nil || (a.err == nil && a.StatusCode == 0) {
+ return ""
+ }
+ if a.err == nil {
+ return fmt.Sprintf("%d", a.StatusCode)
+ }
+ if a.StatusCode == 0 {
+ return a.err.Error()
+ }
+ return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/http.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/http.go
new file mode 100644
index 000000000000..92c435692940
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/http.go
@@ -0,0 +1,15 @@
+package core
+
+import "net/http"
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Response is an HTTP response from an HTTP client.
+type Response[T any] struct {
+ StatusCode int
+ Header http.Header
+ Body T
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/request_option.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/request_option.go
new file mode 100644
index 000000000000..0c2f3608866d
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/request_option.go
@@ -0,0 +1,109 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of the client or an individual request.
+type RequestOption interface {
+ applyRequestOptions(*RequestOptions)
+}
+
+// RequestOptions defines all of the possible request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type RequestOptions struct {
+ BaseURL string
+ HTTPClient HTTPClient
+ HTTPHeader http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ MaxAttempts uint
+}
+
+// NewRequestOptions returns a new *RequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use RequestOption instead.
+func NewRequestOptions(opts ...RequestOption) *RequestOptions {
+ options := &RequestOptions{
+ HTTPHeader: make(http.Header),
+ BodyProperties: make(map[string]interface{}),
+ QueryParameters: make(url.Values),
+ }
+ for _, opt := range opts {
+ opt.applyRequestOptions(options)
+ }
+ return options
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request(s).
+func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() }
+
+func (r *RequestOptions) cloneHeader() http.Header {
+ headers := r.HTTPHeader.Clone()
+ headers.Set("X-Fern-Language", "Go")
+ headers.Set("X-Fern-SDK-Name", "github.com/fern-api/sse-examples-go")
+ headers.Set("X-Fern-SDK-Version", "v0.0.1")
+ headers.Set("User-Agent", "github.com/server-sent-event-examples/fern/0.0.1")
+ return headers
+}
+
+// BaseURLOption implements the RequestOption interface.
+type BaseURLOption struct {
+ BaseURL string
+}
+
+func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+// HTTPClientOption implements the RequestOption interface.
+type HTTPClientOption struct {
+ HTTPClient HTTPClient
+}
+
+func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+// HTTPHeaderOption implements the RequestOption interface.
+type HTTPHeaderOption struct {
+ HTTPHeader http.Header
+}
+
+func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+// BodyPropertiesOption implements the RequestOption interface.
+type BodyPropertiesOption struct {
+ BodyProperties map[string]interface{}
+}
+
+func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+// QueryParametersOption implements the RequestOption interface.
+type QueryParametersOption struct {
+ QueryParameters url.Values
+}
+
+func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+// MaxAttemptsOption implements the RequestOption interface.
+type MaxAttemptsOption struct {
+ MaxAttempts uint
+}
+
+func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/stream.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/stream.go
new file mode 100644
index 000000000000..25c528e89516
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/core/stream.go
@@ -0,0 +1,368 @@
+package core
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "slices"
+ "strings"
+)
+
+type StreamFormat string
+
+const (
+ StreamFormatSSE StreamFormat = "sse"
+ StreamFormatEmpty StreamFormat = ""
+)
+
+const (
+ sseEventSeparator = "\n\n"
+ sseLineSeparator = "\n"
+)
+
+const (
+ defaultMaxBufSize = 64 * 1024 // 64KB
+)
+
+// Stream represents a stream of messages sent from a server.
+type Stream[T any] struct {
+ reader streamReader
+ closer io.Closer
+}
+
+// StreamOption adapts the behavior of the Stream.
+type StreamOption func(*streamOptions)
+
+// WithDelimiter overrides the delimiter for the Stream.
+//
+// By default, the Stream is newline-delimited.
+func WithDelimiter(delimiter string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.delimiter = delimiter
+ }
+}
+
+// WithPrefix overrides the prefix for the Stream.
+//
+// By default, the Stream doesn't have a prefix.
+func WithPrefix(prefix string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.prefix = prefix
+ }
+}
+
+// WithTerminator overrides the terminator for the Stream.
+//
+// By default, the Stream terminates on EOF.
+func WithTerminator(terminator string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.terminator = terminator
+ }
+}
+
+// WithFormat overrides the isSSE flag for the Stream.
+//
+// By default, the Stream is not SSE.
+func WithFormat(format StreamFormat) StreamOption {
+ return func(opts *streamOptions) {
+ opts.format = format
+ }
+}
+
+// NewStream constructs a new Stream from the given *http.Response.
+func NewStream[T any](response *http.Response, opts ...StreamOption) *Stream[T] {
+ options := new(streamOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ return &Stream[T]{
+ reader: newStreamReader(response.Body, options),
+ closer: response.Body,
+ }
+}
+
+// Recv reads a message from the stream, returning io.EOF when
+// all the messages have been read.
+func (s Stream[T]) Recv() (T, error) {
+ var value T
+ bytes, err := s.reader.ReadFromStream()
+ if err != nil {
+ return value, err
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return value, err
+ }
+ return value, nil
+}
+
+// Close closes the Stream.
+func (s Stream[T]) Close() error {
+ return s.closer.Close()
+}
+
+// streamReader reads data from a stream.
+type streamReader interface {
+ ReadFromStream() ([]byte, error)
+}
+
+// newStreamReader returns a new streamReader based on the given
+// delimiter.
+//
+// By default, the streamReader uses a simple a *bufio.Reader
+// which splits on newlines, and otherwise use a *bufio.Scanner to
+// split on custom delimiters.
+func newStreamReader(reader io.Reader, options *streamOptions) streamReader {
+ if !options.isEmpty() {
+ if options.maxBufSize == 0 {
+ options.maxBufSize = defaultMaxBufSize
+ }
+ if options.format == StreamFormatSSE {
+ return newSseStreamReader(reader, options)
+ }
+ return newScannerStreamReader(reader, options)
+ }
+ return newBufferStreamReader(reader)
+}
+
+// BufferStreamReader reads data from a *bufio.Reader, which splits
+// on newlines.
+type BufferStreamReader struct {
+ reader *bufio.Reader
+}
+
+func newBufferStreamReader(reader io.Reader) *BufferStreamReader {
+ return &BufferStreamReader{
+ reader: bufio.NewReader(reader),
+ }
+}
+
+func (b *BufferStreamReader) ReadFromStream() ([]byte, error) {
+ line, err := b.reader.ReadBytes('\n')
+ if err != nil {
+ return nil, err
+ }
+ // Strip the trailing newline
+ return bytes.TrimSuffix(line, []byte("\n")), nil
+}
+
+// ScannerStreamReader reads data from a *bufio.Scanner, which allows for
+// configurable delimiters.
+type ScannerStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newScannerStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *ScannerStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &ScannerStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Split(func(bytes []byte, atEOF bool) (int, []byte, error) {
+ if atEOF && len(bytes) == 0 {
+ return 0, nil, nil
+ }
+ n, data, err := stream.parse(bytes)
+ if stream.isTerminated(data) {
+ return 0, nil, io.EOF
+ }
+ return n, data, err
+ })
+ return stream
+}
+
+func (s *ScannerStreamReader) ReadFromStream() ([]byte, error) {
+ if s.scanner.Scan() {
+ return s.scanner.Bytes(), nil
+ }
+ if err := s.scanner.Err(); err != nil {
+ return nil, err
+ }
+ return nil, io.EOF
+}
+
+func (s *ScannerStreamReader) parse(bytes []byte) (int, []byte, error) {
+ var startIndex int
+ if s.options != nil && s.options.prefix != "" {
+ if i := strings.Index(string(bytes), s.options.prefix); i >= 0 {
+ startIndex = i + len(s.options.prefix)
+ }
+ }
+ data := bytes[startIndex:]
+ lineDelimiter := s.options.getLineDelimiter()
+ delimIndex := strings.Index(string(data), lineDelimiter)
+ if delimIndex < 0 {
+ return startIndex + len(data), data, nil
+ }
+ endIndex := delimIndex + len(lineDelimiter)
+ parsedData := data[:endIndex]
+ n := startIndex + endIndex
+ return n, parsedData, nil
+}
+
+func (s *ScannerStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+type streamOptions struct {
+ delimiter string
+ prefix string
+ terminator string
+ format StreamFormat
+ maxBufSize int
+}
+
+func (s *streamOptions) isEmpty() bool {
+ return s.delimiter == "" && s.prefix == "" && s.terminator == "" && s.format == StreamFormatEmpty
+}
+
+func (s *streamOptions) getLineDelimiter() string {
+ if s.delimiter != "" {
+ return s.delimiter
+ }
+ return sseLineSeparator
+}
+
+type SseStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newSseStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *SseStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &SseStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Buffer(make([]byte, slices.Min([]int{4096, options.maxBufSize})), options.maxBufSize)
+
+ // Configure scanner to split on SSE event separator (\n\n)
+ // This is fixed by the SSE specification and cannot be changed
+ scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ // SSE messages are always separated by blank lines (\n\n)
+ if i := strings.Index(string(data), sseEventSeparator); i >= 0 {
+ return i + len(sseEventSeparator), data[0:i], nil
+ }
+
+ if atEOF || stream.isTerminated(data) {
+ return len(data), data, nil
+ }
+ return 0, nil, nil
+ })
+ return stream
+}
+
+func (s *SseStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+func (s *SseStreamReader) ReadFromStream() ([]byte, error) {
+
+ event, err := s.nextEvent()
+ if err != nil {
+ return nil, err
+ }
+ return event.data, nil
+}
+
+func (s *SseStreamReader) nextEvent() (*SseEvent, error) {
+
+ event := SseEvent{}
+ if s.scanner.Scan() {
+ rawEvent := s.scanner.Bytes()
+
+ // Parse individual lines within the SSE message
+ // Lines are always separated by \n within a message (SSE specification)
+ lines := strings.Split(string(rawEvent), sseLineSeparator)
+ for _, line := range lines {
+ s.parseSseLine([]byte(line), &event)
+ }
+
+ if event.size() > s.options.maxBufSize {
+ return nil, errors.New("SseStreamReader.ReadFromStream: buffer limit exceeded")
+ }
+ return &event, nil
+ }
+ return &event, io.EOF
+}
+
+func (s *SseStreamReader) parseSseLine(_bytes []byte, event *SseEvent) {
+ // Try to parse with space first (standard format), then without space (lenient format)
+ if value, ok := s.tryParseField(_bytes, sseDataPrefix, sseDataPrefixNoSpace); ok {
+ if len(event.data) > 0 {
+ // Join multiple data: lines using the configured delimiter
+ // This allows customization of how multi-line data is concatenated:
+ // - "\n" (default): preserves line breaks for multi-line JSON
+ // - "": concatenates without separator
+ // - Any other string: custom separator
+ lineDelimiter := s.options.getLineDelimiter()
+ event.data = append(event.data, lineDelimiter...)
+ }
+ event.data = append(event.data, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseIdPrefix, sseIdPrefixNoSpace); ok {
+ event.id = append(event.id, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseEventPrefix, sseEventPrefixNoSpace); ok {
+ event.event = append(event.event, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseRetryPrefix, sseRetryPrefixNoSpace); ok {
+ event.retry = append(event.retry, value...)
+ }
+}
+
+// tryParseField attempts to parse an SSE field by trying multiple prefix patterns in order.
+// This handles APIs that don't strictly follow the SSE specification by omitting the space after the colon.
+// It tries each prefix in the order provided and returns the value after the first matching prefix.
+func (s *SseStreamReader) tryParseField(line []byte, prefixes ...[]byte) ([]byte, bool) {
+ for _, prefix := range prefixes {
+ if bytes.HasPrefix(line, prefix) {
+ return line[len(prefix):], true
+ }
+ }
+ return nil, false
+}
+
+func (event *SseEvent) size() int {
+ return len(event.id) + len(event.data) + len(event.event) + len(event.retry)
+}
+
+func (event *SseEvent) String() string {
+ return fmt.Sprintf("SseEvent{id: %q, event: %q, data: %q, retry: %q}", event.id, event.event, event.data, event.retry)
+}
+
+type SseEvent struct {
+ id []byte
+ data []byte
+ event []byte
+ retry []byte
+}
+
+var (
+ sseIdPrefix = []byte("id: ")
+ sseDataPrefix = []byte("data: ")
+ sseEventPrefix = []byte("event: ")
+ sseRetryPrefix = []byte("retry: ")
+
+ // Lenient prefixes without space for APIs that don't strictly follow SSE specification
+ sseIdPrefixNoSpace = []byte("id:")
+ sseDataPrefixNoSpace = []byte("data:")
+ sseEventPrefixNoSpace = []byte("event:")
+ sseRetryPrefixNoSpace = []byte("retry:")
+)
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example0/snippet.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example0/snippet.go
new file mode 100644
index 000000000000..1e2912bb8605
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example0/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/sse-examples-go/client"
+ option "github.com/fern-api/sse-examples-go/option"
+ sse "github.com/fern-api/sse-examples-go"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &sse.StreamCompletionRequest{
+ Query: "foo",
+ }
+ client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example1/snippet.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example1/snippet.go
new file mode 100644
index 000000000000..4461433544fe
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/dynamic-snippets/example1/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/sse-examples-go/client"
+ option "github.com/fern-api/sse-examples-go/option"
+ sse "github.com/fern-api/sse-examples-go"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &sse.StreamCompletionRequest{
+ Query: "query",
+ }
+ client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/error_codes.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/error_codes.go
new file mode 100644
index 000000000000..705434b518c7
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/error_codes.go
@@ -0,0 +1,9 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package sse
+
+import (
+ internal "github.com/fern-api/sse-examples-go/internal"
+)
+
+var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/file_param.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/file_param.go
new file mode 100644
index 000000000000..16e0931f7015
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/file_param.go
@@ -0,0 +1,41 @@
+package sse
+
+import (
+ "io"
+)
+
+// FileParam is a file type suitable for multipart/form-data uploads.
+type FileParam struct {
+ io.Reader
+ filename string
+ contentType string
+}
+
+// FileParamOption adapts the behavior of the FileParam. No options are
+// implemented yet, but this interface allows for future extensibility.
+type FileParamOption interface {
+ apply()
+}
+
+// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file
+// upload endpoints accept a simple io.Reader, which is usually created by opening a file
+// via os.Open.
+//
+// However, some endpoints require additional metadata about the file such as a specific
+// Content-Type or custom filename. FileParam makes it easier to create the correct type
+// signature for these endpoints.
+func NewFileParam(
+ reader io.Reader,
+ filename string,
+ contentType string,
+ opts ...FileParamOption,
+) *FileParam {
+ return &FileParam{
+ Reader: reader,
+ filename: filename,
+ contentType: contentType,
+ }
+}
+
+func (f *FileParam) Name() string { return f.filename }
+func (f *FileParam) ContentType() string { return f.contentType }
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.mod b/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.mod
new file mode 100644
index 000000000000..0d2e97cea9bb
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.mod
@@ -0,0 +1,16 @@
+module github.com/fern-api/sse-examples-go
+
+go 1.21
+
+toolchain go1.23.8
+
+require github.com/google/uuid v1.6.0
+
+require github.com/stretchr/testify v1.8.4
+
+require gopkg.in/yaml.v3 v3.0.1 // indirect
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+)
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.sum b/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.sum
new file mode 100644
index 000000000000..fcca6d128057
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/go.sum
@@ -0,0 +1,12 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller.go
new file mode 100644
index 000000000000..8621a62247ef
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller.go
@@ -0,0 +1,250 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+
+ "github.com/fern-api/sse-examples-go/core"
+)
+
+const (
+ // contentType specifies the JSON Content-Type header value.
+ contentType = "application/json"
+ contentTypeHeader = "Content-Type"
+)
+
+// Caller calls APIs and deserializes their response, if any.
+type Caller struct {
+ client core.HTTPClient
+ retrier *Retrier
+}
+
+// CallerParams represents the parameters used to constrcut a new *Caller.
+type CallerParams struct {
+ Client core.HTTPClient
+ MaxAttempts uint
+}
+
+// NewCaller returns a new *Caller backed by the given parameters.
+func NewCaller(params *CallerParams) *Caller {
+ var httpClient core.HTTPClient = http.DefaultClient
+ if params.Client != nil {
+ httpClient = params.Client
+ }
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+ return &Caller{
+ client: httpClient,
+ retrier: NewRetrier(retryOptions...),
+ }
+}
+
+// CallParams represents the parameters used to issue an API call.
+type CallParams struct {
+ URL string
+ Method string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client core.HTTPClient
+ Request interface{}
+ Response interface{}
+ ResponseIsOptional bool
+ ErrorDecoder ErrorDecoder
+}
+
+// CallResponse is a parsed HTTP response from an API call.
+type CallResponse struct {
+ StatusCode int
+ Header http.Header
+}
+
+// Call issues an API call according to the given call parameters.
+func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := c.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := c.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Close the response body after we're done.
+ defer resp.Body.Close()
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ // Mutate the response parameter in-place.
+ if params.Response != nil {
+ if writer, ok := params.Response.(io.Writer); ok {
+ _, err = io.Copy(writer, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(params.Response)
+ }
+ if err != nil {
+ if err == io.EOF {
+ if params.ResponseIsOptional {
+ // The response is optional, so we should ignore the
+ // io.EOF error
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+ }
+ return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
+ }
+ return nil, err
+ }
+ }
+
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+}
+
+// buildURL constructs the final URL by appending the given query parameters (if any).
+func buildURL(
+ url string,
+ queryParameters url.Values,
+) string {
+ if len(queryParameters) == 0 {
+ return url
+ }
+ if strings.ContainsRune(url, '?') {
+ url += "&"
+ } else {
+ url += "?"
+ }
+ url += queryParameters.Encode()
+ return url
+}
+
+// newRequest returns a new *http.Request with all of the fields
+// required to issue the call.
+func newRequest(
+ ctx context.Context,
+ url string,
+ method string,
+ endpointHeaders http.Header,
+ request interface{},
+ bodyProperties map[string]interface{},
+) (*http.Request, error) {
+ requestBody, err := newRequestBody(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ req.Header.Set(contentTypeHeader, contentType)
+ for name, values := range endpointHeaders {
+ req.Header[name] = values
+ }
+ return req, nil
+}
+
+// newRequestBody returns a new io.Reader that represents the HTTP request body.
+func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
+ if isNil(request) {
+ if len(bodyProperties) == 0 {
+ return nil, nil
+ }
+ requestBytes, err := json.Marshal(bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+ }
+ if body, ok := request.(io.Reader); ok {
+ return body, nil
+ }
+ requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+}
+
+// decodeError decodes the error from the given HTTP response. Note that
+// it's the caller's responsibility to close the response body.
+func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
+ if errorDecoder != nil {
+ // This endpoint has custom errors, so we'll
+ // attempt to unmarshal the error into a structured
+ // type based on the status code.
+ return errorDecoder(response.StatusCode, response.Header, response.Body)
+ }
+ // This endpoint doesn't have any custom error
+ // types, so we just read the body as-is, and
+ // put it into a normal error.
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if err == io.EOF {
+ // The error didn't have a response body,
+ // so all we can do is return an error
+ // with the status code.
+ return core.NewAPIError(response.StatusCode, response.Header, nil)
+ }
+ return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
+}
+
+// isNil is used to determine if the request value is equal to nil (i.e. an interface
+// value that holds a nil concrete value is itself non-nil).
+func isNil(value interface{}) bool {
+ return value == nil || reflect.ValueOf(value).IsNil()
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller_test.go
new file mode 100644
index 000000000000..5023080248f8
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/caller_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/fern-api/sse-examples-go/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// InternalTestCase represents a single test case.
+type InternalTestCase struct {
+ description string
+
+ // Server-side assertions.
+ givePathSuffix string
+ giveMethod string
+ giveResponseIsOptional bool
+ giveHeader http.Header
+ giveErrorDecoder ErrorDecoder
+ giveRequest *InternalTestRequest
+ giveQueryParams url.Values
+ giveBodyProperties map[string]interface{}
+
+ // Client-side assertions.
+ wantResponse *InternalTestResponse
+ wantHeaders http.Header
+ wantError error
+}
+
+// InternalTestRequest a simple request body.
+type InternalTestRequest struct {
+ Id string `json:"id"`
+}
+
+// InternalTestResponse a simple response body.
+type InternalTestResponse struct {
+ Id string `json:"id"`
+ ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
+ QueryParameters url.Values `json:"queryParameters,omitempty"`
+}
+
+// InternalTestNotFoundError represents a 404.
+type InternalTestNotFoundError struct {
+ *core.APIError
+
+ Message string `json:"message"`
+}
+
+func TestCall(t *testing.T) {
+ tests := []*InternalTestCase{
+ {
+ description: "GET success",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ },
+ },
+ {
+ description: "GET success with query",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ },
+ },
+ },
+ {
+ description: "GET not found",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusNotFound),
+ },
+ giveErrorDecoder: newTestErrorDecoder(t),
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(
+ http.StatusNotFound,
+ http.Header{},
+ errors.New(`{"message":"ID \"404\" not found"}`),
+ ),
+ },
+ },
+ {
+ description: "POST empty body",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: nil,
+ wantError: core.NewAPIError(
+ http.StatusBadRequest,
+ http.Header{},
+ errors.New("invalid request"),
+ ),
+ },
+ {
+ description: "POST optional response",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveResponseIsOptional: true,
+ },
+ {
+ description: "POST API error",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusInternalServerError),
+ },
+ wantError: core.NewAPIError(
+ http.StatusInternalServerError,
+ http.Header{},
+ errors.New("failed to process request"),
+ ),
+ },
+ {
+ description: "POST extra properties",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: new(InternalTestRequest),
+ giveBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ wantResponse: &InternalTestResponse{
+ ExtraBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ },
+ },
+ {
+ description: "GET extra query parameters",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "extra": []string{"true"},
+ },
+ },
+ },
+ {
+ description: "GET merge extra query parameters",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ "extra": []string{"true"},
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ var (
+ server = newTestServer(t, test)
+ client = server.Client()
+ )
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL + test.givePathSuffix,
+ Method: test.giveMethod,
+ Headers: test.giveHeader,
+ BodyProperties: test.giveBodyProperties,
+ QueryParameters: test.giveQueryParams,
+ Request: test.giveRequest,
+ Response: &response,
+ ResponseIsOptional: test.giveResponseIsOptional,
+ ErrorDecoder: test.giveErrorDecoder,
+ },
+ )
+ if test.wantError != nil {
+ assert.EqualError(t, err, test.wantError.Error())
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+func TestMergeHeaders(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ merged := MergeHeaders(make(http.Header), make(http.Header))
+ assert.Empty(t, merged)
+ })
+
+ t.Run("empty left", func(t *testing.T) {
+ left := make(http.Header)
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("empty right", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.1")
+
+ right := make(http.Header)
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("single value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.0")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+
+ t.Run("multiple value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Versions", "0.0.0")
+
+ right := make(http.Header)
+ right.Add("X-API-Versions", "0.0.1")
+ right.Add("X-API-Versions", "0.0.2")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
+ })
+
+ t.Run("disjoint merge", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Tenancy", "test")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+}
+
+// newTestServer returns a new *httptest.Server configured with the
+// given test parameters.
+func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tc.giveMethod, r.Method)
+ assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
+ for header, value := range tc.giveHeader {
+ assert.Equal(t, value, r.Header.Values(header))
+ }
+
+ request := new(InternalTestRequest)
+
+ bytes, err := io.ReadAll(r.Body)
+ if tc.giveRequest == nil {
+ require.Empty(t, bytes)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err = w.Write([]byte("invalid request"))
+ require.NoError(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+
+ switch request.Id {
+ case strconv.Itoa(http.StatusNotFound):
+ notFoundError := &InternalTestNotFoundError{
+ APIError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ Message: fmt.Sprintf("ID %q not found", request.Id),
+ }
+ bytes, err = json.Marshal(notFoundError)
+ require.NoError(t, err)
+
+ w.WriteHeader(http.StatusNotFound)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ return
+
+ case strconv.Itoa(http.StatusInternalServerError):
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = w.Write([]byte("failed to process request"))
+ require.NoError(t, err)
+ return
+ }
+
+ if tc.giveResponseIsOptional {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ extraBodyProperties := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
+ delete(extraBodyProperties, "id")
+
+ response := &InternalTestResponse{
+ Id: request.Id,
+ ExtraBodyProperties: extraBodyProperties,
+ QueryParameters: r.URL.Query(),
+ }
+ bytes, err = json.Marshal(response)
+ require.NoError(t, err)
+
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ },
+ ),
+ )
+}
+
+// newTestErrorDecoder returns an error decoder suitable for tests.
+func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ require.NoError(t, err)
+
+ var (
+ apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
+ decoder = json.NewDecoder(bytes.NewReader(raw))
+ )
+ if statusCode == http.StatusNotFound {
+ value := new(InternalTestNotFoundError)
+ value.APIError = apiError
+ require.NoError(t, decoder.Decode(value))
+
+ return value
+ }
+ return apiError
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder.go
new file mode 100644
index 000000000000..8495b4212597
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder.go
@@ -0,0 +1,64 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/fern-api/sse-examples-go/core"
+)
+
+// ErrorCodes maps HTTP status codes to error constructors.
+type ErrorCodes map[int]func(*core.APIError) error
+
+// ErrorDecoder decodes *http.Response errors and returns a
+// typed API error (e.g. *core.APIError).
+type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
+
+// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
+// errorCodesOverrides is optional and will be merged with the default error codes,
+// with overrides taking precedence.
+func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
+ // Merge default error codes with overrides
+ mergedErrorCodes := make(ErrorCodes)
+
+ // Start with default error codes
+ for statusCode, errorFunc := range errorCodes {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+
+ // Apply overrides if provided
+ if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
+ for statusCode, errorFunc := range errorCodesOverrides[0] {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+ }
+
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ if err != nil {
+ return fmt.Errorf("failed to read error from response body: %w", err)
+ }
+ apiError := core.NewAPIError(
+ statusCode,
+ header,
+ errors.New(string(raw)),
+ )
+ newErrorFunc, ok := mergedErrorCodes[statusCode]
+ if !ok {
+ // This status code isn't recognized, so we return
+ // the API error as-is.
+ return apiError
+ }
+ customError := newErrorFunc(apiError)
+ if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
+ // If we fail to decode the error, we return the
+ // API error as-is.
+ return apiError
+ }
+ return customError
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder_test.go
new file mode 100644
index 000000000000..332ab90b24e2
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/error_decoder_test.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/fern-api/sse-examples-go/core"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorDecoder(t *testing.T) {
+ decoder := NewErrorDecoder(
+ ErrorCodes{
+ http.StatusNotFound: func(apiError *core.APIError) error {
+ return &InternalTestNotFoundError{APIError: apiError}
+ },
+ })
+
+ tests := []struct {
+ description string
+ giveStatusCode int
+ giveHeader http.Header
+ giveBody string
+ wantError error
+ }{
+ {
+ description: "unrecognized status code",
+ giveStatusCode: http.StatusInternalServerError,
+ giveHeader: http.Header{},
+ giveBody: "Internal Server Error",
+ wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
+ },
+ {
+ description: "not found with valid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `{"message": "Resource not found"}`,
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
+ Message: "Resource not found",
+ },
+ },
+ {
+ description: "not found with invalid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `Resource not found`,
+ wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
+ })
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields.go
new file mode 100644
index 000000000000..4bdf34fc2b7c
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+ "math/big"
+ "reflect"
+ "strings"
+)
+
+// HandleExplicitFields processes a struct to remove `omitempty` from
+// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
+// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
+// Returns an interface{} that can be passed to json.Marshal.
+func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
+ val := reflect.ValueOf(marshaler)
+ typ := reflect.TypeOf(marshaler)
+
+ // Handle pointer types
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil
+ }
+ val = val.Elem()
+ typ = typ.Elem()
+ }
+
+ // Only handle struct types
+ if val.Kind() != reflect.Struct {
+ return marshaler
+ }
+
+ // Handle embedded struct pattern
+ var sourceVal reflect.Value
+ var sourceType reflect.Type
+
+ // Check if this is an embedded struct pattern
+ if typ.NumField() == 1 && typ.Field(0).Anonymous {
+ // This is likely an embedded struct, get the embedded value
+ embeddedField := val.Field(0)
+ sourceVal = embeddedField
+ sourceType = embeddedField.Type()
+ } else {
+ // Regular struct
+ sourceVal = val
+ sourceType = typ
+ }
+
+ // If no explicit fields set, use standard marshaling
+ if explicitFields == nil || explicitFields.Sign() == 0 {
+ return marshaler
+ }
+
+ // Create a new struct type with modified tags
+ fields := make([]reflect.StructField, 0, sourceType.NumField())
+
+ for i := 0; i < sourceType.NumField(); i++ {
+ field := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !field.IsExported() || field.Name == "explicitFields" {
+ continue
+ }
+
+ // Check if this field has been explicitly set
+ fieldBit := big.NewInt(1)
+ fieldBit.Lsh(fieldBit, uint(i))
+ if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
+ // Remove omitempty from the json tag
+ tag := field.Tag.Get("json")
+ if tag != "" && tag != "-" {
+ // Parse the json tag, remove omitempty from options
+ parts := strings.Split(tag, ",")
+ if len(parts) > 1 {
+ var newParts []string
+ newParts = append(newParts, parts[0]) // Keep the field name
+ for _, part := range parts[1:] {
+ if strings.TrimSpace(part) != "omitempty" {
+ newParts = append(newParts, part)
+ }
+ }
+ tag = strings.Join(newParts, ",")
+ }
+
+ // Reconstruct the struct tag
+ newTag := `json:"` + tag + `"`
+ if urlTag := field.Tag.Get("url"); urlTag != "" {
+ newTag += ` url:"` + urlTag + `"`
+ }
+
+ field.Tag = reflect.StructTag(newTag)
+ }
+ }
+
+ fields = append(fields, field)
+ }
+
+ // Create new struct type with modified tags
+ newType := reflect.StructOf(fields)
+ newVal := reflect.New(newType).Elem()
+
+ // Copy field values from original struct to new struct
+ fieldIndex := 0
+ for i := 0; i < sourceType.NumField(); i++ {
+ originalField := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !originalField.IsExported() || originalField.Name == "explicitFields" {
+ continue
+ }
+
+ originalValue := sourceVal.Field(i)
+ newVal.Field(fieldIndex).Set(originalValue)
+ fieldIndex++
+ }
+
+ return newVal.Interface()
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields_test.go
new file mode 100644
index 000000000000..3d05e88a2ce9
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/explicit_fields_test.go
@@ -0,0 +1,497 @@
+package internal
+
+import (
+ "encoding/json"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testExplicitFieldsStruct struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+ Count *int `json:"count,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ //lint:ignore unused this field is intentionally unused for testing
+ unexported string `json:"-"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ testFieldName = big.NewInt(1 << 0)
+ testFieldCode = big.NewInt(1 << 1)
+ testFieldCount = big.NewInt(1 << 2)
+ testFieldEnabled = big.NewInt(1 << 3)
+ testFieldTags = big.NewInt(1 << 4)
+)
+
+func (t *testExplicitFieldsStruct) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+func (t *testExplicitFieldsStruct) SetName(name *string) {
+ t.Name = name
+ t.require(testFieldName)
+}
+
+func (t *testExplicitFieldsStruct) SetCode(code *string) {
+ t.Code = code
+ t.require(testFieldCode)
+}
+
+func (t *testExplicitFieldsStruct) SetCount(count *int) {
+ t.Count = count
+ t.require(testFieldCount)
+}
+
+func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
+ t.Enabled = enabled
+ t.require(testFieldEnabled)
+}
+
+func (t *testExplicitFieldsStruct) SetTags(tags []string) {
+ t.Tags = tags
+ t.require(testFieldTags)
+}
+
+func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*t),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
+}
+
+type testStructWithoutExplicitFields struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+}
+
+func TestHandleExplicitFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveInput interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "nil input",
+ giveInput: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "non-struct input",
+ giveInput: "string",
+ wantBytes: []byte(`"string"`),
+ },
+ {
+ desc: "slice input",
+ giveInput: []string{"a", "b"},
+ wantBytes: []byte(`["a","b"]`),
+ },
+ {
+ desc: "map input",
+ giveInput: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "struct without explicitFields field",
+ giveInput: &testStructWithoutExplicitFields{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with no explicit fields set",
+ giveInput: &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with explicit nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null}`),
+ },
+ {
+ desc: "struct with explicit non-nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("explicit"))
+ s.SetCode(stringPtr("also-explicit"))
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
+ },
+ {
+ desc: "struct with mixed explicit and implicit fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Count: intPtr(42),
+ }
+ s.SetCode(nil) // explicit nil
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
+ },
+ {
+ desc: "struct with multiple explicit nil fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ s.SetCount(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
+ },
+ {
+ desc: "struct with slice field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Tags: []string{"tag1", "tag2"},
+ }
+ s.SetTags(nil) // explicit nil slice
+ return s
+ }(),
+ wantBytes: []byte(`{"tags":null}`),
+ },
+ {
+ desc: "struct with boolean field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetEnabled(boolPtr(false)) // explicit false
+ return s
+ }(),
+ wantBytes: []byte(`{"enabled":false}`),
+ },
+ {
+ desc: "struct with all fields explicit",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("test"))
+ s.SetCode(nil)
+ s.SetCount(intPtr(0))
+ s.SetEnabled(boolPtr(false))
+ s.SetTags([]string{})
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ var explicitFields *big.Int
+ if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
+ explicitFields = s.explicitFields
+ }
+ bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
+ t.Run("custom marshaler with explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
+ t.Run("nil pointer", func(t *testing.T) {
+ var s *testExplicitFieldsStruct
+ bytes, err := json.Marshal(HandleExplicitFields(s, nil))
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`null`), bytes)
+ })
+
+ t.Run("pointer to struct", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
+ t.Run("embedded struct with explicit fields", func(t *testing.T) {
+ // Create a struct similar to what MarshalJSON creates
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include both explicit fields (name as null, code as "test-code")
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should only include non-nil fields (omitempty behavior)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with mixed fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Count: intPtr(42), // implicit field
+ }
+ s.SetName(nil) // explicit nil
+ s.SetCode(stringPtr("explicit")) // explicit value
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include explicit null, explicit value, and implicit value
+ assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsTagHandling(t *testing.T) {
+ type testStructWithComplexTags struct {
+ Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
+ Field2 *string `json:"field2,omitempty,string" url:"field2"`
+ Field3 *string `json:"-"`
+ Field4 *string `json:"field4"`
+ explicitFields *big.Int `json:"-"`
+ }
+
+ s := &testStructWithComplexTags{
+ Field1: stringPtr("test1"),
+ Field4: stringPtr("test4"),
+ explicitFields: big.NewInt(1), // Only first field is explicit
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+
+ // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
+ assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
+}
+
+// Test types for nested struct explicit fields testing
+type testNestedStruct struct {
+ NestedName *string `json:"nested_name,omitempty"`
+ NestedCode *string `json:"nested_code,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+type testParentStruct struct {
+ ParentName *string `json:"parent_name,omitempty"`
+ Nested *testNestedStruct `json:"nested,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ nestedFieldName = big.NewInt(1 << 0)
+ nestedFieldCode = big.NewInt(1 << 1)
+)
+
+var (
+ parentFieldName = big.NewInt(1 << 0)
+ parentFieldNested = big.NewInt(1 << 1)
+)
+
+func (n *testNestedStruct) require(field *big.Int) {
+ if n.explicitFields == nil {
+ n.explicitFields = big.NewInt(0)
+ }
+ n.explicitFields.Or(n.explicitFields, field)
+}
+
+func (n *testNestedStruct) SetNestedName(name *string) {
+ n.NestedName = name
+ n.require(nestedFieldName)
+}
+
+func (n *testNestedStruct) SetNestedCode(code *string) {
+ n.NestedCode = code
+ n.require(nestedFieldCode)
+}
+
+func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
+ type embed testNestedStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*n),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
+}
+
+func (p *testParentStruct) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+func (p *testParentStruct) SetParentName(name *string) {
+ p.ParentName = name
+ p.require(parentFieldName)
+}
+
+func (p *testParentStruct) SetNested(nested *testNestedStruct) {
+ p.Nested = nested
+ p.require(parentFieldNested)
+}
+
+func (p *testParentStruct) MarshalJSON() ([]byte, error) {
+ type embed testParentStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
+}
+
+func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
+ tests := []struct {
+ desc string
+ setupFunc func() *testParentStruct
+ wantBytes []byte
+ }{
+ {
+ desc: "nested struct with explicit nil in nested object",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{
+ NestedName: stringPtr("implicit-nested"),
+ }
+ nested.SetNestedCode(nil) // explicit nil
+
+ return &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ Nested: nested,
+ }
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
+ },
+ {
+ desc: "parent with explicit nil nested struct",
+ setupFunc: func() *testParentStruct {
+ parent := &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ }
+ parent.SetNested(nil) // explicit nil nested struct
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
+ },
+ {
+ desc: "all explicit fields in nested structure",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{}
+ nested.SetNestedName(stringPtr("explicit-nested"))
+ nested.SetNestedCode(nil) // explicit nil
+
+ parent := &testParentStruct{}
+ parent.SetParentName(nil) // explicit nil
+ parent.SetNested(nested) // explicit nested struct
+
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ parent := tt.setupFunc()
+ bytes, err := parent.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties.go
new file mode 100644
index 000000000000..540c3fd89eeb
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]interface{}
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value interface{}) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties_test.go
new file mode 100644
index 000000000000..aa2510ee5121
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler interface{}
+ giveExtraProperties map[string]interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]interface{}{42: "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]interface{}{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/http.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/http.go
new file mode 100644
index 000000000000..77863752bb58
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/http.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+)
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// ResolveBaseURL resolves the base URL from the given arguments,
+// preferring the first non-empty value.
+func ResolveBaseURL(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// EncodeURL encodes the given arguments into the URL, escaping
+// values as needed. Pointer arguments are dereferenced before processing.
+func EncodeURL(urlFormat string, args ...interface{}) string {
+ escapedArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ // Dereference the argument if it's a pointer
+ value := dereferenceArg(arg)
+ escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
+ }
+ return fmt.Sprintf(urlFormat, escapedArgs...)
+}
+
+// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
+// If the argument is not a pointer or is nil, it returns the argument as-is.
+func dereferenceArg(arg interface{}) interface{} {
+ if arg == nil {
+ return arg
+ }
+
+ v := reflect.ValueOf(arg)
+
+ // Keep dereferencing until we get to a non-pointer value or hit nil
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return nil
+ }
+ v = v.Elem()
+ }
+
+ return v.Interface()
+}
+
+// MergeHeaders merges the given headers together, where the right
+// takes precedence over the left.
+func MergeHeaders(left, right http.Header) http.Header {
+ for key, values := range right {
+ if len(values) > 1 {
+ left[key] = values
+ continue
+ }
+ if value := right.Get(key); value != "" {
+ left.Set(key, value)
+ }
+ }
+ return left
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query.go
new file mode 100644
index 000000000000..1cbaf7fe1c02
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query.go
@@ -0,0 +1,353 @@
+package internal
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+var (
+ bytesType = reflect.TypeOf([]byte{})
+ queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ uuidType = reflect.TypeOf(uuid.UUID{})
+)
+
+// QueryEncoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type QueryEncoder interface {
+ EncodeQueryValues(key string, v *url.Values) error
+}
+
+// prepareValue handles common validation and unwrapping logic for both functions
+func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return reflect.Value{}, values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return reflect.Value{}, values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ if err != nil {
+ return reflect.Value{}, nil, err
+ }
+
+ return val, values, nil
+}
+
+// QueryValues encodes url.Values from request objects.
+//
+// Note: This type is inspired by Google's query encoding library, but
+// supports far less customization and is tailored to fit this SDK's use case.
+//
+// Ref: https://github.com/google/go-querystring
+func QueryValues(v interface{}) (url.Values, error) {
+ _, values, err := prepareValue(v)
+ return values, err
+}
+
+// QueryValuesWithDefaults encodes url.Values from request objects
+// and default values, merging the defaults into the request.
+// It's expected that the values of defaults are wire names.
+func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
+ val, values, err := prepareValue(v)
+ if err != nil {
+ return values, err
+ }
+ if !val.IsValid() {
+ return values, nil
+ }
+
+ // apply defaults to zero-value fields directly on the original struct
+ valType := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Field(i)
+ fieldType := valType.Field(i)
+ fieldName := fieldType.Name
+
+ if fieldType.PkgPath != "" && !fieldType.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ // check if field is zero value and we have a default for it
+ if field.CanSet() && field.IsZero() {
+ tag := fieldType.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+ wireName, _ := parseTag(tag)
+ if wireName == "" {
+ wireName = fieldName
+ }
+ if defaultVal, exists := defaults[wireName]; exists {
+ values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
+ }
+ }
+ }
+
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+
+ name, opts := parseTag(tag)
+ if name == "" {
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(queryEncoderType) {
+ // If sv is a nil pointer and the custom encoder is defined on a non-pointer
+ // method receiver, set sv to the zero value of the underlying type
+ if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(QueryEncoder)
+ if err := m.EncodeQueryValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ // Recursively dereference pointers, but stop at nil pointers.
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts, sf))
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ if sv.Len() == 0 {
+ // Skip if slice or array is empty.
+ continue
+ }
+ for i := 0; i < sv.Len(); i++ {
+ value := sv.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), name); err != nil {
+ return err
+ }
+ } else {
+ values.Add(name, valueString(value, opts, sf))
+ }
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Map {
+ if err := reflectMap(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ if err := reflectValue(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts, sf))
+ }
+
+ return nil
+}
+
+// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
+func reflectMap(values url.Values, val reflect.Value, scope string) error {
+ if val.IsNil() {
+ return nil
+ }
+
+ iter := val.MapRange()
+ for iter.Next() {
+ k := iter.Key()
+ v := iter.Value()
+
+ key := fmt.Sprint(k.Interface())
+ paramName := scope + "[" + key + "]"
+
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ break
+ }
+ v = v.Elem()
+ }
+
+ for v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Map {
+ if err := reflectMap(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Struct {
+ if err := reflectValue(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ if v.Len() == 0 {
+ continue
+ }
+ for i := 0; i < v.Len(); i++ {
+ value := v.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), paramName); err != nil {
+ return err
+ }
+ } else {
+ values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
+ }
+ }
+ continue
+ }
+
+ values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if format := sf.Tag.Get("format"); format == "date" {
+ return t.Format("2006-01-02")
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ if v.Type() == uuidType {
+ u := v.Interface().(uuid.UUID)
+ return u.String()
+ }
+
+ if v.Type() == bytesType {
+ b := v.Interface().([]byte)
+ return base64.StdEncoding.EncodeToString(b)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ type zeroable interface {
+ IsZero() bool
+ }
+
+ if !v.IsZero() {
+ if z, ok := v.Interface().(zeroable); ok {
+ return z.IsZero()
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
+ return false
+ }
+
+ return false
+}
+
+// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
+func isStructPointer(v reflect.Value) bool {
+ return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query_test.go
new file mode 100644
index 000000000000..2c28cb8acf68
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/query_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryValues(t *testing.T) {
+ t.Run("empty optional", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+
+ t.Run("empty required", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Equal(t, "required=", values.Encode())
+ })
+
+ t.Run("allow multiple", func(t *testing.T) {
+ type example struct {
+ Values []string `json:"values" url:"values"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Values: []string{"foo", "bar", "baz"},
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
+ })
+
+ t.Run("nested object", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ nestedValue := "nestedValue"
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ Nested: &nested{
+ Value: &nestedValue,
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
+ })
+
+ t.Run("url unspecified", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("url ignored", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound" url:"-"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("datetime", func(t *testing.T) {
+ type example struct {
+ DateTime time.Time `json:"dateTime" url:"dateTime"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
+ })
+
+ t.Run("date", func(t *testing.T) {
+ type example struct {
+ Date time.Time `json:"date" url:"date" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "date=1994-03-16", values.Encode())
+ })
+
+ t.Run("optional time", func(t *testing.T) {
+ type example struct {
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
+ type enum string
+
+ type example struct {
+ Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("object array", func(t *testing.T) {
+ type object struct {
+ Key string `json:"key" url:"key"`
+ Value string `json:"value" url:"value"`
+ }
+ type example struct {
+ Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Objects: []*object{
+ {
+ Key: "hello",
+ Value: "world",
+ },
+ {
+ Key: "foo",
+ Value: "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
+ })
+
+ t.Run("map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "foo": "bar",
+ "baz": "qux",
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map array", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": []string{
+ "one",
+ "two",
+ "three",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
+ })
+}
+
+func TestQueryValuesWithDefaults(t *testing.T) {
+ t.Run("apply defaults to zero values", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ })
+
+ t.Run("preserve non-zero values over defaults", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Name: "actual-name",
+ Age: 30,
+ // Enabled remains false (zero value), should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
+ })
+
+ t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "nonexistent": "should-be-ignored",
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&name=default-name", values.Encode())
+ })
+
+ t.Run("type conversion for compatible defaults", func(t *testing.T) {
+ type example struct {
+ Count int64 `json:"count" url:"count"`
+ Rate float64 `json:"rate" url:"rate"`
+ Message string `json:"message" url:"message"`
+ }
+
+ defaults := map[string]interface{}{
+ "count": int(100), // int -> int64 conversion
+ "rate": float32(2.5), // float32 -> float64 conversion
+ "message": "hello", // string -> string (no conversion needed)
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
+ })
+
+ t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
+ Count int `json:"count,omitempty" url:"count,omitempty"`
+ }
+
+ defaultOptional := "default-optional"
+ defaults := map[string]interface{}{
+ "required": "default-required",
+ "optional": &defaultOptional, // pointer type
+ "count": 42,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Required: "custom-required", // should override default
+ // Optional is nil, should get default
+ // Count is 0, should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
+ })
+
+ t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
+ type example struct {
+ Name *string `json:"name" url:"name"`
+ Age *int `json:"age" url:"age"`
+ Enabled *bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ // first, test that a properly empty request is overridden:
+ {
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ }
+
+ // second, test that a request that contains zeros is not overridden:
+ var (
+ name = ""
+ age = 0
+ enabled = false
+ )
+ values, err := QueryValuesWithDefaults(&example{
+ Name: &name, // explicit empty string should override default
+ Age: &age, // explicit zero should override default
+ Enabled: &enabled, // explicit false should override default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
+ })
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier.go
new file mode 100644
index 000000000000..4efae1b4c286
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier.go
@@ -0,0 +1,230 @@
+package internal
+
+import (
+ "crypto/rand"
+ "math/big"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+const (
+ defaultRetryAttempts = 2
+ minRetryDelay = 1000 * time.Millisecond
+ maxRetryDelay = 60000 * time.Millisecond
+)
+
+// RetryOption adapts the behavior the *Retrier.
+type RetryOption func(*retryOptions)
+
+// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
+type RetryFunc func(*http.Request) (*http.Response, error)
+
+// WithMaxAttempts configures the maximum number of attempts
+// of the *Retrier.
+func WithMaxAttempts(attempts uint) RetryOption {
+ return func(opts *retryOptions) {
+ opts.attempts = attempts
+ }
+}
+
+// Retrier retries failed requests a configurable number of times with an
+// exponential back-off between each retry.
+type Retrier struct {
+ attempts uint
+}
+
+// NewRetrier constructs a new *Retrier with the given options, if any.
+func NewRetrier(opts ...RetryOption) *Retrier {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ attempts := uint(defaultRetryAttempts)
+ if options.attempts > 0 {
+ attempts = options.attempts
+ }
+ return &Retrier{
+ attempts: attempts,
+ }
+}
+
+// Run issues the request and, upon failure, retries the request if possible.
+//
+// The request will be retried as long as the request is deemed retryable and the
+// number of retry attempts has not grown larger than the configured retry limit.
+func (r *Retrier) Run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ opts ...RetryOption,
+) (*http.Response, error) {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ maxRetryAttempts := r.attempts
+ if options.attempts > 0 {
+ maxRetryAttempts = options.attempts
+ }
+ var (
+ retryAttempt uint
+ previousError error
+ )
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt,
+ previousError,
+ )
+}
+
+func (r *Retrier) run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ maxRetryAttempts uint,
+ retryAttempt uint,
+ previousError error,
+) (*http.Response, error) {
+ if retryAttempt >= maxRetryAttempts {
+ return nil, previousError
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := request.Context().Err(); err != nil {
+ return nil, err
+ }
+
+ response, err := fn(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.shouldRetry(response) {
+ defer response.Body.Close()
+
+ delay, err := r.retryDelay(response, retryAttempt)
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(delay)
+
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt+1,
+ decodeError(response, errorDecoder),
+ )
+ }
+
+ return response, nil
+}
+
+// shouldRetry returns true if the request should be retried based on the given
+// response status code.
+func (r *Retrier) shouldRetry(response *http.Response) bool {
+ return response.StatusCode == http.StatusTooManyRequests ||
+ response.StatusCode == http.StatusRequestTimeout ||
+ response.StatusCode >= http.StatusInternalServerError
+}
+
+// retryDelay calculates the delay time based on response headers,
+// falling back to exponential backoff if no headers are present.
+func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
+ // Check for Retry-After header first (RFC 7231), applying no jitter
+ if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
+ // Parse as number of seconds...
+ if seconds, err := strconv.Atoi(retryAfter); err == nil {
+ delay := time.Duration(seconds) * time.Second
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+
+ // ...or as an HTTP date; both are valid
+ if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
+ delay := time.Until(retryTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+ }
+
+ // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
+ if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
+ if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
+ // Assume Unix timestamp in seconds
+ resetTime := time.Unix(resetTimestamp, 0)
+ delay := time.Until(resetTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return r.addPositiveJitter(delay)
+ }
+ }
+ }
+
+ // Fall back to exponential backoff
+ return r.exponentialBackoff(retryAttempt)
+}
+
+// exponentialBackoff calculates the delay time based on the retry attempt
+// and applies symmetric jitter (±10% around the delay).
+func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
+ if retryAttempt > 63 { // 2^63+ would overflow uint64
+ retryAttempt = 63
+ }
+
+ delay := minRetryDelay << retryAttempt
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+
+ return r.addSymmetricJitter(delay)
+}
+
+// addJitterWithRange applies jitter to the given delay.
+// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
+func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
+ jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
+ jitter, err := rand.Int(rand.Reader, jitterRange)
+ if err != nil {
+ return 0, err
+ }
+
+ jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
+ if jitteredDelay < minRetryDelay {
+ jitteredDelay = minRetryDelay
+ }
+ if jitteredDelay > maxRetryDelay {
+ jitteredDelay = maxRetryDelay
+ }
+ return jitteredDelay, nil
+}
+
+// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
+func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 100, 120)
+}
+
+// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
+func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 90, 110)
+}
+
+type retryOptions struct {
+ attempts uint
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier_test.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier_test.go
new file mode 100644
index 000000000000..da09c37a0c41
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/retrier_test.go
@@ -0,0 +1,300 @@
+package internal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/fern-api/sse-examples-go/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type RetryTestCase struct {
+ description string
+
+ giveAttempts uint
+ giveStatusCodes []int
+ giveResponse *InternalTestResponse
+
+ wantResponse *InternalTestResponse
+ wantError *core.APIError
+}
+
+func TestRetrier(t *testing.T) {
+ tests := []*RetryTestCase{
+ {
+ description: "retry request succeeds after multiple failures",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ giveResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ },
+ {
+ description: "retry request fails if MaxAttempts is exceeded",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusOK,
+ },
+ wantError: &core.APIError{
+ StatusCode: http.StatusRequestTimeout,
+ },
+ },
+ {
+ description: "retry durations increase exponentially and stay within the min and max delay values",
+ giveAttempts: 4,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ },
+ {
+ description: "retry does not occur on status code 404",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
+ wantError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ },
+ {
+ description: "retries occur on status code 429",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 408",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 500",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.description, func(t *testing.T) {
+ var (
+ test = tc
+ server = newTestRetryServer(t, test)
+ client = server.Client()
+ )
+
+ t.Parallel()
+
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: test.giveAttempts,
+ ResponseIsOptional: true,
+ },
+ )
+
+ if test.wantError != nil {
+ require.IsType(t, err, &core.APIError{})
+ expectedErrorCode := test.wantError.StatusCode
+ actualErrorCode := err.(*core.APIError).StatusCode
+ assert.Equal(t, expectedErrorCode, actualErrorCode)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+// newTestRetryServer returns a new *httptest.Server configured with the
+// given test parameters, suitable for testing retries.
+func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
+ var index int
+ timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
+
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if index > 0 && index < len(expectedRetryDurations) {
+ // Ensure that the duration between retries increases exponentially,
+ // and that it is within the minimum and maximum retry delay values.
+ actualDuration := timestamps[index].Sub(timestamps[index-1])
+ expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
+ expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
+ assert.True(
+ t,
+ actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
+ "expected duration to be in range [%v, %v], got %v",
+ expectedDurationMin,
+ expectedDurationMax,
+ actualDuration,
+ )
+ assert.LessOrEqual(
+ t,
+ actualDuration,
+ maxRetryDelay,
+ "expected duration to be less than the maxRetryDelay (%v), got %v",
+ maxRetryDelay,
+ actualDuration,
+ )
+ assert.GreaterOrEqual(
+ t,
+ actualDuration,
+ minRetryDelay,
+ "expected duration to be greater than the minRetryDelay (%v), got %v",
+ minRetryDelay,
+ actualDuration,
+ )
+ }
+
+ request := new(InternalTestRequest)
+ bytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+ require.LessOrEqual(t, index, len(tc.giveStatusCodes))
+
+ statusCode := tc.giveStatusCodes[index]
+
+ w.WriteHeader(statusCode)
+
+ if tc.giveResponse != nil && statusCode == http.StatusOK {
+ bytes, err = json.Marshal(tc.giveResponse)
+ require.NoError(t, err)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ }
+
+ index++
+ },
+ ),
+ )
+}
+
+// expectedRetryDurations holds an array of calculated retry durations,
+// where the index of the array should correspond to the retry attempt.
+//
+// Values are calculated based off of `minRetryDelay * 2^i`.
+var expectedRetryDurations = []time.Duration{
+ 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
+ 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
+ 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
+ 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
+}
+
+func TestRetryDelayTiming(t *testing.T) {
+ tests := []struct {
+ name string
+ headerName string
+ headerValueFunc func() string
+ expectedMinMs int64
+ expectedMaxMs int64
+ }{
+ {
+ name: "retry-after with seconds value",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return "1"
+ },
+ expectedMinMs: 500,
+ expectedMaxMs: 1500,
+ },
+ {
+ name: "retry-after with HTTP date",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return time.Now().Add(3 * time.Second).Format(time.RFC1123)
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ {
+ name: "x-ratelimit-reset with future timestamp",
+ headerName: "x-ratelimit-reset",
+ headerValueFunc: func() string {
+ return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ var timestamps []time.Time
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if len(timestamps) == 1 {
+ // First request - return retryable error with header
+ w.Header().Set(tt.headerName, tt.headerValueFunc())
+ w.WriteHeader(http.StatusTooManyRequests)
+ } else {
+ // Second request - return success
+ w.WriteHeader(http.StatusOK)
+ response := &InternalTestResponse{Id: "success"}
+ bytes, _ := json.Marshal(response)
+ w.Write(bytes)
+ }
+ }))
+ defer server.Close()
+
+ caller := NewCaller(&CallerParams{
+ Client: server.Client(),
+ })
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: 2,
+ ResponseIsOptional: true,
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, timestamps, 2, "Expected exactly 2 requests")
+
+ actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
+
+ assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
+ "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
+ assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
+ "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
+ })
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/streamer.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/streamer.go
new file mode 100644
index 000000000000..c20475356867
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/streamer.go
@@ -0,0 +1,118 @@
+package internal
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/fern-api/sse-examples-go/core"
+)
+
+const (
+ // DefaultDataPrefix is the default prefix used for SSE streaming.
+ DefaultSSEDataPrefix = "data: "
+
+ // DefaultTerminator is the default terminator used for SSE streaming.
+ DefaultSSETerminator = "[DONE]"
+)
+
+// Streamer calls APIs and streams responses using a *Stream.
+type Streamer[T any] struct {
+ client HTTPClient
+ retrier *Retrier
+}
+
+// NewStreamer returns a new *Streamer backed by the given caller's HTTP client.
+func NewStreamer[T any](caller *Caller) *Streamer[T] {
+ return &Streamer[T]{
+ client: caller.client,
+ retrier: caller.retrier,
+ }
+}
+
+// StreamParams represents the parameters used to issue an API streaming call.
+type StreamParams struct {
+ URL string
+ Method string
+ Prefix string
+ Delimiter string
+ Terminator string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client HTTPClient
+ Request interface{}
+ ErrorDecoder ErrorDecoder
+ Format core.StreamFormat
+}
+
+// Stream issues an API streaming call according to the given stream parameters.
+func (s *Streamer[T]) Stream(ctx context.Context, params *StreamParams) (*core.Stream[T], error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := s.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := s.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ defer resp.Body.Close()
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ defer resp.Body.Close()
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ var opts []core.StreamOption
+ if params.Delimiter != "" {
+ opts = append(opts, core.WithDelimiter(params.Delimiter))
+ }
+ if params.Prefix != "" {
+ opts = append(opts, core.WithPrefix(params.Prefix))
+ }
+ if params.Terminator != "" {
+ opts = append(opts, core.WithTerminator(params.Terminator))
+ }
+ if params.Format != core.StreamFormatEmpty {
+ opts = append(opts, core.WithFormat(params.Format))
+ }
+
+ return core.NewStream[T](resp, opts...), nil
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/stringer.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/stringer.go
new file mode 100644
index 000000000000..312801851e0e
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value interface{}) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/time.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/option/request_option.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/option/request_option.go
new file mode 100644
index 000000000000..b4477721ee56
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/option/request_option.go
@@ -0,0 +1,64 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/fern-api/sse-examples-go/core"
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of an individual request.
+type RequestOption = core.RequestOption
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return &core.BaseURLOption{
+ BaseURL: baseURL,
+ }
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return &core.HTTPClientOption{
+ HTTPClient: httpClient,
+ }
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return &core.HTTPHeaderOption{
+ // Clone the headers so they can't be modified after the option call.
+ HTTPHeader: httpHeader.Clone(),
+ }
+}
+
+// WithBodyProperties adds the given body properties to the request.
+func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
+ copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
+ for key, value := range bodyProperties {
+ copiedBodyProperties[key] = value
+ }
+ return &core.BodyPropertiesOption{
+ BodyProperties: copiedBodyProperties,
+ }
+}
+
+// WithQueryParameters adds the given query parameters to the request.
+func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
+ copiedQueryParameters := make(url.Values, len(queryParameters))
+ for key, values := range queryParameters {
+ copiedQueryParameters[key] = values
+ }
+ return &core.QueryParametersOption{
+ QueryParameters: copiedQueryParameters,
+ }
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return &core.MaxAttemptsOption{
+ MaxAttempts: attempts,
+ }
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/pointer.go b/seed/go-sdk/server-sent-event-examples/with-wire-tests/pointer.go
new file mode 100644
index 000000000000..143c4ca885ce
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/pointer.go
@@ -0,0 +1,132 @@
+package sse
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// Bool returns a pointer to the given bool value.
+func Bool(b bool) *bool {
+ return &b
+}
+
+// Byte returns a pointer to the given byte value.
+func Byte(b byte) *byte {
+ return &b
+}
+
+// Complex64 returns a pointer to the given complex64 value.
+func Complex64(c complex64) *complex64 {
+ return &c
+}
+
+// Complex128 returns a pointer to the given complex128 value.
+func Complex128(c complex128) *complex128 {
+ return &c
+}
+
+// Float32 returns a pointer to the given float32 value.
+func Float32(f float32) *float32 {
+ return &f
+}
+
+// Float64 returns a pointer to the given float64 value.
+func Float64(f float64) *float64 {
+ return &f
+}
+
+// Int returns a pointer to the given int value.
+func Int(i int) *int {
+ return &i
+}
+
+// Int8 returns a pointer to the given int8 value.
+func Int8(i int8) *int8 {
+ return &i
+}
+
+// Int16 returns a pointer to the given int16 value.
+func Int16(i int16) *int16 {
+ return &i
+}
+
+// Int32 returns a pointer to the given int32 value.
+func Int32(i int32) *int32 {
+ return &i
+}
+
+// Int64 returns a pointer to the given int64 value.
+func Int64(i int64) *int64 {
+ return &i
+}
+
+// Rune returns a pointer to the given rune value.
+func Rune(r rune) *rune {
+ return &r
+}
+
+// String returns a pointer to the given string value.
+func String(s string) *string {
+ return &s
+}
+
+// Uint returns a pointer to the given uint value.
+func Uint(u uint) *uint {
+ return &u
+}
+
+// Uint8 returns a pointer to the given uint8 value.
+func Uint8(u uint8) *uint8 {
+ return &u
+}
+
+// Uint16 returns a pointer to the given uint16 value.
+func Uint16(u uint16) *uint16 {
+ return &u
+}
+
+// Uint32 returns a pointer to the given uint32 value.
+func Uint32(u uint32) *uint32 {
+ return &u
+}
+
+// Uint64 returns a pointer to the given uint64 value.
+func Uint64(u uint64) *uint64 {
+ return &u
+}
+
+// Uintptr returns a pointer to the given uintptr value.
+func Uintptr(u uintptr) *uintptr {
+ return &u
+}
+
+// UUID returns a pointer to the given uuid.UUID value.
+func UUID(u uuid.UUID) *uuid.UUID {
+ return &u
+}
+
+// Time returns a pointer to the given time.Time value.
+func Time(t time.Time) *time.Time {
+ return &t
+}
+
+// MustParseDate attempts to parse the given string as a
+// date time.Time, and panics upon failure.
+func MustParseDate(date string) time.Time {
+ t, err := time.Parse("2006-01-02", date)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
+
+// MustParseDateTime attempts to parse the given string as a
+// datetime time.Time, and panics upon failure.
+func MustParseDateTime(datetime string) time.Time {
+ t, err := time.Parse(time.RFC3339, datetime)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/reference.md b/seed/go-sdk/server-sent-event-examples/with-wire-tests/reference.md
new file mode 100644
index 000000000000..20dc92b19010
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/reference.md
@@ -0,0 +1,48 @@
+# Reference
+## Completions
+client.Completions.Stream(request) -> sse.StreamedCompletion
+
+-
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```go
+request := &sse.StreamCompletionRequest{
+ Query: "foo",
+ }
+client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**query:** `string`
+
+
+
+
+
+
+
+
+
+
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/snippet.json b/seed/go-sdk/server-sent-event-examples/with-wire-tests/snippet.json
new file mode 100644
index 000000000000..5a3b2f48fad7
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/snippet.json
@@ -0,0 +1,15 @@
+{
+ "endpoints": [
+ {
+ "id": {
+ "path": "/stream",
+ "method": "POST",
+ "identifier_override": "endpoint_completions.stream"
+ },
+ "snippet": {
+ "type": "go",
+ "client": "import (\n\tcontext \"context\"\n\tsseexamplesgo \"github.com/fern-api/sse-examples-go\"\n\tsseexamplesgoclient \"github.com/fern-api/sse-examples-go/client\"\n)\n\nclient := sseexamplesgoclient.NewClient()\nresponse, err := client.Completions.Stream(\n\tcontext.TODO(),\n\t\u0026sseexamplesgo.StreamCompletionRequest{\n\t\tQuery: \"foo\",\n\t},\n)\n"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/docker-compose.test.yml b/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/docker-compose.test.yml
new file mode 100644
index 000000000000..b65fc8855e0a
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/docker-compose.test.yml
@@ -0,0 +1,8 @@
+services:
+ wiremock:
+ image: wiremock/wiremock:3.9.1
+ ports:
+ - "8080:8080"
+ volumes:
+ - ./wiremock-mappings.json:/home/wiremock/mappings/wiremock-mappings.json
+ command: ["--global-response-templating", "--verbose"]
diff --git a/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/wiremock-mappings.json b/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/wiremock-mappings.json
new file mode 100644
index 000000000000..55fd00daf635
--- /dev/null
+++ b/seed/go-sdk/server-sent-event-examples/with-wire-tests/wiremock/wiremock-mappings.json
@@ -0,0 +1 @@
+{"mappings":[{"id":"07afdcff-a307-475a-b81a-89ebf2b474b2","name":"stream - default","request":{"urlPathTemplate":"/stream","method":"POST"},"response":{"status":200,"body":"event: discriminant-1\ndata: {\"delta\":\"foo\",\"tokens\":1}\n\nevent: discriminant-2\ndata: {\"delta\":\"bar\",\"tokens\":2}\n","headers":{"Content-Type":"text/event-stream"}},"uuid":"07afdcff-a307-475a-b81a-89ebf2b474b2","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}}],"meta":{"total":1}}
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/.fern/metadata.json b/seed/go-sdk/server-sent-events/with-wire-tests/.fern/metadata.json
new file mode 100644
index 000000000000..274156a8e052
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/.fern/metadata.json
@@ -0,0 +1,12 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-sdk",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "enableWireTests": true,
+ "packageName": "sse",
+ "module": {
+ "path": "github.com/fern-api/sse-go"
+ }
+ }
+}
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/.github/workflows/ci.yml b/seed/go-sdk/server-sent-events/with-wire-tests/.github/workflows/ci.yml
new file mode 100644
index 000000000000..56310d69624b
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on: [push]
+
+jobs:
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Compile
+ run: go build ./...
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Setup wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
+
+ - name: Test
+ run: go test ./...
+
+ - name: Teardown wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/README.md b/seed/go-sdk/server-sent-events/with-wire-tests/README.md
new file mode 100644
index 000000000000..d1ac153a0292
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/README.md
@@ -0,0 +1,193 @@
+# Seed Go Library
+
+[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FGo)
+
+The Seed Go library provides convenient access to the Seed APIs from Go.
+
+## Table of Contents
+
+- [Reference](#reference)
+- [Usage](#usage)
+- [Environments](#environments)
+- [Errors](#errors)
+- [Request Options](#request-options)
+- [Advanced](#advanced)
+ - [Response Headers](#response-headers)
+ - [Retries](#retries)
+ - [Timeouts](#timeouts)
+ - [Explicit Null](#explicit-null)
+- [Contributing](#contributing)
+
+## Reference
+
+A full reference for this library is available [here](./reference.md).
+
+## Usage
+
+Instantiate and use the client with the following:
+
+```go
+package example
+
+import (
+ client "github.com/fern-api/sse-go/client"
+ sse "github.com/fern-api/sse-go"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient()
+ request := &sse.StreamCompletionRequest{
+ Query: "query",
+ }
+ client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+## Environments
+
+You can choose between different environments by using the `option.WithBaseURL` option. You can configure any arbitrary base
+URL, which is particularly useful in test environments.
+
+```go
+client := client.NewClient(
+ option.WithBaseURL("https://example.com"),
+)
+```
+
+## Errors
+
+Structured error types are returned from API calls that return non-success status codes. These errors are compatible
+with the `errors.Is` and `errors.As` APIs, so you can access the error like so:
+
+```go
+response, err := client.Completions.Stream(...)
+if err != nil {
+ var apiError *core.APIError
+ if errors.As(err, apiError) {
+ // Do something with the API error ...
+ }
+ return err
+}
+```
+
+## Request Options
+
+A variety of request options are included to adapt the behavior of the library, which includes configuring
+authorization tokens, or providing your own instrumented `*http.Client`.
+
+These request options can either be
+specified on the client so that they're applied on every request, or for an individual request, like so:
+
+> Providing your own `*http.Client` is recommended. Otherwise, the `http.DefaultClient` will be used,
+> and your client will wait indefinitely for a response (unless the per-request, context-based timeout
+> is used).
+
+```go
+// Specify default options applied on every request.
+client := client.NewClient(
+ option.WithToken(""),
+ option.WithHTTPClient(
+ &http.Client{
+ Timeout: 5 * time.Second,
+ },
+ ),
+)
+
+// Specify options for an individual request.
+response, err := client.Completions.Stream(
+ ...,
+ option.WithToken(""),
+)
+```
+
+## Advanced
+
+### Response Headers
+
+You can access the raw HTTP response data by using the `WithRawResponse` field on the client. This is useful
+when you need to examine the response headers received from the API call. (When the endpoint is paginated,
+the raw HTTP response data will be included automatically in the Page response object.)
+
+```go
+response, err := client.Completions.WithRawResponse.Stream(...)
+if err != nil {
+ return err
+}
+fmt.Printf("Got response headers: %v", response.Header)
+fmt.Printf("Got status code: %d", response.StatusCode)
+```
+
+### Retries
+
+The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long
+as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
+retry limit (default: 2).
+
+A request is deemed retryable when any of the following HTTP status codes is returned:
+
+- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
+- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
+- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
+
+If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly
+over the default exponential backoff.
+
+Use the `option.WithMaxAttempts` option to configure this behavior for the entire client or an individual request:
+
+```go
+client := client.NewClient(
+ option.WithMaxAttempts(1),
+)
+
+response, err := client.Completions.Stream(
+ ...,
+ option.WithMaxAttempts(1),
+)
+```
+
+### Timeouts
+
+Setting a timeout for each individual request is as simple as using the standard context library. Setting a one second timeout for an individual API call looks like the following:
+
+```go
+ctx, cancel := context.WithTimeout(ctx, time.Second)
+defer cancel()
+
+response, err := client.Completions.Stream(ctx, ...)
+```
+
+### Explicit Null
+
+If you want to send the explicit `null` JSON value through an optional parameter, you can use the setters\
+that come with every object. Calling a setter method for a property will flip a bit in the `explicitFields`
+bitfield for that setter's object; during serialization, any property with a flipped bit will have its
+omittable status stripped, so zero or `nil` values will be sent explicitly rather than omitted altogether:
+
+```go
+type ExampleRequest struct {
+ // An optional string parameter.
+ Name *string `json:"name,omitempty" url:"-"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+request := &ExampleRequest{}
+request.SetName(nil)
+
+response, err := client.Completions.Stream(ctx, request, ...)
+```
+
+## Contributing
+
+While we value open-source contributions to this SDK, this library is generated programmatically.
+Additions made directly to this library would have to be moved over to our generation code,
+otherwise they would be overwritten upon the next generated release. Feel free to open a PR as
+a proof of concept, but know that we will not be able to merge it as-is. We suggest opening
+an issue first to discuss with us!
+
+On the other hand, contributions to the README are always very welcome!
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/client/client.go b/seed/go-sdk/server-sent-events/with-wire-tests/client/client.go
new file mode 100644
index 000000000000..659d3ebc2ca3
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/client/client.go
@@ -0,0 +1,33 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ completions "github.com/fern-api/sse-go/completions"
+ core "github.com/fern-api/sse-go/core"
+ internal "github.com/fern-api/sse-go/internal"
+ option "github.com/fern-api/sse-go/option"
+)
+
+type Client struct {
+ Completions *completions.Client
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(opts ...option.RequestOption) *Client {
+ options := core.NewRequestOptions(opts...)
+ return &Client{
+ Completions: completions.NewClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/client/client_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/client/client_test.go
new file mode 100644
index 000000000000..333fa5a29f06
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/client/client_test.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ option "github.com/fern-api/sse-go/option"
+ assert "github.com/stretchr/testify/assert"
+ http "net/http"
+ testing "testing"
+ time "time"
+)
+
+func TestNewClient(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ c := NewClient()
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("base url", func(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("test.co"),
+ )
+ assert.Equal(t, "test.co", c.baseURL)
+ })
+
+ t.Run("http client", func(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ c := NewClient(
+ option.WithHTTPClient(httpClient),
+ )
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("http header", func(t *testing.T) {
+ header := make(http.Header)
+ header.Set("X-API-Tenancy", "test")
+ c := NewClient(
+ option.WithHTTPHeader(header),
+ )
+ assert.Empty(t, c.baseURL)
+ assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
+ })
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/completions.go b/seed/go-sdk/server-sent-events/with-wire-tests/completions.go
new file mode 100644
index 000000000000..449f68a5afda
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/completions.go
@@ -0,0 +1,129 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package sse
+
+import (
+ json "encoding/json"
+ fmt "fmt"
+ internal "github.com/fern-api/sse-go/internal"
+ big "math/big"
+)
+
+var (
+ streamCompletionRequestFieldQuery = big.NewInt(1 << 0)
+)
+
+type StreamCompletionRequest struct {
+ Query string `json:"query" url:"-"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+}
+
+func (s *StreamCompletionRequest) require(field *big.Int) {
+ if s.explicitFields == nil {
+ s.explicitFields = big.NewInt(0)
+ }
+ s.explicitFields.Or(s.explicitFields, field)
+}
+
+// SetQuery sets the Query field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamCompletionRequest) SetQuery(query string) {
+ s.Query = query
+ s.require(streamCompletionRequestFieldQuery)
+}
+
+var (
+ streamedCompletionFieldDelta = big.NewInt(1 << 0)
+ streamedCompletionFieldTokens = big.NewInt(1 << 1)
+)
+
+type StreamedCompletion struct {
+ Delta string `json:"delta" url:"delta"`
+ Tokens *int `json:"tokens,omitempty" url:"tokens,omitempty"`
+
+ // Private bitmask of fields set to an explicit value and therefore not to be omitted
+ explicitFields *big.Int `json:"-" url:"-"`
+
+ extraProperties map[string]interface{}
+ rawJSON json.RawMessage
+}
+
+func (s *StreamedCompletion) GetDelta() string {
+ if s == nil {
+ return ""
+ }
+ return s.Delta
+}
+
+func (s *StreamedCompletion) GetTokens() *int {
+ if s == nil {
+ return nil
+ }
+ return s.Tokens
+}
+
+func (s *StreamedCompletion) GetExtraProperties() map[string]interface{} {
+ return s.extraProperties
+}
+
+func (s *StreamedCompletion) require(field *big.Int) {
+ if s.explicitFields == nil {
+ s.explicitFields = big.NewInt(0)
+ }
+ s.explicitFields.Or(s.explicitFields, field)
+}
+
+// SetDelta sets the Delta field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamedCompletion) SetDelta(delta string) {
+ s.Delta = delta
+ s.require(streamedCompletionFieldDelta)
+}
+
+// SetTokens sets the Tokens field and marks it as non-optional;
+// this prevents an empty or null value for this field from being omitted during serialization.
+func (s *StreamedCompletion) SetTokens(tokens *int) {
+ s.Tokens = tokens
+ s.require(streamedCompletionFieldTokens)
+}
+
+func (s *StreamedCompletion) UnmarshalJSON(data []byte) error {
+ type unmarshaler StreamedCompletion
+ var value unmarshaler
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ *s = StreamedCompletion(value)
+ extraProperties, err := internal.ExtractExtraProperties(data, *s)
+ if err != nil {
+ return err
+ }
+ s.extraProperties = extraProperties
+ s.rawJSON = json.RawMessage(data)
+ return nil
+}
+
+func (s *StreamedCompletion) MarshalJSON() ([]byte, error) {
+ type embed StreamedCompletion
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+ explicitMarshaler := internal.HandleExplicitFields(marshaler, s.explicitFields)
+ return json.Marshal(explicitMarshaler)
+}
+
+func (s *StreamedCompletion) String() string {
+ if len(s.rawJSON) > 0 {
+ if value, err := internal.StringifyJSON(s.rawJSON); err == nil {
+ return value
+ }
+ }
+ if value, err := internal.StringifyJSON(s); err == nil {
+ return value
+ }
+ return fmt.Sprintf("%#v", s)
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/completions/client.go b/seed/go-sdk/server-sent-events/with-wire-tests/completions/client.go
new file mode 100644
index 000000000000..bcf8ee84837e
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/completions/client.go
@@ -0,0 +1,71 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions
+
+import (
+ context "context"
+ sse "github.com/fern-api/sse-go"
+ core "github.com/fern-api/sse-go/core"
+ internal "github.com/fern-api/sse-go/internal"
+ option "github.com/fern-api/sse-go/option"
+ http "net/http"
+)
+
+type Client struct {
+ WithRawResponse *RawClient
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(options *core.RequestOptions) *Client {
+ return &Client{
+ WithRawResponse: NewRawClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (c *Client) Stream(
+ ctx context.Context,
+ request *sse.StreamCompletionRequest,
+ opts ...option.RequestOption,
+) (*core.Stream[sse.StreamedCompletion], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ c.baseURL,
+ "",
+ )
+ endpointURL := baseURL + "/stream"
+ headers := internal.MergeHeaders(
+ c.options.ToHeader(),
+ options.ToHeader(),
+ )
+ headers.Add("Accept", "text/event-stream")
+ streamer := internal.NewStreamer[sse.StreamedCompletion](c.caller)
+ return streamer.Stream(
+ ctx,
+ &internal.StreamParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ Prefix: internal.DefaultSSEDataPrefix,
+ Terminator: "[[DONE]]",
+ Format: core.StreamFormatSSE,
+ Request: request,
+ ErrorDecoder: internal.NewErrorDecoder(sse.ErrorCodes),
+ },
+ )
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/completions/completions_test/completions_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/completions/completions_test/completions_test.go
new file mode 100644
index 000000000000..c51f67e0e827
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/completions/completions_test/completions_test.go
@@ -0,0 +1,84 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions_test
+
+import (
+ bytes "bytes"
+ context "context"
+ json "encoding/json"
+ sse "github.com/fern-api/sse-go"
+ client "github.com/fern-api/sse-go/client"
+ option "github.com/fern-api/sse-go/option"
+ require "github.com/stretchr/testify/require"
+ http "net/http"
+ testing "testing"
+)
+
+func ResetWireMockRequests(
+ t *testing.T,
+) {
+ WiremockAdminURL := "http://localhost:8080/__admin"
+ _, err := http.Post(WiremockAdminURL+"/requests/reset", "application/json", nil)
+ require.NoError(t, err)
+}
+
+func VerifyRequestCount(
+ t *testing.T,
+ method string,
+ urlPath string,
+ queryParams map[string]string,
+ expected int,
+) {
+ WiremockAdminURL := "http://localhost:8080/__admin"
+ var reqBody bytes.Buffer
+ reqBody.WriteString(`{"method":"`)
+ reqBody.WriteString(method)
+ reqBody.WriteString(`","urlPath":"`)
+ reqBody.WriteString(urlPath)
+ reqBody.WriteString(`"}`)
+ if len(queryParams) > 0 {
+ reqBody.WriteString(`,"queryParameters":{`)
+ first := true
+ for key, value := range queryParams {
+ if !first {
+ reqBody.WriteString(",")
+ }
+ reqBody.WriteString(`"`)
+ reqBody.WriteString(key)
+ reqBody.WriteString(`":{"equalTo":"`)
+ reqBody.WriteString(value)
+ reqBody.WriteString(`"}`)
+ first = false
+ }
+ reqBody.WriteString("}")
+ }
+ resp, err := http.Post(WiremockAdminURL+"/requests/find", "application/json", &reqBody)
+ require.NoError(t, err)
+ var result struct {
+ Requests []interface{} `json:"requests"`
+ }
+ json.NewDecoder(resp.Body).Decode(&result)
+ require.Equal(t, expected, len(result.Requests))
+}
+
+func TestCompletionsStreamWithWireMock(
+ t *testing.T,
+) {
+ ResetWireMockRequests(t)
+ WireMockBaseURL := "http://localhost:8080"
+ client := client.NewClient(
+ option.WithBaseURL(
+ WireMockBaseURL,
+ ),
+ )
+ request := &sse.StreamCompletionRequest{
+ Query: "query",
+ }
+ _, invocationErr := client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+
+ require.NoError(t, invocationErr, "Client method call should succeed")
+ VerifyRequestCount(t, "POST", "/stream", nil, 1)
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/completions/raw_client.go b/seed/go-sdk/server-sent-events/with-wire-tests/completions/raw_client.go
new file mode 100644
index 000000000000..6637f70cebb0
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/completions/raw_client.go
@@ -0,0 +1,27 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package completions
+
+import (
+ core "github.com/fern-api/sse-go/core"
+ internal "github.com/fern-api/sse-go/internal"
+)
+
+type RawClient struct {
+ baseURL string
+ caller *internal.Caller
+ options *core.RequestOptions
+}
+
+func NewRawClient(options *core.RequestOptions) *RawClient {
+ return &RawClient{
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/core/api_error.go b/seed/go-sdk/server-sent-events/with-wire-tests/core/api_error.go
new file mode 100644
index 000000000000..6168388541b4
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/core/api_error.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// APIError is a lightweight wrapper around the standard error
+// interface that preserves the status code from the RPC, if any.
+type APIError struct {
+ err error
+
+ StatusCode int `json:"-"`
+ Header http.Header `json:"-"`
+}
+
+// NewAPIError constructs a new API error.
+func NewAPIError(statusCode int, header http.Header, err error) *APIError {
+ return &APIError{
+ err: err,
+ Header: header,
+ StatusCode: statusCode,
+ }
+}
+
+// Unwrap returns the underlying error. This also makes the error compatible
+// with errors.As and errors.Is.
+func (a *APIError) Unwrap() error {
+ if a == nil {
+ return nil
+ }
+ return a.err
+}
+
+// Error returns the API error's message.
+func (a *APIError) Error() string {
+ if a == nil || (a.err == nil && a.StatusCode == 0) {
+ return ""
+ }
+ if a.err == nil {
+ return fmt.Sprintf("%d", a.StatusCode)
+ }
+ if a.StatusCode == 0 {
+ return a.err.Error()
+ }
+ return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/core/http.go b/seed/go-sdk/server-sent-events/with-wire-tests/core/http.go
new file mode 100644
index 000000000000..92c435692940
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/core/http.go
@@ -0,0 +1,15 @@
+package core
+
+import "net/http"
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Response is an HTTP response from an HTTP client.
+type Response[T any] struct {
+ StatusCode int
+ Header http.Header
+ Body T
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/core/request_option.go b/seed/go-sdk/server-sent-events/with-wire-tests/core/request_option.go
new file mode 100644
index 000000000000..d805af1dc219
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/core/request_option.go
@@ -0,0 +1,109 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of the client or an individual request.
+type RequestOption interface {
+ applyRequestOptions(*RequestOptions)
+}
+
+// RequestOptions defines all of the possible request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type RequestOptions struct {
+ BaseURL string
+ HTTPClient HTTPClient
+ HTTPHeader http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ MaxAttempts uint
+}
+
+// NewRequestOptions returns a new *RequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use RequestOption instead.
+func NewRequestOptions(opts ...RequestOption) *RequestOptions {
+ options := &RequestOptions{
+ HTTPHeader: make(http.Header),
+ BodyProperties: make(map[string]interface{}),
+ QueryParameters: make(url.Values),
+ }
+ for _, opt := range opts {
+ opt.applyRequestOptions(options)
+ }
+ return options
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request(s).
+func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() }
+
+func (r *RequestOptions) cloneHeader() http.Header {
+ headers := r.HTTPHeader.Clone()
+ headers.Set("X-Fern-Language", "Go")
+ headers.Set("X-Fern-SDK-Name", "github.com/fern-api/sse-go")
+ headers.Set("X-Fern-SDK-Version", "v0.0.1")
+ headers.Set("User-Agent", "github.com/server-sent-events/fern/0.0.1")
+ return headers
+}
+
+// BaseURLOption implements the RequestOption interface.
+type BaseURLOption struct {
+ BaseURL string
+}
+
+func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+// HTTPClientOption implements the RequestOption interface.
+type HTTPClientOption struct {
+ HTTPClient HTTPClient
+}
+
+func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+// HTTPHeaderOption implements the RequestOption interface.
+type HTTPHeaderOption struct {
+ HTTPHeader http.Header
+}
+
+func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+// BodyPropertiesOption implements the RequestOption interface.
+type BodyPropertiesOption struct {
+ BodyProperties map[string]interface{}
+}
+
+func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+// QueryParametersOption implements the RequestOption interface.
+type QueryParametersOption struct {
+ QueryParameters url.Values
+}
+
+func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+// MaxAttemptsOption implements the RequestOption interface.
+type MaxAttemptsOption struct {
+ MaxAttempts uint
+}
+
+func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/core/stream.go b/seed/go-sdk/server-sent-events/with-wire-tests/core/stream.go
new file mode 100644
index 000000000000..25c528e89516
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/core/stream.go
@@ -0,0 +1,368 @@
+package core
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "slices"
+ "strings"
+)
+
+type StreamFormat string
+
+const (
+ StreamFormatSSE StreamFormat = "sse"
+ StreamFormatEmpty StreamFormat = ""
+)
+
+const (
+ sseEventSeparator = "\n\n"
+ sseLineSeparator = "\n"
+)
+
+const (
+ defaultMaxBufSize = 64 * 1024 // 64KB
+)
+
+// Stream represents a stream of messages sent from a server.
+type Stream[T any] struct {
+ reader streamReader
+ closer io.Closer
+}
+
+// StreamOption adapts the behavior of the Stream.
+type StreamOption func(*streamOptions)
+
+// WithDelimiter overrides the delimiter for the Stream.
+//
+// By default, the Stream is newline-delimited.
+func WithDelimiter(delimiter string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.delimiter = delimiter
+ }
+}
+
+// WithPrefix overrides the prefix for the Stream.
+//
+// By default, the Stream doesn't have a prefix.
+func WithPrefix(prefix string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.prefix = prefix
+ }
+}
+
+// WithTerminator overrides the terminator for the Stream.
+//
+// By default, the Stream terminates on EOF.
+func WithTerminator(terminator string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.terminator = terminator
+ }
+}
+
+// WithFormat overrides the isSSE flag for the Stream.
+//
+// By default, the Stream is not SSE.
+func WithFormat(format StreamFormat) StreamOption {
+ return func(opts *streamOptions) {
+ opts.format = format
+ }
+}
+
+// NewStream constructs a new Stream from the given *http.Response.
+func NewStream[T any](response *http.Response, opts ...StreamOption) *Stream[T] {
+ options := new(streamOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ return &Stream[T]{
+ reader: newStreamReader(response.Body, options),
+ closer: response.Body,
+ }
+}
+
+// Recv reads a message from the stream, returning io.EOF when
+// all the messages have been read.
+func (s Stream[T]) Recv() (T, error) {
+ var value T
+ bytes, err := s.reader.ReadFromStream()
+ if err != nil {
+ return value, err
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return value, err
+ }
+ return value, nil
+}
+
+// Close closes the Stream.
+func (s Stream[T]) Close() error {
+ return s.closer.Close()
+}
+
+// streamReader reads data from a stream.
+type streamReader interface {
+ ReadFromStream() ([]byte, error)
+}
+
+// newStreamReader returns a new streamReader based on the given
+// delimiter.
+//
+// By default, the streamReader uses a simple a *bufio.Reader
+// which splits on newlines, and otherwise use a *bufio.Scanner to
+// split on custom delimiters.
+func newStreamReader(reader io.Reader, options *streamOptions) streamReader {
+ if !options.isEmpty() {
+ if options.maxBufSize == 0 {
+ options.maxBufSize = defaultMaxBufSize
+ }
+ if options.format == StreamFormatSSE {
+ return newSseStreamReader(reader, options)
+ }
+ return newScannerStreamReader(reader, options)
+ }
+ return newBufferStreamReader(reader)
+}
+
+// BufferStreamReader reads data from a *bufio.Reader, which splits
+// on newlines.
+type BufferStreamReader struct {
+ reader *bufio.Reader
+}
+
+func newBufferStreamReader(reader io.Reader) *BufferStreamReader {
+ return &BufferStreamReader{
+ reader: bufio.NewReader(reader),
+ }
+}
+
+func (b *BufferStreamReader) ReadFromStream() ([]byte, error) {
+ line, err := b.reader.ReadBytes('\n')
+ if err != nil {
+ return nil, err
+ }
+ // Strip the trailing newline
+ return bytes.TrimSuffix(line, []byte("\n")), nil
+}
+
+// ScannerStreamReader reads data from a *bufio.Scanner, which allows for
+// configurable delimiters.
+type ScannerStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newScannerStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *ScannerStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &ScannerStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Split(func(bytes []byte, atEOF bool) (int, []byte, error) {
+ if atEOF && len(bytes) == 0 {
+ return 0, nil, nil
+ }
+ n, data, err := stream.parse(bytes)
+ if stream.isTerminated(data) {
+ return 0, nil, io.EOF
+ }
+ return n, data, err
+ })
+ return stream
+}
+
+func (s *ScannerStreamReader) ReadFromStream() ([]byte, error) {
+ if s.scanner.Scan() {
+ return s.scanner.Bytes(), nil
+ }
+ if err := s.scanner.Err(); err != nil {
+ return nil, err
+ }
+ return nil, io.EOF
+}
+
+func (s *ScannerStreamReader) parse(bytes []byte) (int, []byte, error) {
+ var startIndex int
+ if s.options != nil && s.options.prefix != "" {
+ if i := strings.Index(string(bytes), s.options.prefix); i >= 0 {
+ startIndex = i + len(s.options.prefix)
+ }
+ }
+ data := bytes[startIndex:]
+ lineDelimiter := s.options.getLineDelimiter()
+ delimIndex := strings.Index(string(data), lineDelimiter)
+ if delimIndex < 0 {
+ return startIndex + len(data), data, nil
+ }
+ endIndex := delimIndex + len(lineDelimiter)
+ parsedData := data[:endIndex]
+ n := startIndex + endIndex
+ return n, parsedData, nil
+}
+
+func (s *ScannerStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+type streamOptions struct {
+ delimiter string
+ prefix string
+ terminator string
+ format StreamFormat
+ maxBufSize int
+}
+
+func (s *streamOptions) isEmpty() bool {
+ return s.delimiter == "" && s.prefix == "" && s.terminator == "" && s.format == StreamFormatEmpty
+}
+
+func (s *streamOptions) getLineDelimiter() string {
+ if s.delimiter != "" {
+ return s.delimiter
+ }
+ return sseLineSeparator
+}
+
+type SseStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newSseStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *SseStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &SseStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Buffer(make([]byte, slices.Min([]int{4096, options.maxBufSize})), options.maxBufSize)
+
+ // Configure scanner to split on SSE event separator (\n\n)
+ // This is fixed by the SSE specification and cannot be changed
+ scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ // SSE messages are always separated by blank lines (\n\n)
+ if i := strings.Index(string(data), sseEventSeparator); i >= 0 {
+ return i + len(sseEventSeparator), data[0:i], nil
+ }
+
+ if atEOF || stream.isTerminated(data) {
+ return len(data), data, nil
+ }
+ return 0, nil, nil
+ })
+ return stream
+}
+
+func (s *SseStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+func (s *SseStreamReader) ReadFromStream() ([]byte, error) {
+
+ event, err := s.nextEvent()
+ if err != nil {
+ return nil, err
+ }
+ return event.data, nil
+}
+
+func (s *SseStreamReader) nextEvent() (*SseEvent, error) {
+
+ event := SseEvent{}
+ if s.scanner.Scan() {
+ rawEvent := s.scanner.Bytes()
+
+ // Parse individual lines within the SSE message
+ // Lines are always separated by \n within a message (SSE specification)
+ lines := strings.Split(string(rawEvent), sseLineSeparator)
+ for _, line := range lines {
+ s.parseSseLine([]byte(line), &event)
+ }
+
+ if event.size() > s.options.maxBufSize {
+ return nil, errors.New("SseStreamReader.ReadFromStream: buffer limit exceeded")
+ }
+ return &event, nil
+ }
+ return &event, io.EOF
+}
+
+func (s *SseStreamReader) parseSseLine(_bytes []byte, event *SseEvent) {
+ // Try to parse with space first (standard format), then without space (lenient format)
+ if value, ok := s.tryParseField(_bytes, sseDataPrefix, sseDataPrefixNoSpace); ok {
+ if len(event.data) > 0 {
+ // Join multiple data: lines using the configured delimiter
+ // This allows customization of how multi-line data is concatenated:
+ // - "\n" (default): preserves line breaks for multi-line JSON
+ // - "": concatenates without separator
+ // - Any other string: custom separator
+ lineDelimiter := s.options.getLineDelimiter()
+ event.data = append(event.data, lineDelimiter...)
+ }
+ event.data = append(event.data, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseIdPrefix, sseIdPrefixNoSpace); ok {
+ event.id = append(event.id, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseEventPrefix, sseEventPrefixNoSpace); ok {
+ event.event = append(event.event, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseRetryPrefix, sseRetryPrefixNoSpace); ok {
+ event.retry = append(event.retry, value...)
+ }
+}
+
+// tryParseField attempts to parse an SSE field by trying multiple prefix patterns in order.
+// This handles APIs that don't strictly follow the SSE specification by omitting the space after the colon.
+// It tries each prefix in the order provided and returns the value after the first matching prefix.
+func (s *SseStreamReader) tryParseField(line []byte, prefixes ...[]byte) ([]byte, bool) {
+ for _, prefix := range prefixes {
+ if bytes.HasPrefix(line, prefix) {
+ return line[len(prefix):], true
+ }
+ }
+ return nil, false
+}
+
+func (event *SseEvent) size() int {
+ return len(event.id) + len(event.data) + len(event.event) + len(event.retry)
+}
+
+func (event *SseEvent) String() string {
+ return fmt.Sprintf("SseEvent{id: %q, event: %q, data: %q, retry: %q}", event.id, event.event, event.data, event.retry)
+}
+
+type SseEvent struct {
+ id []byte
+ data []byte
+ event []byte
+ retry []byte
+}
+
+var (
+ sseIdPrefix = []byte("id: ")
+ sseDataPrefix = []byte("data: ")
+ sseEventPrefix = []byte("event: ")
+ sseRetryPrefix = []byte("retry: ")
+
+ // Lenient prefixes without space for APIs that don't strictly follow SSE specification
+ sseIdPrefixNoSpace = []byte("id:")
+ sseDataPrefixNoSpace = []byte("data:")
+ sseEventPrefixNoSpace = []byte("event:")
+ sseRetryPrefixNoSpace = []byte("retry:")
+)
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/dynamic-snippets/example0/snippet.go b/seed/go-sdk/server-sent-events/with-wire-tests/dynamic-snippets/example0/snippet.go
new file mode 100644
index 000000000000..de04b3cedb33
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/dynamic-snippets/example0/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/sse-go/client"
+ option "github.com/fern-api/sse-go/option"
+ sse "github.com/fern-api/sse-go"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &sse.StreamCompletionRequest{
+ Query: "query",
+ }
+ client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/error_codes.go b/seed/go-sdk/server-sent-events/with-wire-tests/error_codes.go
new file mode 100644
index 000000000000..144732a0c1f9
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/error_codes.go
@@ -0,0 +1,9 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package sse
+
+import (
+ internal "github.com/fern-api/sse-go/internal"
+)
+
+var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/file_param.go b/seed/go-sdk/server-sent-events/with-wire-tests/file_param.go
new file mode 100644
index 000000000000..16e0931f7015
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/file_param.go
@@ -0,0 +1,41 @@
+package sse
+
+import (
+ "io"
+)
+
+// FileParam is a file type suitable for multipart/form-data uploads.
+type FileParam struct {
+ io.Reader
+ filename string
+ contentType string
+}
+
+// FileParamOption adapts the behavior of the FileParam. No options are
+// implemented yet, but this interface allows for future extensibility.
+type FileParamOption interface {
+ apply()
+}
+
+// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file
+// upload endpoints accept a simple io.Reader, which is usually created by opening a file
+// via os.Open.
+//
+// However, some endpoints require additional metadata about the file such as a specific
+// Content-Type or custom filename. FileParam makes it easier to create the correct type
+// signature for these endpoints.
+func NewFileParam(
+ reader io.Reader,
+ filename string,
+ contentType string,
+ opts ...FileParamOption,
+) *FileParam {
+ return &FileParam{
+ Reader: reader,
+ filename: filename,
+ contentType: contentType,
+ }
+}
+
+func (f *FileParam) Name() string { return f.filename }
+func (f *FileParam) ContentType() string { return f.contentType }
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/go.mod b/seed/go-sdk/server-sent-events/with-wire-tests/go.mod
new file mode 100644
index 000000000000..9ee294b2f13b
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/go.mod
@@ -0,0 +1,16 @@
+module github.com/fern-api/sse-go
+
+go 1.21
+
+toolchain go1.23.8
+
+require github.com/google/uuid v1.6.0
+
+require github.com/stretchr/testify v1.8.4
+
+require gopkg.in/yaml.v3 v3.0.1 // indirect
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+)
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/go.sum b/seed/go-sdk/server-sent-events/with-wire-tests/go.sum
new file mode 100644
index 000000000000..fcca6d128057
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/go.sum
@@ -0,0 +1,12 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller.go
new file mode 100644
index 000000000000..d830bcbbeb77
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller.go
@@ -0,0 +1,250 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+
+ "github.com/fern-api/sse-go/core"
+)
+
+const (
+ // contentType specifies the JSON Content-Type header value.
+ contentType = "application/json"
+ contentTypeHeader = "Content-Type"
+)
+
+// Caller calls APIs and deserializes their response, if any.
+type Caller struct {
+ client core.HTTPClient
+ retrier *Retrier
+}
+
+// CallerParams represents the parameters used to constrcut a new *Caller.
+type CallerParams struct {
+ Client core.HTTPClient
+ MaxAttempts uint
+}
+
+// NewCaller returns a new *Caller backed by the given parameters.
+func NewCaller(params *CallerParams) *Caller {
+ var httpClient core.HTTPClient = http.DefaultClient
+ if params.Client != nil {
+ httpClient = params.Client
+ }
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+ return &Caller{
+ client: httpClient,
+ retrier: NewRetrier(retryOptions...),
+ }
+}
+
+// CallParams represents the parameters used to issue an API call.
+type CallParams struct {
+ URL string
+ Method string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client core.HTTPClient
+ Request interface{}
+ Response interface{}
+ ResponseIsOptional bool
+ ErrorDecoder ErrorDecoder
+}
+
+// CallResponse is a parsed HTTP response from an API call.
+type CallResponse struct {
+ StatusCode int
+ Header http.Header
+}
+
+// Call issues an API call according to the given call parameters.
+func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := c.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := c.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Close the response body after we're done.
+ defer resp.Body.Close()
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ // Mutate the response parameter in-place.
+ if params.Response != nil {
+ if writer, ok := params.Response.(io.Writer); ok {
+ _, err = io.Copy(writer, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(params.Response)
+ }
+ if err != nil {
+ if err == io.EOF {
+ if params.ResponseIsOptional {
+ // The response is optional, so we should ignore the
+ // io.EOF error
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+ }
+ return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
+ }
+ return nil, err
+ }
+ }
+
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+}
+
+// buildURL constructs the final URL by appending the given query parameters (if any).
+func buildURL(
+ url string,
+ queryParameters url.Values,
+) string {
+ if len(queryParameters) == 0 {
+ return url
+ }
+ if strings.ContainsRune(url, '?') {
+ url += "&"
+ } else {
+ url += "?"
+ }
+ url += queryParameters.Encode()
+ return url
+}
+
+// newRequest returns a new *http.Request with all of the fields
+// required to issue the call.
+func newRequest(
+ ctx context.Context,
+ url string,
+ method string,
+ endpointHeaders http.Header,
+ request interface{},
+ bodyProperties map[string]interface{},
+) (*http.Request, error) {
+ requestBody, err := newRequestBody(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ req.Header.Set(contentTypeHeader, contentType)
+ for name, values := range endpointHeaders {
+ req.Header[name] = values
+ }
+ return req, nil
+}
+
+// newRequestBody returns a new io.Reader that represents the HTTP request body.
+func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
+ if isNil(request) {
+ if len(bodyProperties) == 0 {
+ return nil, nil
+ }
+ requestBytes, err := json.Marshal(bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+ }
+ if body, ok := request.(io.Reader); ok {
+ return body, nil
+ }
+ requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+}
+
+// decodeError decodes the error from the given HTTP response. Note that
+// it's the caller's responsibility to close the response body.
+func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
+ if errorDecoder != nil {
+ // This endpoint has custom errors, so we'll
+ // attempt to unmarshal the error into a structured
+ // type based on the status code.
+ return errorDecoder(response.StatusCode, response.Header, response.Body)
+ }
+ // This endpoint doesn't have any custom error
+ // types, so we just read the body as-is, and
+ // put it into a normal error.
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if err == io.EOF {
+ // The error didn't have a response body,
+ // so all we can do is return an error
+ // with the status code.
+ return core.NewAPIError(response.StatusCode, response.Header, nil)
+ }
+ return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
+}
+
+// isNil is used to determine if the request value is equal to nil (i.e. an interface
+// value that holds a nil concrete value is itself non-nil).
+func isNil(value interface{}) bool {
+ return value == nil || reflect.ValueOf(value).IsNil()
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller_test.go
new file mode 100644
index 000000000000..2bb769e1dd18
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/caller_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/fern-api/sse-go/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// InternalTestCase represents a single test case.
+type InternalTestCase struct {
+ description string
+
+ // Server-side assertions.
+ givePathSuffix string
+ giveMethod string
+ giveResponseIsOptional bool
+ giveHeader http.Header
+ giveErrorDecoder ErrorDecoder
+ giveRequest *InternalTestRequest
+ giveQueryParams url.Values
+ giveBodyProperties map[string]interface{}
+
+ // Client-side assertions.
+ wantResponse *InternalTestResponse
+ wantHeaders http.Header
+ wantError error
+}
+
+// InternalTestRequest a simple request body.
+type InternalTestRequest struct {
+ Id string `json:"id"`
+}
+
+// InternalTestResponse a simple response body.
+type InternalTestResponse struct {
+ Id string `json:"id"`
+ ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
+ QueryParameters url.Values `json:"queryParameters,omitempty"`
+}
+
+// InternalTestNotFoundError represents a 404.
+type InternalTestNotFoundError struct {
+ *core.APIError
+
+ Message string `json:"message"`
+}
+
+func TestCall(t *testing.T) {
+ tests := []*InternalTestCase{
+ {
+ description: "GET success",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ },
+ },
+ {
+ description: "GET success with query",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ },
+ },
+ },
+ {
+ description: "GET not found",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusNotFound),
+ },
+ giveErrorDecoder: newTestErrorDecoder(t),
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(
+ http.StatusNotFound,
+ http.Header{},
+ errors.New(`{"message":"ID \"404\" not found"}`),
+ ),
+ },
+ },
+ {
+ description: "POST empty body",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: nil,
+ wantError: core.NewAPIError(
+ http.StatusBadRequest,
+ http.Header{},
+ errors.New("invalid request"),
+ ),
+ },
+ {
+ description: "POST optional response",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveResponseIsOptional: true,
+ },
+ {
+ description: "POST API error",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusInternalServerError),
+ },
+ wantError: core.NewAPIError(
+ http.StatusInternalServerError,
+ http.Header{},
+ errors.New("failed to process request"),
+ ),
+ },
+ {
+ description: "POST extra properties",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: new(InternalTestRequest),
+ giveBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ wantResponse: &InternalTestResponse{
+ ExtraBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ },
+ },
+ {
+ description: "GET extra query parameters",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "extra": []string{"true"},
+ },
+ },
+ },
+ {
+ description: "GET merge extra query parameters",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ "extra": []string{"true"},
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ var (
+ server = newTestServer(t, test)
+ client = server.Client()
+ )
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL + test.givePathSuffix,
+ Method: test.giveMethod,
+ Headers: test.giveHeader,
+ BodyProperties: test.giveBodyProperties,
+ QueryParameters: test.giveQueryParams,
+ Request: test.giveRequest,
+ Response: &response,
+ ResponseIsOptional: test.giveResponseIsOptional,
+ ErrorDecoder: test.giveErrorDecoder,
+ },
+ )
+ if test.wantError != nil {
+ assert.EqualError(t, err, test.wantError.Error())
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+func TestMergeHeaders(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ merged := MergeHeaders(make(http.Header), make(http.Header))
+ assert.Empty(t, merged)
+ })
+
+ t.Run("empty left", func(t *testing.T) {
+ left := make(http.Header)
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("empty right", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.1")
+
+ right := make(http.Header)
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("single value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.0")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+
+ t.Run("multiple value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Versions", "0.0.0")
+
+ right := make(http.Header)
+ right.Add("X-API-Versions", "0.0.1")
+ right.Add("X-API-Versions", "0.0.2")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
+ })
+
+ t.Run("disjoint merge", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Tenancy", "test")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+}
+
+// newTestServer returns a new *httptest.Server configured with the
+// given test parameters.
+func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tc.giveMethod, r.Method)
+ assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
+ for header, value := range tc.giveHeader {
+ assert.Equal(t, value, r.Header.Values(header))
+ }
+
+ request := new(InternalTestRequest)
+
+ bytes, err := io.ReadAll(r.Body)
+ if tc.giveRequest == nil {
+ require.Empty(t, bytes)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err = w.Write([]byte("invalid request"))
+ require.NoError(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+
+ switch request.Id {
+ case strconv.Itoa(http.StatusNotFound):
+ notFoundError := &InternalTestNotFoundError{
+ APIError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ Message: fmt.Sprintf("ID %q not found", request.Id),
+ }
+ bytes, err = json.Marshal(notFoundError)
+ require.NoError(t, err)
+
+ w.WriteHeader(http.StatusNotFound)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ return
+
+ case strconv.Itoa(http.StatusInternalServerError):
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = w.Write([]byte("failed to process request"))
+ require.NoError(t, err)
+ return
+ }
+
+ if tc.giveResponseIsOptional {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ extraBodyProperties := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
+ delete(extraBodyProperties, "id")
+
+ response := &InternalTestResponse{
+ Id: request.Id,
+ ExtraBodyProperties: extraBodyProperties,
+ QueryParameters: r.URL.Query(),
+ }
+ bytes, err = json.Marshal(response)
+ require.NoError(t, err)
+
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ },
+ ),
+ )
+}
+
+// newTestErrorDecoder returns an error decoder suitable for tests.
+func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ require.NoError(t, err)
+
+ var (
+ apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
+ decoder = json.NewDecoder(bytes.NewReader(raw))
+ )
+ if statusCode == http.StatusNotFound {
+ value := new(InternalTestNotFoundError)
+ value.APIError = apiError
+ require.NoError(t, decoder.Decode(value))
+
+ return value
+ }
+ return apiError
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder.go
new file mode 100644
index 000000000000..4a31679a6b63
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder.go
@@ -0,0 +1,64 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/fern-api/sse-go/core"
+)
+
+// ErrorCodes maps HTTP status codes to error constructors.
+type ErrorCodes map[int]func(*core.APIError) error
+
+// ErrorDecoder decodes *http.Response errors and returns a
+// typed API error (e.g. *core.APIError).
+type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
+
+// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
+// errorCodesOverrides is optional and will be merged with the default error codes,
+// with overrides taking precedence.
+func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
+ // Merge default error codes with overrides
+ mergedErrorCodes := make(ErrorCodes)
+
+ // Start with default error codes
+ for statusCode, errorFunc := range errorCodes {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+
+ // Apply overrides if provided
+ if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
+ for statusCode, errorFunc := range errorCodesOverrides[0] {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+ }
+
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ if err != nil {
+ return fmt.Errorf("failed to read error from response body: %w", err)
+ }
+ apiError := core.NewAPIError(
+ statusCode,
+ header,
+ errors.New(string(raw)),
+ )
+ newErrorFunc, ok := mergedErrorCodes[statusCode]
+ if !ok {
+ // This status code isn't recognized, so we return
+ // the API error as-is.
+ return apiError
+ }
+ customError := newErrorFunc(apiError)
+ if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
+ // If we fail to decode the error, we return the
+ // API error as-is.
+ return apiError
+ }
+ return customError
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder_test.go
new file mode 100644
index 000000000000..eb42b9eb7007
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/error_decoder_test.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/fern-api/sse-go/core"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorDecoder(t *testing.T) {
+ decoder := NewErrorDecoder(
+ ErrorCodes{
+ http.StatusNotFound: func(apiError *core.APIError) error {
+ return &InternalTestNotFoundError{APIError: apiError}
+ },
+ })
+
+ tests := []struct {
+ description string
+ giveStatusCode int
+ giveHeader http.Header
+ giveBody string
+ wantError error
+ }{
+ {
+ description: "unrecognized status code",
+ giveStatusCode: http.StatusInternalServerError,
+ giveHeader: http.Header{},
+ giveBody: "Internal Server Error",
+ wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
+ },
+ {
+ description: "not found with valid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `{"message": "Resource not found"}`,
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
+ Message: "Resource not found",
+ },
+ },
+ {
+ description: "not found with invalid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `Resource not found`,
+ wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
+ })
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields.go
new file mode 100644
index 000000000000..4bdf34fc2b7c
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+ "math/big"
+ "reflect"
+ "strings"
+)
+
+// HandleExplicitFields processes a struct to remove `omitempty` from
+// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
+// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
+// Returns an interface{} that can be passed to json.Marshal.
+func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
+ val := reflect.ValueOf(marshaler)
+ typ := reflect.TypeOf(marshaler)
+
+ // Handle pointer types
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil
+ }
+ val = val.Elem()
+ typ = typ.Elem()
+ }
+
+ // Only handle struct types
+ if val.Kind() != reflect.Struct {
+ return marshaler
+ }
+
+ // Handle embedded struct pattern
+ var sourceVal reflect.Value
+ var sourceType reflect.Type
+
+ // Check if this is an embedded struct pattern
+ if typ.NumField() == 1 && typ.Field(0).Anonymous {
+ // This is likely an embedded struct, get the embedded value
+ embeddedField := val.Field(0)
+ sourceVal = embeddedField
+ sourceType = embeddedField.Type()
+ } else {
+ // Regular struct
+ sourceVal = val
+ sourceType = typ
+ }
+
+ // If no explicit fields set, use standard marshaling
+ if explicitFields == nil || explicitFields.Sign() == 0 {
+ return marshaler
+ }
+
+ // Create a new struct type with modified tags
+ fields := make([]reflect.StructField, 0, sourceType.NumField())
+
+ for i := 0; i < sourceType.NumField(); i++ {
+ field := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !field.IsExported() || field.Name == "explicitFields" {
+ continue
+ }
+
+ // Check if this field has been explicitly set
+ fieldBit := big.NewInt(1)
+ fieldBit.Lsh(fieldBit, uint(i))
+ if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
+ // Remove omitempty from the json tag
+ tag := field.Tag.Get("json")
+ if tag != "" && tag != "-" {
+ // Parse the json tag, remove omitempty from options
+ parts := strings.Split(tag, ",")
+ if len(parts) > 1 {
+ var newParts []string
+ newParts = append(newParts, parts[0]) // Keep the field name
+ for _, part := range parts[1:] {
+ if strings.TrimSpace(part) != "omitempty" {
+ newParts = append(newParts, part)
+ }
+ }
+ tag = strings.Join(newParts, ",")
+ }
+
+ // Reconstruct the struct tag
+ newTag := `json:"` + tag + `"`
+ if urlTag := field.Tag.Get("url"); urlTag != "" {
+ newTag += ` url:"` + urlTag + `"`
+ }
+
+ field.Tag = reflect.StructTag(newTag)
+ }
+ }
+
+ fields = append(fields, field)
+ }
+
+ // Create new struct type with modified tags
+ newType := reflect.StructOf(fields)
+ newVal := reflect.New(newType).Elem()
+
+ // Copy field values from original struct to new struct
+ fieldIndex := 0
+ for i := 0; i < sourceType.NumField(); i++ {
+ originalField := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !originalField.IsExported() || originalField.Name == "explicitFields" {
+ continue
+ }
+
+ originalValue := sourceVal.Field(i)
+ newVal.Field(fieldIndex).Set(originalValue)
+ fieldIndex++
+ }
+
+ return newVal.Interface()
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields_test.go
new file mode 100644
index 000000000000..3d05e88a2ce9
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/explicit_fields_test.go
@@ -0,0 +1,497 @@
+package internal
+
+import (
+ "encoding/json"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testExplicitFieldsStruct struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+ Count *int `json:"count,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ //lint:ignore unused this field is intentionally unused for testing
+ unexported string `json:"-"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ testFieldName = big.NewInt(1 << 0)
+ testFieldCode = big.NewInt(1 << 1)
+ testFieldCount = big.NewInt(1 << 2)
+ testFieldEnabled = big.NewInt(1 << 3)
+ testFieldTags = big.NewInt(1 << 4)
+)
+
+func (t *testExplicitFieldsStruct) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+func (t *testExplicitFieldsStruct) SetName(name *string) {
+ t.Name = name
+ t.require(testFieldName)
+}
+
+func (t *testExplicitFieldsStruct) SetCode(code *string) {
+ t.Code = code
+ t.require(testFieldCode)
+}
+
+func (t *testExplicitFieldsStruct) SetCount(count *int) {
+ t.Count = count
+ t.require(testFieldCount)
+}
+
+func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
+ t.Enabled = enabled
+ t.require(testFieldEnabled)
+}
+
+func (t *testExplicitFieldsStruct) SetTags(tags []string) {
+ t.Tags = tags
+ t.require(testFieldTags)
+}
+
+func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*t),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
+}
+
+type testStructWithoutExplicitFields struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+}
+
+func TestHandleExplicitFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveInput interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "nil input",
+ giveInput: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "non-struct input",
+ giveInput: "string",
+ wantBytes: []byte(`"string"`),
+ },
+ {
+ desc: "slice input",
+ giveInput: []string{"a", "b"},
+ wantBytes: []byte(`["a","b"]`),
+ },
+ {
+ desc: "map input",
+ giveInput: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "struct without explicitFields field",
+ giveInput: &testStructWithoutExplicitFields{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with no explicit fields set",
+ giveInput: &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with explicit nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null}`),
+ },
+ {
+ desc: "struct with explicit non-nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("explicit"))
+ s.SetCode(stringPtr("also-explicit"))
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
+ },
+ {
+ desc: "struct with mixed explicit and implicit fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Count: intPtr(42),
+ }
+ s.SetCode(nil) // explicit nil
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
+ },
+ {
+ desc: "struct with multiple explicit nil fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ s.SetCount(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
+ },
+ {
+ desc: "struct with slice field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Tags: []string{"tag1", "tag2"},
+ }
+ s.SetTags(nil) // explicit nil slice
+ return s
+ }(),
+ wantBytes: []byte(`{"tags":null}`),
+ },
+ {
+ desc: "struct with boolean field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetEnabled(boolPtr(false)) // explicit false
+ return s
+ }(),
+ wantBytes: []byte(`{"enabled":false}`),
+ },
+ {
+ desc: "struct with all fields explicit",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("test"))
+ s.SetCode(nil)
+ s.SetCount(intPtr(0))
+ s.SetEnabled(boolPtr(false))
+ s.SetTags([]string{})
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ var explicitFields *big.Int
+ if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
+ explicitFields = s.explicitFields
+ }
+ bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
+ t.Run("custom marshaler with explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
+ t.Run("nil pointer", func(t *testing.T) {
+ var s *testExplicitFieldsStruct
+ bytes, err := json.Marshal(HandleExplicitFields(s, nil))
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`null`), bytes)
+ })
+
+ t.Run("pointer to struct", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
+ t.Run("embedded struct with explicit fields", func(t *testing.T) {
+ // Create a struct similar to what MarshalJSON creates
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include both explicit fields (name as null, code as "test-code")
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should only include non-nil fields (omitempty behavior)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with mixed fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Count: intPtr(42), // implicit field
+ }
+ s.SetName(nil) // explicit nil
+ s.SetCode(stringPtr("explicit")) // explicit value
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include explicit null, explicit value, and implicit value
+ assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsTagHandling(t *testing.T) {
+ type testStructWithComplexTags struct {
+ Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
+ Field2 *string `json:"field2,omitempty,string" url:"field2"`
+ Field3 *string `json:"-"`
+ Field4 *string `json:"field4"`
+ explicitFields *big.Int `json:"-"`
+ }
+
+ s := &testStructWithComplexTags{
+ Field1: stringPtr("test1"),
+ Field4: stringPtr("test4"),
+ explicitFields: big.NewInt(1), // Only first field is explicit
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+
+ // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
+ assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
+}
+
+// Test types for nested struct explicit fields testing
+type testNestedStruct struct {
+ NestedName *string `json:"nested_name,omitempty"`
+ NestedCode *string `json:"nested_code,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+type testParentStruct struct {
+ ParentName *string `json:"parent_name,omitempty"`
+ Nested *testNestedStruct `json:"nested,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ nestedFieldName = big.NewInt(1 << 0)
+ nestedFieldCode = big.NewInt(1 << 1)
+)
+
+var (
+ parentFieldName = big.NewInt(1 << 0)
+ parentFieldNested = big.NewInt(1 << 1)
+)
+
+func (n *testNestedStruct) require(field *big.Int) {
+ if n.explicitFields == nil {
+ n.explicitFields = big.NewInt(0)
+ }
+ n.explicitFields.Or(n.explicitFields, field)
+}
+
+func (n *testNestedStruct) SetNestedName(name *string) {
+ n.NestedName = name
+ n.require(nestedFieldName)
+}
+
+func (n *testNestedStruct) SetNestedCode(code *string) {
+ n.NestedCode = code
+ n.require(nestedFieldCode)
+}
+
+func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
+ type embed testNestedStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*n),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
+}
+
+func (p *testParentStruct) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+func (p *testParentStruct) SetParentName(name *string) {
+ p.ParentName = name
+ p.require(parentFieldName)
+}
+
+func (p *testParentStruct) SetNested(nested *testNestedStruct) {
+ p.Nested = nested
+ p.require(parentFieldNested)
+}
+
+func (p *testParentStruct) MarshalJSON() ([]byte, error) {
+ type embed testParentStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
+}
+
+func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
+ tests := []struct {
+ desc string
+ setupFunc func() *testParentStruct
+ wantBytes []byte
+ }{
+ {
+ desc: "nested struct with explicit nil in nested object",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{
+ NestedName: stringPtr("implicit-nested"),
+ }
+ nested.SetNestedCode(nil) // explicit nil
+
+ return &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ Nested: nested,
+ }
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
+ },
+ {
+ desc: "parent with explicit nil nested struct",
+ setupFunc: func() *testParentStruct {
+ parent := &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ }
+ parent.SetNested(nil) // explicit nil nested struct
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
+ },
+ {
+ desc: "all explicit fields in nested structure",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{}
+ nested.SetNestedName(stringPtr("explicit-nested"))
+ nested.SetNestedCode(nil) // explicit nil
+
+ parent := &testParentStruct{}
+ parent.SetParentName(nil) // explicit nil
+ parent.SetNested(nested) // explicit nested struct
+
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ parent := tt.setupFunc()
+ bytes, err := parent.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties.go
new file mode 100644
index 000000000000..540c3fd89eeb
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]interface{}
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value interface{}) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties_test.go
new file mode 100644
index 000000000000..aa2510ee5121
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler interface{}
+ giveExtraProperties map[string]interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]interface{}{42: "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]interface{}{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/http.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/http.go
new file mode 100644
index 000000000000..77863752bb58
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/http.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+)
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// ResolveBaseURL resolves the base URL from the given arguments,
+// preferring the first non-empty value.
+func ResolveBaseURL(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// EncodeURL encodes the given arguments into the URL, escaping
+// values as needed. Pointer arguments are dereferenced before processing.
+func EncodeURL(urlFormat string, args ...interface{}) string {
+ escapedArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ // Dereference the argument if it's a pointer
+ value := dereferenceArg(arg)
+ escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
+ }
+ return fmt.Sprintf(urlFormat, escapedArgs...)
+}
+
+// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
+// If the argument is not a pointer or is nil, it returns the argument as-is.
+func dereferenceArg(arg interface{}) interface{} {
+ if arg == nil {
+ return arg
+ }
+
+ v := reflect.ValueOf(arg)
+
+ // Keep dereferencing until we get to a non-pointer value or hit nil
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return nil
+ }
+ v = v.Elem()
+ }
+
+ return v.Interface()
+}
+
+// MergeHeaders merges the given headers together, where the right
+// takes precedence over the left.
+func MergeHeaders(left, right http.Header) http.Header {
+ for key, values := range right {
+ if len(values) > 1 {
+ left[key] = values
+ continue
+ }
+ if value := right.Get(key); value != "" {
+ left.Set(key, value)
+ }
+ }
+ return left
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/query.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/query.go
new file mode 100644
index 000000000000..1cbaf7fe1c02
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/query.go
@@ -0,0 +1,353 @@
+package internal
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+var (
+ bytesType = reflect.TypeOf([]byte{})
+ queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ uuidType = reflect.TypeOf(uuid.UUID{})
+)
+
+// QueryEncoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type QueryEncoder interface {
+ EncodeQueryValues(key string, v *url.Values) error
+}
+
+// prepareValue handles common validation and unwrapping logic for both functions
+func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return reflect.Value{}, values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return reflect.Value{}, values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ if err != nil {
+ return reflect.Value{}, nil, err
+ }
+
+ return val, values, nil
+}
+
+// QueryValues encodes url.Values from request objects.
+//
+// Note: This type is inspired by Google's query encoding library, but
+// supports far less customization and is tailored to fit this SDK's use case.
+//
+// Ref: https://github.com/google/go-querystring
+func QueryValues(v interface{}) (url.Values, error) {
+ _, values, err := prepareValue(v)
+ return values, err
+}
+
+// QueryValuesWithDefaults encodes url.Values from request objects
+// and default values, merging the defaults into the request.
+// It's expected that the values of defaults are wire names.
+func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
+ val, values, err := prepareValue(v)
+ if err != nil {
+ return values, err
+ }
+ if !val.IsValid() {
+ return values, nil
+ }
+
+ // apply defaults to zero-value fields directly on the original struct
+ valType := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Field(i)
+ fieldType := valType.Field(i)
+ fieldName := fieldType.Name
+
+ if fieldType.PkgPath != "" && !fieldType.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ // check if field is zero value and we have a default for it
+ if field.CanSet() && field.IsZero() {
+ tag := fieldType.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+ wireName, _ := parseTag(tag)
+ if wireName == "" {
+ wireName = fieldName
+ }
+ if defaultVal, exists := defaults[wireName]; exists {
+ values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
+ }
+ }
+ }
+
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+
+ name, opts := parseTag(tag)
+ if name == "" {
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(queryEncoderType) {
+ // If sv is a nil pointer and the custom encoder is defined on a non-pointer
+ // method receiver, set sv to the zero value of the underlying type
+ if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(QueryEncoder)
+ if err := m.EncodeQueryValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ // Recursively dereference pointers, but stop at nil pointers.
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts, sf))
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ if sv.Len() == 0 {
+ // Skip if slice or array is empty.
+ continue
+ }
+ for i := 0; i < sv.Len(); i++ {
+ value := sv.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), name); err != nil {
+ return err
+ }
+ } else {
+ values.Add(name, valueString(value, opts, sf))
+ }
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Map {
+ if err := reflectMap(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ if err := reflectValue(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts, sf))
+ }
+
+ return nil
+}
+
+// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
+func reflectMap(values url.Values, val reflect.Value, scope string) error {
+ if val.IsNil() {
+ return nil
+ }
+
+ iter := val.MapRange()
+ for iter.Next() {
+ k := iter.Key()
+ v := iter.Value()
+
+ key := fmt.Sprint(k.Interface())
+ paramName := scope + "[" + key + "]"
+
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ break
+ }
+ v = v.Elem()
+ }
+
+ for v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Map {
+ if err := reflectMap(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Struct {
+ if err := reflectValue(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ if v.Len() == 0 {
+ continue
+ }
+ for i := 0; i < v.Len(); i++ {
+ value := v.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), paramName); err != nil {
+ return err
+ }
+ } else {
+ values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
+ }
+ }
+ continue
+ }
+
+ values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if format := sf.Tag.Get("format"); format == "date" {
+ return t.Format("2006-01-02")
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ if v.Type() == uuidType {
+ u := v.Interface().(uuid.UUID)
+ return u.String()
+ }
+
+ if v.Type() == bytesType {
+ b := v.Interface().([]byte)
+ return base64.StdEncoding.EncodeToString(b)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ type zeroable interface {
+ IsZero() bool
+ }
+
+ if !v.IsZero() {
+ if z, ok := v.Interface().(zeroable); ok {
+ return z.IsZero()
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
+ return false
+ }
+
+ return false
+}
+
+// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
+func isStructPointer(v reflect.Value) bool {
+ return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/query_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/query_test.go
new file mode 100644
index 000000000000..2c28cb8acf68
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/query_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryValues(t *testing.T) {
+ t.Run("empty optional", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+
+ t.Run("empty required", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Equal(t, "required=", values.Encode())
+ })
+
+ t.Run("allow multiple", func(t *testing.T) {
+ type example struct {
+ Values []string `json:"values" url:"values"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Values: []string{"foo", "bar", "baz"},
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
+ })
+
+ t.Run("nested object", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ nestedValue := "nestedValue"
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ Nested: &nested{
+ Value: &nestedValue,
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
+ })
+
+ t.Run("url unspecified", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("url ignored", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound" url:"-"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("datetime", func(t *testing.T) {
+ type example struct {
+ DateTime time.Time `json:"dateTime" url:"dateTime"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
+ })
+
+ t.Run("date", func(t *testing.T) {
+ type example struct {
+ Date time.Time `json:"date" url:"date" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "date=1994-03-16", values.Encode())
+ })
+
+ t.Run("optional time", func(t *testing.T) {
+ type example struct {
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
+ type enum string
+
+ type example struct {
+ Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("object array", func(t *testing.T) {
+ type object struct {
+ Key string `json:"key" url:"key"`
+ Value string `json:"value" url:"value"`
+ }
+ type example struct {
+ Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Objects: []*object{
+ {
+ Key: "hello",
+ Value: "world",
+ },
+ {
+ Key: "foo",
+ Value: "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
+ })
+
+ t.Run("map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "foo": "bar",
+ "baz": "qux",
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map array", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": []string{
+ "one",
+ "two",
+ "three",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
+ })
+}
+
+func TestQueryValuesWithDefaults(t *testing.T) {
+ t.Run("apply defaults to zero values", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ })
+
+ t.Run("preserve non-zero values over defaults", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Name: "actual-name",
+ Age: 30,
+ // Enabled remains false (zero value), should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
+ })
+
+ t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "nonexistent": "should-be-ignored",
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&name=default-name", values.Encode())
+ })
+
+ t.Run("type conversion for compatible defaults", func(t *testing.T) {
+ type example struct {
+ Count int64 `json:"count" url:"count"`
+ Rate float64 `json:"rate" url:"rate"`
+ Message string `json:"message" url:"message"`
+ }
+
+ defaults := map[string]interface{}{
+ "count": int(100), // int -> int64 conversion
+ "rate": float32(2.5), // float32 -> float64 conversion
+ "message": "hello", // string -> string (no conversion needed)
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
+ })
+
+ t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
+ Count int `json:"count,omitempty" url:"count,omitempty"`
+ }
+
+ defaultOptional := "default-optional"
+ defaults := map[string]interface{}{
+ "required": "default-required",
+ "optional": &defaultOptional, // pointer type
+ "count": 42,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Required: "custom-required", // should override default
+ // Optional is nil, should get default
+ // Count is 0, should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
+ })
+
+ t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
+ type example struct {
+ Name *string `json:"name" url:"name"`
+ Age *int `json:"age" url:"age"`
+ Enabled *bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ // first, test that a properly empty request is overridden:
+ {
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ }
+
+ // second, test that a request that contains zeros is not overridden:
+ var (
+ name = ""
+ age = 0
+ enabled = false
+ )
+ values, err := QueryValuesWithDefaults(&example{
+ Name: &name, // explicit empty string should override default
+ Age: &age, // explicit zero should override default
+ Enabled: &enabled, // explicit false should override default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
+ })
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier.go
new file mode 100644
index 000000000000..4efae1b4c286
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier.go
@@ -0,0 +1,230 @@
+package internal
+
+import (
+ "crypto/rand"
+ "math/big"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+const (
+ defaultRetryAttempts = 2
+ minRetryDelay = 1000 * time.Millisecond
+ maxRetryDelay = 60000 * time.Millisecond
+)
+
+// RetryOption adapts the behavior the *Retrier.
+type RetryOption func(*retryOptions)
+
+// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
+type RetryFunc func(*http.Request) (*http.Response, error)
+
+// WithMaxAttempts configures the maximum number of attempts
+// of the *Retrier.
+func WithMaxAttempts(attempts uint) RetryOption {
+ return func(opts *retryOptions) {
+ opts.attempts = attempts
+ }
+}
+
+// Retrier retries failed requests a configurable number of times with an
+// exponential back-off between each retry.
+type Retrier struct {
+ attempts uint
+}
+
+// NewRetrier constructs a new *Retrier with the given options, if any.
+func NewRetrier(opts ...RetryOption) *Retrier {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ attempts := uint(defaultRetryAttempts)
+ if options.attempts > 0 {
+ attempts = options.attempts
+ }
+ return &Retrier{
+ attempts: attempts,
+ }
+}
+
+// Run issues the request and, upon failure, retries the request if possible.
+//
+// The request will be retried as long as the request is deemed retryable and the
+// number of retry attempts has not grown larger than the configured retry limit.
+func (r *Retrier) Run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ opts ...RetryOption,
+) (*http.Response, error) {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ maxRetryAttempts := r.attempts
+ if options.attempts > 0 {
+ maxRetryAttempts = options.attempts
+ }
+ var (
+ retryAttempt uint
+ previousError error
+ )
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt,
+ previousError,
+ )
+}
+
+func (r *Retrier) run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ maxRetryAttempts uint,
+ retryAttempt uint,
+ previousError error,
+) (*http.Response, error) {
+ if retryAttempt >= maxRetryAttempts {
+ return nil, previousError
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := request.Context().Err(); err != nil {
+ return nil, err
+ }
+
+ response, err := fn(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.shouldRetry(response) {
+ defer response.Body.Close()
+
+ delay, err := r.retryDelay(response, retryAttempt)
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(delay)
+
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt+1,
+ decodeError(response, errorDecoder),
+ )
+ }
+
+ return response, nil
+}
+
+// shouldRetry returns true if the request should be retried based on the given
+// response status code.
+func (r *Retrier) shouldRetry(response *http.Response) bool {
+ return response.StatusCode == http.StatusTooManyRequests ||
+ response.StatusCode == http.StatusRequestTimeout ||
+ response.StatusCode >= http.StatusInternalServerError
+}
+
+// retryDelay calculates the delay time based on response headers,
+// falling back to exponential backoff if no headers are present.
+func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
+ // Check for Retry-After header first (RFC 7231), applying no jitter
+ if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
+ // Parse as number of seconds...
+ if seconds, err := strconv.Atoi(retryAfter); err == nil {
+ delay := time.Duration(seconds) * time.Second
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+
+ // ...or as an HTTP date; both are valid
+ if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
+ delay := time.Until(retryTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+ }
+
+ // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
+ if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
+ if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
+ // Assume Unix timestamp in seconds
+ resetTime := time.Unix(resetTimestamp, 0)
+ delay := time.Until(resetTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return r.addPositiveJitter(delay)
+ }
+ }
+ }
+
+ // Fall back to exponential backoff
+ return r.exponentialBackoff(retryAttempt)
+}
+
+// exponentialBackoff calculates the delay time based on the retry attempt
+// and applies symmetric jitter (±10% around the delay).
+func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
+ if retryAttempt > 63 { // 2^63+ would overflow uint64
+ retryAttempt = 63
+ }
+
+ delay := minRetryDelay << retryAttempt
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+
+ return r.addSymmetricJitter(delay)
+}
+
+// addJitterWithRange applies jitter to the given delay.
+// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
+func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
+ jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
+ jitter, err := rand.Int(rand.Reader, jitterRange)
+ if err != nil {
+ return 0, err
+ }
+
+ jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
+ if jitteredDelay < minRetryDelay {
+ jitteredDelay = minRetryDelay
+ }
+ if jitteredDelay > maxRetryDelay {
+ jitteredDelay = maxRetryDelay
+ }
+ return jitteredDelay, nil
+}
+
+// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
+func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 100, 120)
+}
+
+// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
+func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 90, 110)
+}
+
+type retryOptions struct {
+ attempts uint
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier_test.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier_test.go
new file mode 100644
index 000000000000..3c503ffb1195
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/retrier_test.go
@@ -0,0 +1,300 @@
+package internal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/fern-api/sse-go/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type RetryTestCase struct {
+ description string
+
+ giveAttempts uint
+ giveStatusCodes []int
+ giveResponse *InternalTestResponse
+
+ wantResponse *InternalTestResponse
+ wantError *core.APIError
+}
+
+func TestRetrier(t *testing.T) {
+ tests := []*RetryTestCase{
+ {
+ description: "retry request succeeds after multiple failures",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ giveResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ },
+ {
+ description: "retry request fails if MaxAttempts is exceeded",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusOK,
+ },
+ wantError: &core.APIError{
+ StatusCode: http.StatusRequestTimeout,
+ },
+ },
+ {
+ description: "retry durations increase exponentially and stay within the min and max delay values",
+ giveAttempts: 4,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ },
+ {
+ description: "retry does not occur on status code 404",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
+ wantError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ },
+ {
+ description: "retries occur on status code 429",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 408",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 500",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.description, func(t *testing.T) {
+ var (
+ test = tc
+ server = newTestRetryServer(t, test)
+ client = server.Client()
+ )
+
+ t.Parallel()
+
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: test.giveAttempts,
+ ResponseIsOptional: true,
+ },
+ )
+
+ if test.wantError != nil {
+ require.IsType(t, err, &core.APIError{})
+ expectedErrorCode := test.wantError.StatusCode
+ actualErrorCode := err.(*core.APIError).StatusCode
+ assert.Equal(t, expectedErrorCode, actualErrorCode)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+// newTestRetryServer returns a new *httptest.Server configured with the
+// given test parameters, suitable for testing retries.
+func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
+ var index int
+ timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
+
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if index > 0 && index < len(expectedRetryDurations) {
+ // Ensure that the duration between retries increases exponentially,
+ // and that it is within the minimum and maximum retry delay values.
+ actualDuration := timestamps[index].Sub(timestamps[index-1])
+ expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
+ expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
+ assert.True(
+ t,
+ actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
+ "expected duration to be in range [%v, %v], got %v",
+ expectedDurationMin,
+ expectedDurationMax,
+ actualDuration,
+ )
+ assert.LessOrEqual(
+ t,
+ actualDuration,
+ maxRetryDelay,
+ "expected duration to be less than the maxRetryDelay (%v), got %v",
+ maxRetryDelay,
+ actualDuration,
+ )
+ assert.GreaterOrEqual(
+ t,
+ actualDuration,
+ minRetryDelay,
+ "expected duration to be greater than the minRetryDelay (%v), got %v",
+ minRetryDelay,
+ actualDuration,
+ )
+ }
+
+ request := new(InternalTestRequest)
+ bytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+ require.LessOrEqual(t, index, len(tc.giveStatusCodes))
+
+ statusCode := tc.giveStatusCodes[index]
+
+ w.WriteHeader(statusCode)
+
+ if tc.giveResponse != nil && statusCode == http.StatusOK {
+ bytes, err = json.Marshal(tc.giveResponse)
+ require.NoError(t, err)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ }
+
+ index++
+ },
+ ),
+ )
+}
+
+// expectedRetryDurations holds an array of calculated retry durations,
+// where the index of the array should correspond to the retry attempt.
+//
+// Values are calculated based off of `minRetryDelay * 2^i`.
+var expectedRetryDurations = []time.Duration{
+ 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
+ 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
+ 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
+ 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
+}
+
+func TestRetryDelayTiming(t *testing.T) {
+ tests := []struct {
+ name string
+ headerName string
+ headerValueFunc func() string
+ expectedMinMs int64
+ expectedMaxMs int64
+ }{
+ {
+ name: "retry-after with seconds value",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return "1"
+ },
+ expectedMinMs: 500,
+ expectedMaxMs: 1500,
+ },
+ {
+ name: "retry-after with HTTP date",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return time.Now().Add(3 * time.Second).Format(time.RFC1123)
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ {
+ name: "x-ratelimit-reset with future timestamp",
+ headerName: "x-ratelimit-reset",
+ headerValueFunc: func() string {
+ return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ var timestamps []time.Time
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if len(timestamps) == 1 {
+ // First request - return retryable error with header
+ w.Header().Set(tt.headerName, tt.headerValueFunc())
+ w.WriteHeader(http.StatusTooManyRequests)
+ } else {
+ // Second request - return success
+ w.WriteHeader(http.StatusOK)
+ response := &InternalTestResponse{Id: "success"}
+ bytes, _ := json.Marshal(response)
+ w.Write(bytes)
+ }
+ }))
+ defer server.Close()
+
+ caller := NewCaller(&CallerParams{
+ Client: server.Client(),
+ })
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: 2,
+ ResponseIsOptional: true,
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, timestamps, 2, "Expected exactly 2 requests")
+
+ actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
+
+ assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
+ "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
+ assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
+ "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
+ })
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/streamer.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/streamer.go
new file mode 100644
index 000000000000..737f7b4b0218
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/streamer.go
@@ -0,0 +1,118 @@
+package internal
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/fern-api/sse-go/core"
+)
+
+const (
+ // DefaultDataPrefix is the default prefix used for SSE streaming.
+ DefaultSSEDataPrefix = "data: "
+
+ // DefaultTerminator is the default terminator used for SSE streaming.
+ DefaultSSETerminator = "[DONE]"
+)
+
+// Streamer calls APIs and streams responses using a *Stream.
+type Streamer[T any] struct {
+ client HTTPClient
+ retrier *Retrier
+}
+
+// NewStreamer returns a new *Streamer backed by the given caller's HTTP client.
+func NewStreamer[T any](caller *Caller) *Streamer[T] {
+ return &Streamer[T]{
+ client: caller.client,
+ retrier: caller.retrier,
+ }
+}
+
+// StreamParams represents the parameters used to issue an API streaming call.
+type StreamParams struct {
+ URL string
+ Method string
+ Prefix string
+ Delimiter string
+ Terminator string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client HTTPClient
+ Request interface{}
+ ErrorDecoder ErrorDecoder
+ Format core.StreamFormat
+}
+
+// Stream issues an API streaming call according to the given stream parameters.
+func (s *Streamer[T]) Stream(ctx context.Context, params *StreamParams) (*core.Stream[T], error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := s.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := s.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ defer resp.Body.Close()
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ defer resp.Body.Close()
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ var opts []core.StreamOption
+ if params.Delimiter != "" {
+ opts = append(opts, core.WithDelimiter(params.Delimiter))
+ }
+ if params.Prefix != "" {
+ opts = append(opts, core.WithPrefix(params.Prefix))
+ }
+ if params.Terminator != "" {
+ opts = append(opts, core.WithTerminator(params.Terminator))
+ }
+ if params.Format != core.StreamFormatEmpty {
+ opts = append(opts, core.WithFormat(params.Format))
+ }
+
+ return core.NewStream[T](resp, opts...), nil
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/stringer.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/stringer.go
new file mode 100644
index 000000000000..312801851e0e
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value interface{}) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/internal/time.go b/seed/go-sdk/server-sent-events/with-wire-tests/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/option/request_option.go b/seed/go-sdk/server-sent-events/with-wire-tests/option/request_option.go
new file mode 100644
index 000000000000..7421761f37bb
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/option/request_option.go
@@ -0,0 +1,64 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/fern-api/sse-go/core"
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of an individual request.
+type RequestOption = core.RequestOption
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return &core.BaseURLOption{
+ BaseURL: baseURL,
+ }
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return &core.HTTPClientOption{
+ HTTPClient: httpClient,
+ }
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return &core.HTTPHeaderOption{
+ // Clone the headers so they can't be modified after the option call.
+ HTTPHeader: httpHeader.Clone(),
+ }
+}
+
+// WithBodyProperties adds the given body properties to the request.
+func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
+ copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
+ for key, value := range bodyProperties {
+ copiedBodyProperties[key] = value
+ }
+ return &core.BodyPropertiesOption{
+ BodyProperties: copiedBodyProperties,
+ }
+}
+
+// WithQueryParameters adds the given query parameters to the request.
+func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
+ copiedQueryParameters := make(url.Values, len(queryParameters))
+ for key, values := range queryParameters {
+ copiedQueryParameters[key] = values
+ }
+ return &core.QueryParametersOption{
+ QueryParameters: copiedQueryParameters,
+ }
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return &core.MaxAttemptsOption{
+ MaxAttempts: attempts,
+ }
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/pointer.go b/seed/go-sdk/server-sent-events/with-wire-tests/pointer.go
new file mode 100644
index 000000000000..143c4ca885ce
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/pointer.go
@@ -0,0 +1,132 @@
+package sse
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// Bool returns a pointer to the given bool value.
+func Bool(b bool) *bool {
+ return &b
+}
+
+// Byte returns a pointer to the given byte value.
+func Byte(b byte) *byte {
+ return &b
+}
+
+// Complex64 returns a pointer to the given complex64 value.
+func Complex64(c complex64) *complex64 {
+ return &c
+}
+
+// Complex128 returns a pointer to the given complex128 value.
+func Complex128(c complex128) *complex128 {
+ return &c
+}
+
+// Float32 returns a pointer to the given float32 value.
+func Float32(f float32) *float32 {
+ return &f
+}
+
+// Float64 returns a pointer to the given float64 value.
+func Float64(f float64) *float64 {
+ return &f
+}
+
+// Int returns a pointer to the given int value.
+func Int(i int) *int {
+ return &i
+}
+
+// Int8 returns a pointer to the given int8 value.
+func Int8(i int8) *int8 {
+ return &i
+}
+
+// Int16 returns a pointer to the given int16 value.
+func Int16(i int16) *int16 {
+ return &i
+}
+
+// Int32 returns a pointer to the given int32 value.
+func Int32(i int32) *int32 {
+ return &i
+}
+
+// Int64 returns a pointer to the given int64 value.
+func Int64(i int64) *int64 {
+ return &i
+}
+
+// Rune returns a pointer to the given rune value.
+func Rune(r rune) *rune {
+ return &r
+}
+
+// String returns a pointer to the given string value.
+func String(s string) *string {
+ return &s
+}
+
+// Uint returns a pointer to the given uint value.
+func Uint(u uint) *uint {
+ return &u
+}
+
+// Uint8 returns a pointer to the given uint8 value.
+func Uint8(u uint8) *uint8 {
+ return &u
+}
+
+// Uint16 returns a pointer to the given uint16 value.
+func Uint16(u uint16) *uint16 {
+ return &u
+}
+
+// Uint32 returns a pointer to the given uint32 value.
+func Uint32(u uint32) *uint32 {
+ return &u
+}
+
+// Uint64 returns a pointer to the given uint64 value.
+func Uint64(u uint64) *uint64 {
+ return &u
+}
+
+// Uintptr returns a pointer to the given uintptr value.
+func Uintptr(u uintptr) *uintptr {
+ return &u
+}
+
+// UUID returns a pointer to the given uuid.UUID value.
+func UUID(u uuid.UUID) *uuid.UUID {
+ return &u
+}
+
+// Time returns a pointer to the given time.Time value.
+func Time(t time.Time) *time.Time {
+ return &t
+}
+
+// MustParseDate attempts to parse the given string as a
+// date time.Time, and panics upon failure.
+func MustParseDate(date string) time.Time {
+ t, err := time.Parse("2006-01-02", date)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
+
+// MustParseDateTime attempts to parse the given string as a
+// datetime time.Time, and panics upon failure.
+func MustParseDateTime(datetime string) time.Time {
+ t, err := time.Parse(time.RFC3339, datetime)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/reference.md b/seed/go-sdk/server-sent-events/with-wire-tests/reference.md
new file mode 100644
index 000000000000..27dc67976459
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/reference.md
@@ -0,0 +1,48 @@
+# Reference
+## Completions
+client.Completions.Stream(request) -> sse.StreamedCompletion
+
+-
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```go
+request := &sse.StreamCompletionRequest{
+ Query: "query",
+ }
+client.Completions.Stream(
+ context.TODO(),
+ request,
+ )
+}
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**query:** `string`
+
+
+
+
+
+
+
+
+
+
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/snippet.json b/seed/go-sdk/server-sent-events/with-wire-tests/snippet.json
new file mode 100644
index 000000000000..3beff974f7e2
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/snippet.json
@@ -0,0 +1,15 @@
+{
+ "endpoints": [
+ {
+ "id": {
+ "path": "/stream",
+ "method": "POST",
+ "identifier_override": "endpoint_completions.stream"
+ },
+ "snippet": {
+ "type": "go",
+ "client": "import (\n\tcontext \"context\"\n\tssego \"github.com/fern-api/sse-go\"\n\tssegoclient \"github.com/fern-api/sse-go/client\"\n)\n\nclient := ssegoclient.NewClient()\nresponse, err := client.Completions.Stream(\n\tcontext.TODO(),\n\t\u0026ssego.StreamCompletionRequest{\n\t\tQuery: \"query\",\n\t},\n)\n"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/docker-compose.test.yml b/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/docker-compose.test.yml
new file mode 100644
index 000000000000..b65fc8855e0a
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/docker-compose.test.yml
@@ -0,0 +1,8 @@
+services:
+ wiremock:
+ image: wiremock/wiremock:3.9.1
+ ports:
+ - "8080:8080"
+ volumes:
+ - ./wiremock-mappings.json:/home/wiremock/mappings/wiremock-mappings.json
+ command: ["--global-response-templating", "--verbose"]
diff --git a/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/wiremock-mappings.json b/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/wiremock-mappings.json
new file mode 100644
index 000000000000..db787e413c13
--- /dev/null
+++ b/seed/go-sdk/server-sent-events/with-wire-tests/wiremock/wiremock-mappings.json
@@ -0,0 +1 @@
+{"mappings":[{"id":"07afdcff-a307-475a-b81a-89ebf2b474b2","name":"stream - default","request":{"urlPathTemplate":"/stream","method":"POST"},"response":{"status":200,"body":"event: message\ndata: {\"delta\":\"delta\",\"tokens\":1}\n","headers":{"Content-Type":"text/event-stream"}},"uuid":"07afdcff-a307-475a-b81a-89ebf2b474b2","persistent":true,"priority":3,"metadata":{"mocklab":{"created":{"at":"2020-01-01T00:00:00.000Z","via":"SYSTEM"}}}}],"meta":{"total":1}}
\ No newline at end of file
diff --git a/seed/go-sdk/simple-api/internal/query.go b/seed/go-sdk/simple-api/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/simple-api/internal/query.go
+++ b/seed/go-sdk/simple-api/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/simple-api/internal/query_test.go b/seed/go-sdk/simple-api/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/simple-api/internal/query_test.go
+++ b/seed/go-sdk/simple-api/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/simple-fhir/internal/query.go b/seed/go-sdk/simple-fhir/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/simple-fhir/internal/query.go
+++ b/seed/go-sdk/simple-fhir/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/simple-fhir/internal/query_test.go b/seed/go-sdk/simple-fhir/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/simple-fhir/internal/query_test.go
+++ b/seed/go-sdk/simple-fhir/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/single-url-environment-default/internal/query.go b/seed/go-sdk/single-url-environment-default/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/single-url-environment-default/internal/query.go
+++ b/seed/go-sdk/single-url-environment-default/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/single-url-environment-default/internal/query_test.go b/seed/go-sdk/single-url-environment-default/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/single-url-environment-default/internal/query_test.go
+++ b/seed/go-sdk/single-url-environment-default/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/single-url-environment-no-default/internal/query.go b/seed/go-sdk/single-url-environment-no-default/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/single-url-environment-no-default/internal/query.go
+++ b/seed/go-sdk/single-url-environment-no-default/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/single-url-environment-no-default/internal/query_test.go b/seed/go-sdk/single-url-environment-no-default/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/single-url-environment-no-default/internal/query_test.go
+++ b/seed/go-sdk/single-url-environment-no-default/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/streaming/.fern/metadata.json b/seed/go-sdk/streaming/.fern/metadata.json
new file mode 100644
index 000000000000..abf3778e9da9
--- /dev/null
+++ b/seed/go-sdk/streaming/.fern/metadata.json
@@ -0,0 +1,12 @@
+{
+ "cliVersion": "DUMMY",
+ "generatorName": "fernapi/fern-go-sdk",
+ "generatorVersion": "latest",
+ "generatorConfig": {
+ "enableWireTests": false,
+ "packageName": "stream",
+ "module": {
+ "path": "github.com/fern-api/stream-go"
+ }
+ }
+}
\ No newline at end of file
diff --git a/seed/go-sdk/streaming/.github/workflows/ci.yml b/seed/go-sdk/streaming/.github/workflows/ci.yml
new file mode 100644
index 000000000000..56310d69624b
--- /dev/null
+++ b/seed/go-sdk/streaming/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on: [push]
+
+jobs:
+ compile:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Compile
+ run: go build ./...
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Set up go
+ uses: actions/setup-go@v4
+
+ - name: Setup wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
+
+ - name: Test
+ run: go test ./...
+
+ - name: Teardown wiremock server
+ run: |
+ if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/streaming/client/client.go b/seed/go-sdk/streaming/client/client.go
new file mode 100644
index 000000000000..902525e73215
--- /dev/null
+++ b/seed/go-sdk/streaming/client/client.go
@@ -0,0 +1,33 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ core "github.com/fern-api/stream-go/v2/core"
+ dummy "github.com/fern-api/stream-go/v2/dummy"
+ internal "github.com/fern-api/stream-go/v2/internal"
+ option "github.com/fern-api/stream-go/v2/option"
+)
+
+type Client struct {
+ Dummy *dummy.Client
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(opts ...option.RequestOption) *Client {
+ options := core.NewRequestOptions(opts...)
+ return &Client{
+ Dummy: dummy.NewClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
diff --git a/seed/go-sdk/streaming/client/client_test.go b/seed/go-sdk/streaming/client/client_test.go
new file mode 100644
index 000000000000..6b0ee5caf60e
--- /dev/null
+++ b/seed/go-sdk/streaming/client/client_test.go
@@ -0,0 +1,45 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package client
+
+import (
+ option "github.com/fern-api/stream-go/v2/option"
+ assert "github.com/stretchr/testify/assert"
+ http "net/http"
+ testing "testing"
+ time "time"
+)
+
+func TestNewClient(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ c := NewClient()
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("base url", func(t *testing.T) {
+ c := NewClient(
+ option.WithBaseURL("test.co"),
+ )
+ assert.Equal(t, "test.co", c.baseURL)
+ })
+
+ t.Run("http client", func(t *testing.T) {
+ httpClient := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+ c := NewClient(
+ option.WithHTTPClient(httpClient),
+ )
+ assert.Empty(t, c.baseURL)
+ })
+
+ t.Run("http header", func(t *testing.T) {
+ header := make(http.Header)
+ header.Set("X-API-Tenancy", "test")
+ c := NewClient(
+ option.WithHTTPHeader(header),
+ )
+ assert.Empty(t, c.baseURL)
+ assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
+ })
+}
diff --git a/seed/go-sdk/streaming/core/api_error.go b/seed/go-sdk/streaming/core/api_error.go
new file mode 100644
index 000000000000..6168388541b4
--- /dev/null
+++ b/seed/go-sdk/streaming/core/api_error.go
@@ -0,0 +1,47 @@
+package core
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// APIError is a lightweight wrapper around the standard error
+// interface that preserves the status code from the RPC, if any.
+type APIError struct {
+ err error
+
+ StatusCode int `json:"-"`
+ Header http.Header `json:"-"`
+}
+
+// NewAPIError constructs a new API error.
+func NewAPIError(statusCode int, header http.Header, err error) *APIError {
+ return &APIError{
+ err: err,
+ Header: header,
+ StatusCode: statusCode,
+ }
+}
+
+// Unwrap returns the underlying error. This also makes the error compatible
+// with errors.As and errors.Is.
+func (a *APIError) Unwrap() error {
+ if a == nil {
+ return nil
+ }
+ return a.err
+}
+
+// Error returns the API error's message.
+func (a *APIError) Error() string {
+ if a == nil || (a.err == nil && a.StatusCode == 0) {
+ return ""
+ }
+ if a.err == nil {
+ return fmt.Sprintf("%d", a.StatusCode)
+ }
+ if a.StatusCode == 0 {
+ return a.err.Error()
+ }
+ return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
+}
diff --git a/seed/go-sdk/streaming/core/http.go b/seed/go-sdk/streaming/core/http.go
new file mode 100644
index 000000000000..92c435692940
--- /dev/null
+++ b/seed/go-sdk/streaming/core/http.go
@@ -0,0 +1,15 @@
+package core
+
+import "net/http"
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Response is an HTTP response from an HTTP client.
+type Response[T any] struct {
+ StatusCode int
+ Header http.Header
+ Body T
+}
diff --git a/seed/go-sdk/streaming/core/request_option.go b/seed/go-sdk/streaming/core/request_option.go
new file mode 100644
index 000000000000..119c02bfe449
--- /dev/null
+++ b/seed/go-sdk/streaming/core/request_option.go
@@ -0,0 +1,109 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package core
+
+import (
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of the client or an individual request.
+type RequestOption interface {
+ applyRequestOptions(*RequestOptions)
+}
+
+// RequestOptions defines all of the possible request options.
+//
+// This type is primarily used by the generated code and is not meant
+// to be used directly; use the option package instead.
+type RequestOptions struct {
+ BaseURL string
+ HTTPClient HTTPClient
+ HTTPHeader http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ MaxAttempts uint
+}
+
+// NewRequestOptions returns a new *RequestOptions value.
+//
+// This function is primarily used by the generated code and is not meant
+// to be used directly; use RequestOption instead.
+func NewRequestOptions(opts ...RequestOption) *RequestOptions {
+ options := &RequestOptions{
+ HTTPHeader: make(http.Header),
+ BodyProperties: make(map[string]interface{}),
+ QueryParameters: make(url.Values),
+ }
+ for _, opt := range opts {
+ opt.applyRequestOptions(options)
+ }
+ return options
+}
+
+// ToHeader maps the configured request options into a http.Header used
+// for the request(s).
+func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() }
+
+func (r *RequestOptions) cloneHeader() http.Header {
+ headers := r.HTTPHeader.Clone()
+ headers.Set("X-Fern-Language", "Go")
+ headers.Set("X-Fern-SDK-Name", "github.com/fern-api/stream-go/v2")
+ headers.Set("X-Fern-SDK-Version", "v2.0.0")
+ headers.Set("User-Agent", "github.com/streaming/fern/v2.0.0")
+ return headers
+}
+
+// BaseURLOption implements the RequestOption interface.
+type BaseURLOption struct {
+ BaseURL string
+}
+
+func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BaseURL = b.BaseURL
+}
+
+// HTTPClientOption implements the RequestOption interface.
+type HTTPClientOption struct {
+ HTTPClient HTTPClient
+}
+
+func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPClient = h.HTTPClient
+}
+
+// HTTPHeaderOption implements the RequestOption interface.
+type HTTPHeaderOption struct {
+ HTTPHeader http.Header
+}
+
+func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
+ opts.HTTPHeader = h.HTTPHeader
+}
+
+// BodyPropertiesOption implements the RequestOption interface.
+type BodyPropertiesOption struct {
+ BodyProperties map[string]interface{}
+}
+
+func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
+ opts.BodyProperties = b.BodyProperties
+}
+
+// QueryParametersOption implements the RequestOption interface.
+type QueryParametersOption struct {
+ QueryParameters url.Values
+}
+
+func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
+ opts.QueryParameters = q.QueryParameters
+}
+
+// MaxAttemptsOption implements the RequestOption interface.
+type MaxAttemptsOption struct {
+ MaxAttempts uint
+}
+
+func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
+ opts.MaxAttempts = m.MaxAttempts
+}
diff --git a/seed/go-sdk/streaming/core/stream.go b/seed/go-sdk/streaming/core/stream.go
new file mode 100644
index 000000000000..25c528e89516
--- /dev/null
+++ b/seed/go-sdk/streaming/core/stream.go
@@ -0,0 +1,368 @@
+package core
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "slices"
+ "strings"
+)
+
+type StreamFormat string
+
+const (
+ StreamFormatSSE StreamFormat = "sse"
+ StreamFormatEmpty StreamFormat = ""
+)
+
+const (
+ sseEventSeparator = "\n\n"
+ sseLineSeparator = "\n"
+)
+
+const (
+ defaultMaxBufSize = 64 * 1024 // 64KB
+)
+
+// Stream represents a stream of messages sent from a server.
+type Stream[T any] struct {
+ reader streamReader
+ closer io.Closer
+}
+
+// StreamOption adapts the behavior of the Stream.
+type StreamOption func(*streamOptions)
+
+// WithDelimiter overrides the delimiter for the Stream.
+//
+// By default, the Stream is newline-delimited.
+func WithDelimiter(delimiter string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.delimiter = delimiter
+ }
+}
+
+// WithPrefix overrides the prefix for the Stream.
+//
+// By default, the Stream doesn't have a prefix.
+func WithPrefix(prefix string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.prefix = prefix
+ }
+}
+
+// WithTerminator overrides the terminator for the Stream.
+//
+// By default, the Stream terminates on EOF.
+func WithTerminator(terminator string) StreamOption {
+ return func(opts *streamOptions) {
+ opts.terminator = terminator
+ }
+}
+
+// WithFormat overrides the isSSE flag for the Stream.
+//
+// By default, the Stream is not SSE.
+func WithFormat(format StreamFormat) StreamOption {
+ return func(opts *streamOptions) {
+ opts.format = format
+ }
+}
+
+// NewStream constructs a new Stream from the given *http.Response.
+func NewStream[T any](response *http.Response, opts ...StreamOption) *Stream[T] {
+ options := new(streamOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ return &Stream[T]{
+ reader: newStreamReader(response.Body, options),
+ closer: response.Body,
+ }
+}
+
+// Recv reads a message from the stream, returning io.EOF when
+// all the messages have been read.
+func (s Stream[T]) Recv() (T, error) {
+ var value T
+ bytes, err := s.reader.ReadFromStream()
+ if err != nil {
+ return value, err
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return value, err
+ }
+ return value, nil
+}
+
+// Close closes the Stream.
+func (s Stream[T]) Close() error {
+ return s.closer.Close()
+}
+
+// streamReader reads data from a stream.
+type streamReader interface {
+ ReadFromStream() ([]byte, error)
+}
+
+// newStreamReader returns a new streamReader based on the given
+// delimiter.
+//
+// By default, the streamReader uses a simple a *bufio.Reader
+// which splits on newlines, and otherwise use a *bufio.Scanner to
+// split on custom delimiters.
+func newStreamReader(reader io.Reader, options *streamOptions) streamReader {
+ if !options.isEmpty() {
+ if options.maxBufSize == 0 {
+ options.maxBufSize = defaultMaxBufSize
+ }
+ if options.format == StreamFormatSSE {
+ return newSseStreamReader(reader, options)
+ }
+ return newScannerStreamReader(reader, options)
+ }
+ return newBufferStreamReader(reader)
+}
+
+// BufferStreamReader reads data from a *bufio.Reader, which splits
+// on newlines.
+type BufferStreamReader struct {
+ reader *bufio.Reader
+}
+
+func newBufferStreamReader(reader io.Reader) *BufferStreamReader {
+ return &BufferStreamReader{
+ reader: bufio.NewReader(reader),
+ }
+}
+
+func (b *BufferStreamReader) ReadFromStream() ([]byte, error) {
+ line, err := b.reader.ReadBytes('\n')
+ if err != nil {
+ return nil, err
+ }
+ // Strip the trailing newline
+ return bytes.TrimSuffix(line, []byte("\n")), nil
+}
+
+// ScannerStreamReader reads data from a *bufio.Scanner, which allows for
+// configurable delimiters.
+type ScannerStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newScannerStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *ScannerStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &ScannerStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Split(func(bytes []byte, atEOF bool) (int, []byte, error) {
+ if atEOF && len(bytes) == 0 {
+ return 0, nil, nil
+ }
+ n, data, err := stream.parse(bytes)
+ if stream.isTerminated(data) {
+ return 0, nil, io.EOF
+ }
+ return n, data, err
+ })
+ return stream
+}
+
+func (s *ScannerStreamReader) ReadFromStream() ([]byte, error) {
+ if s.scanner.Scan() {
+ return s.scanner.Bytes(), nil
+ }
+ if err := s.scanner.Err(); err != nil {
+ return nil, err
+ }
+ return nil, io.EOF
+}
+
+func (s *ScannerStreamReader) parse(bytes []byte) (int, []byte, error) {
+ var startIndex int
+ if s.options != nil && s.options.prefix != "" {
+ if i := strings.Index(string(bytes), s.options.prefix); i >= 0 {
+ startIndex = i + len(s.options.prefix)
+ }
+ }
+ data := bytes[startIndex:]
+ lineDelimiter := s.options.getLineDelimiter()
+ delimIndex := strings.Index(string(data), lineDelimiter)
+ if delimIndex < 0 {
+ return startIndex + len(data), data, nil
+ }
+ endIndex := delimIndex + len(lineDelimiter)
+ parsedData := data[:endIndex]
+ n := startIndex + endIndex
+ return n, parsedData, nil
+}
+
+func (s *ScannerStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+type streamOptions struct {
+ delimiter string
+ prefix string
+ terminator string
+ format StreamFormat
+ maxBufSize int
+}
+
+func (s *streamOptions) isEmpty() bool {
+ return s.delimiter == "" && s.prefix == "" && s.terminator == "" && s.format == StreamFormatEmpty
+}
+
+func (s *streamOptions) getLineDelimiter() string {
+ if s.delimiter != "" {
+ return s.delimiter
+ }
+ return sseLineSeparator
+}
+
+type SseStreamReader struct {
+ scanner *bufio.Scanner
+ options *streamOptions
+}
+
+func newSseStreamReader(
+ reader io.Reader,
+ options *streamOptions,
+) *SseStreamReader {
+ scanner := bufio.NewScanner(reader)
+ stream := &SseStreamReader{
+ scanner: scanner,
+ options: options,
+ }
+ scanner.Buffer(make([]byte, slices.Min([]int{4096, options.maxBufSize})), options.maxBufSize)
+
+ // Configure scanner to split on SSE event separator (\n\n)
+ // This is fixed by the SSE specification and cannot be changed
+ scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ // SSE messages are always separated by blank lines (\n\n)
+ if i := strings.Index(string(data), sseEventSeparator); i >= 0 {
+ return i + len(sseEventSeparator), data[0:i], nil
+ }
+
+ if atEOF || stream.isTerminated(data) {
+ return len(data), data, nil
+ }
+ return 0, nil, nil
+ })
+ return stream
+}
+
+func (s *SseStreamReader) isTerminated(bytes []byte) bool {
+ if s.options == nil || s.options.terminator == "" {
+ return false
+ }
+ return strings.Contains(string(bytes), s.options.terminator)
+}
+
+func (s *SseStreamReader) ReadFromStream() ([]byte, error) {
+
+ event, err := s.nextEvent()
+ if err != nil {
+ return nil, err
+ }
+ return event.data, nil
+}
+
+func (s *SseStreamReader) nextEvent() (*SseEvent, error) {
+
+ event := SseEvent{}
+ if s.scanner.Scan() {
+ rawEvent := s.scanner.Bytes()
+
+ // Parse individual lines within the SSE message
+ // Lines are always separated by \n within a message (SSE specification)
+ lines := strings.Split(string(rawEvent), sseLineSeparator)
+ for _, line := range lines {
+ s.parseSseLine([]byte(line), &event)
+ }
+
+ if event.size() > s.options.maxBufSize {
+ return nil, errors.New("SseStreamReader.ReadFromStream: buffer limit exceeded")
+ }
+ return &event, nil
+ }
+ return &event, io.EOF
+}
+
+func (s *SseStreamReader) parseSseLine(_bytes []byte, event *SseEvent) {
+ // Try to parse with space first (standard format), then without space (lenient format)
+ if value, ok := s.tryParseField(_bytes, sseDataPrefix, sseDataPrefixNoSpace); ok {
+ if len(event.data) > 0 {
+ // Join multiple data: lines using the configured delimiter
+ // This allows customization of how multi-line data is concatenated:
+ // - "\n" (default): preserves line breaks for multi-line JSON
+ // - "": concatenates without separator
+ // - Any other string: custom separator
+ lineDelimiter := s.options.getLineDelimiter()
+ event.data = append(event.data, lineDelimiter...)
+ }
+ event.data = append(event.data, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseIdPrefix, sseIdPrefixNoSpace); ok {
+ event.id = append(event.id, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseEventPrefix, sseEventPrefixNoSpace); ok {
+ event.event = append(event.event, value...)
+ } else if value, ok := s.tryParseField(_bytes, sseRetryPrefix, sseRetryPrefixNoSpace); ok {
+ event.retry = append(event.retry, value...)
+ }
+}
+
+// tryParseField attempts to parse an SSE field by trying multiple prefix patterns in order.
+// This handles APIs that don't strictly follow the SSE specification by omitting the space after the colon.
+// It tries each prefix in the order provided and returns the value after the first matching prefix.
+func (s *SseStreamReader) tryParseField(line []byte, prefixes ...[]byte) ([]byte, bool) {
+ for _, prefix := range prefixes {
+ if bytes.HasPrefix(line, prefix) {
+ return line[len(prefix):], true
+ }
+ }
+ return nil, false
+}
+
+func (event *SseEvent) size() int {
+ return len(event.id) + len(event.data) + len(event.event) + len(event.retry)
+}
+
+func (event *SseEvent) String() string {
+ return fmt.Sprintf("SseEvent{id: %q, event: %q, data: %q, retry: %q}", event.id, event.event, event.data, event.retry)
+}
+
+type SseEvent struct {
+ id []byte
+ data []byte
+ event []byte
+ retry []byte
+}
+
+var (
+ sseIdPrefix = []byte("id: ")
+ sseDataPrefix = []byte("data: ")
+ sseEventPrefix = []byte("event: ")
+ sseRetryPrefix = []byte("retry: ")
+
+ // Lenient prefixes without space for APIs that don't strictly follow SSE specification
+ sseIdPrefixNoSpace = []byte("id:")
+ sseDataPrefixNoSpace = []byte("data:")
+ sseEventPrefixNoSpace = []byte("event:")
+ sseRetryPrefixNoSpace = []byte("retry:")
+)
diff --git a/seed/go-sdk/streaming/dummy/client.go b/seed/go-sdk/streaming/dummy/client.go
new file mode 100644
index 000000000000..47c2a23f6cce
--- /dev/null
+++ b/seed/go-sdk/streaming/dummy/client.go
@@ -0,0 +1,83 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package dummy
+
+import (
+ context "context"
+ stream "github.com/fern-api/stream-go/v2"
+ core "github.com/fern-api/stream-go/v2/core"
+ internal "github.com/fern-api/stream-go/v2/internal"
+ option "github.com/fern-api/stream-go/v2/option"
+ http "net/http"
+)
+
+type Client struct {
+ WithRawResponse *RawClient
+
+ options *core.RequestOptions
+ baseURL string
+ caller *internal.Caller
+}
+
+func NewClient(options *core.RequestOptions) *Client {
+ return &Client{
+ WithRawResponse: NewRawClient(options),
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (c *Client) GenerateStream(
+ ctx context.Context,
+ request *stream.GenerateStreamRequest,
+ opts ...option.RequestOption,
+) (*core.Stream[stream.StreamResponse], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ c.baseURL,
+ "",
+ )
+ endpointURL := baseURL + "/generate-stream"
+ headers := internal.MergeHeaders(
+ c.options.ToHeader(),
+ options.ToHeader(),
+ )
+ streamer := internal.NewStreamer[stream.StreamResponse](c.caller)
+ return streamer.Stream(
+ ctx,
+ &internal.StreamParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ Request: request,
+ ErrorDecoder: internal.NewErrorDecoder(stream.ErrorCodes),
+ },
+ )
+}
+
+func (c *Client) Generate(
+ ctx context.Context,
+ request *stream.Generateequest,
+ opts ...option.RequestOption,
+) (*stream.StreamResponse, error) {
+ response, err := c.WithRawResponse.Generate(
+ ctx,
+ request,
+ opts...,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return response.Body, nil
+}
diff --git a/seed/go-sdk/streaming/dummy/raw_client.go b/seed/go-sdk/streaming/dummy/raw_client.go
new file mode 100644
index 000000000000..361c67f5907f
--- /dev/null
+++ b/seed/go-sdk/streaming/dummy/raw_client.go
@@ -0,0 +1,72 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package dummy
+
+import (
+ context "context"
+ stream "github.com/fern-api/stream-go/v2"
+ core "github.com/fern-api/stream-go/v2/core"
+ internal "github.com/fern-api/stream-go/v2/internal"
+ option "github.com/fern-api/stream-go/v2/option"
+ http "net/http"
+)
+
+type RawClient struct {
+ baseURL string
+ caller *internal.Caller
+ options *core.RequestOptions
+}
+
+func NewRawClient(options *core.RequestOptions) *RawClient {
+ return &RawClient{
+ options: options,
+ baseURL: options.BaseURL,
+ caller: internal.NewCaller(
+ &internal.CallerParams{
+ Client: options.HTTPClient,
+ MaxAttempts: options.MaxAttempts,
+ },
+ ),
+ }
+}
+
+func (r *RawClient) Generate(
+ ctx context.Context,
+ request *stream.Generateequest,
+ opts ...option.RequestOption,
+) (*core.Response[*stream.StreamResponse], error) {
+ options := core.NewRequestOptions(opts...)
+ baseURL := internal.ResolveBaseURL(
+ options.BaseURL,
+ r.baseURL,
+ "",
+ )
+ endpointURL := baseURL + "/generate"
+ headers := internal.MergeHeaders(
+ r.options.ToHeader(),
+ options.ToHeader(),
+ )
+ var response *stream.StreamResponse
+ raw, err := r.caller.Call(
+ ctx,
+ &internal.CallParams{
+ URL: endpointURL,
+ Method: http.MethodPost,
+ Headers: headers,
+ MaxAttempts: options.MaxAttempts,
+ BodyProperties: options.BodyProperties,
+ QueryParameters: options.QueryParameters,
+ Client: options.HTTPClient,
+ Request: request,
+ Response: &response,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &core.Response[*stream.StreamResponse]{
+ StatusCode: raw.StatusCode,
+ Header: raw.Header,
+ Body: response,
+ }, nil
+}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
new file mode 100644
index 000000000000..cdc45a765996
--- /dev/null
+++ b/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/stream-go/v2/client"
+ option "github.com/fern-api/stream-go/v2/option"
+ stream "github.com/fern-api/stream-go/v2"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &stream.GenerateStreamRequest{
+ NumEvents: 1,
+ }
+ client.Dummy.GenerateStream(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
new file mode 100644
index 000000000000..5a3f2af9383d
--- /dev/null
+++ b/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/stream-go/v2/client"
+ option "github.com/fern-api/stream-go/v2/option"
+ stream "github.com/fern-api/stream-go/v2"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &stream.Generateequest{
+ NumEvents: 5,
+ }
+ client.Dummy.Generate(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
new file mode 100644
index 000000000000..4e83013bdd35
--- /dev/null
+++ b/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
@@ -0,0 +1,23 @@
+package example
+
+import (
+ client "github.com/fern-api/stream-go/v2/client"
+ option "github.com/fern-api/stream-go/v2/option"
+ stream "github.com/fern-api/stream-go/v2"
+ context "context"
+)
+
+func do() {
+ client := client.NewClient(
+ option.WithBaseURL(
+ "https://api.fern.com",
+ ),
+ )
+ request := &stream.Generateequest{
+ NumEvents: 1,
+ }
+ client.Dummy.Generate(
+ context.TODO(),
+ request,
+ )
+}
diff --git a/seed/go-sdk/streaming/internal/caller.go b/seed/go-sdk/streaming/internal/caller.go
new file mode 100644
index 000000000000..53d1c2d54b22
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/caller.go
@@ -0,0 +1,250 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+
+ "github.com/fern-api/stream-go/v2/core"
+)
+
+const (
+ // contentType specifies the JSON Content-Type header value.
+ contentType = "application/json"
+ contentTypeHeader = "Content-Type"
+)
+
+// Caller calls APIs and deserializes their response, if any.
+type Caller struct {
+ client core.HTTPClient
+ retrier *Retrier
+}
+
+// CallerParams represents the parameters used to constrcut a new *Caller.
+type CallerParams struct {
+ Client core.HTTPClient
+ MaxAttempts uint
+}
+
+// NewCaller returns a new *Caller backed by the given parameters.
+func NewCaller(params *CallerParams) *Caller {
+ var httpClient core.HTTPClient = http.DefaultClient
+ if params.Client != nil {
+ httpClient = params.Client
+ }
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+ return &Caller{
+ client: httpClient,
+ retrier: NewRetrier(retryOptions...),
+ }
+}
+
+// CallParams represents the parameters used to issue an API call.
+type CallParams struct {
+ URL string
+ Method string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client core.HTTPClient
+ Request interface{}
+ Response interface{}
+ ResponseIsOptional bool
+ ErrorDecoder ErrorDecoder
+}
+
+// CallResponse is a parsed HTTP response from an API call.
+type CallResponse struct {
+ StatusCode int
+ Header http.Header
+}
+
+// Call issues an API call according to the given call parameters.
+func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := c.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := c.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Close the response body after we're done.
+ defer resp.Body.Close()
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ // Mutate the response parameter in-place.
+ if params.Response != nil {
+ if writer, ok := params.Response.(io.Writer); ok {
+ _, err = io.Copy(writer, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(params.Response)
+ }
+ if err != nil {
+ if err == io.EOF {
+ if params.ResponseIsOptional {
+ // The response is optional, so we should ignore the
+ // io.EOF error
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+ }
+ return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
+ }
+ return nil, err
+ }
+ }
+
+ return &CallResponse{
+ StatusCode: resp.StatusCode,
+ Header: resp.Header,
+ }, nil
+}
+
+// buildURL constructs the final URL by appending the given query parameters (if any).
+func buildURL(
+ url string,
+ queryParameters url.Values,
+) string {
+ if len(queryParameters) == 0 {
+ return url
+ }
+ if strings.ContainsRune(url, '?') {
+ url += "&"
+ } else {
+ url += "?"
+ }
+ url += queryParameters.Encode()
+ return url
+}
+
+// newRequest returns a new *http.Request with all of the fields
+// required to issue the call.
+func newRequest(
+ ctx context.Context,
+ url string,
+ method string,
+ endpointHeaders http.Header,
+ request interface{},
+ bodyProperties map[string]interface{},
+) (*http.Request, error) {
+ requestBody, err := newRequestBody(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ req.Header.Set(contentTypeHeader, contentType)
+ for name, values := range endpointHeaders {
+ req.Header[name] = values
+ }
+ return req, nil
+}
+
+// newRequestBody returns a new io.Reader that represents the HTTP request body.
+func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
+ if isNil(request) {
+ if len(bodyProperties) == 0 {
+ return nil, nil
+ }
+ requestBytes, err := json.Marshal(bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+ }
+ if body, ok := request.(io.Reader); ok {
+ return body, nil
+ }
+ requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(requestBytes), nil
+}
+
+// decodeError decodes the error from the given HTTP response. Note that
+// it's the caller's responsibility to close the response body.
+func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
+ if errorDecoder != nil {
+ // This endpoint has custom errors, so we'll
+ // attempt to unmarshal the error into a structured
+ // type based on the status code.
+ return errorDecoder(response.StatusCode, response.Header, response.Body)
+ }
+ // This endpoint doesn't have any custom error
+ // types, so we just read the body as-is, and
+ // put it into a normal error.
+ bytes, err := io.ReadAll(response.Body)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if err == io.EOF {
+ // The error didn't have a response body,
+ // so all we can do is return an error
+ // with the status code.
+ return core.NewAPIError(response.StatusCode, response.Header, nil)
+ }
+ return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
+}
+
+// isNil is used to determine if the request value is equal to nil (i.e. an interface
+// value that holds a nil concrete value is itself non-nil).
+func isNil(value interface{}) bool {
+ return value == nil || reflect.ValueOf(value).IsNil()
+}
diff --git a/seed/go-sdk/streaming/internal/caller_test.go b/seed/go-sdk/streaming/internal/caller_test.go
new file mode 100644
index 000000000000..bab98eb18836
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/caller_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/fern-api/stream-go/v2/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// InternalTestCase represents a single test case.
+type InternalTestCase struct {
+ description string
+
+ // Server-side assertions.
+ givePathSuffix string
+ giveMethod string
+ giveResponseIsOptional bool
+ giveHeader http.Header
+ giveErrorDecoder ErrorDecoder
+ giveRequest *InternalTestRequest
+ giveQueryParams url.Values
+ giveBodyProperties map[string]interface{}
+
+ // Client-side assertions.
+ wantResponse *InternalTestResponse
+ wantHeaders http.Header
+ wantError error
+}
+
+// InternalTestRequest a simple request body.
+type InternalTestRequest struct {
+ Id string `json:"id"`
+}
+
+// InternalTestResponse a simple response body.
+type InternalTestResponse struct {
+ Id string `json:"id"`
+ ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
+ QueryParameters url.Values `json:"queryParameters,omitempty"`
+}
+
+// InternalTestNotFoundError represents a 404.
+type InternalTestNotFoundError struct {
+ *core.APIError
+
+ Message string `json:"message"`
+}
+
+func TestCall(t *testing.T) {
+ tests := []*InternalTestCase{
+ {
+ description: "GET success",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ },
+ },
+ {
+ description: "GET success with query",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ },
+ },
+ },
+ {
+ description: "GET not found",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusNotFound),
+ },
+ giveErrorDecoder: newTestErrorDecoder(t),
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(
+ http.StatusNotFound,
+ http.Header{},
+ errors.New(`{"message":"ID \"404\" not found"}`),
+ ),
+ },
+ },
+ {
+ description: "POST empty body",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: nil,
+ wantError: core.NewAPIError(
+ http.StatusBadRequest,
+ http.Header{},
+ errors.New("invalid request"),
+ ),
+ },
+ {
+ description: "POST optional response",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveResponseIsOptional: true,
+ },
+ {
+ description: "POST API error",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"fail"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: strconv.Itoa(http.StatusInternalServerError),
+ },
+ wantError: core.NewAPIError(
+ http.StatusInternalServerError,
+ http.Header{},
+ errors.New("failed to process request"),
+ ),
+ },
+ {
+ description: "POST extra properties",
+ giveMethod: http.MethodPost,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: new(InternalTestRequest),
+ giveBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ wantResponse: &InternalTestResponse{
+ ExtraBodyProperties: map[string]interface{}{
+ "key": "value",
+ },
+ },
+ },
+ {
+ description: "GET extra query parameters",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "extra": []string{"true"},
+ },
+ },
+ },
+ {
+ description: "GET merge extra query parameters",
+ givePathSuffix: "?limit=1",
+ giveMethod: http.MethodGet,
+ giveHeader: http.Header{
+ "X-API-Status": []string{"success"},
+ },
+ giveRequest: &InternalTestRequest{
+ Id: "123",
+ },
+ giveQueryParams: url.Values{
+ "extra": []string{"true"},
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "123",
+ QueryParameters: url.Values{
+ "limit": []string{"1"},
+ "extra": []string{"true"},
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.description, func(t *testing.T) {
+ var (
+ server = newTestServer(t, test)
+ client = server.Client()
+ )
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL + test.givePathSuffix,
+ Method: test.giveMethod,
+ Headers: test.giveHeader,
+ BodyProperties: test.giveBodyProperties,
+ QueryParameters: test.giveQueryParams,
+ Request: test.giveRequest,
+ Response: &response,
+ ResponseIsOptional: test.giveResponseIsOptional,
+ ErrorDecoder: test.giveErrorDecoder,
+ },
+ )
+ if test.wantError != nil {
+ assert.EqualError(t, err, test.wantError.Error())
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+func TestMergeHeaders(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ merged := MergeHeaders(make(http.Header), make(http.Header))
+ assert.Empty(t, merged)
+ })
+
+ t.Run("empty left", func(t *testing.T) {
+ left := make(http.Header)
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("empty right", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.1")
+
+ right := make(http.Header)
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
+ })
+
+ t.Run("single value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Version", "0.0.0")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+
+ t.Run("multiple value override", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Versions", "0.0.0")
+
+ right := make(http.Header)
+ right.Add("X-API-Versions", "0.0.1")
+ right.Add("X-API-Versions", "0.0.2")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
+ })
+
+ t.Run("disjoint merge", func(t *testing.T) {
+ left := make(http.Header)
+ left.Set("X-API-Tenancy", "test")
+
+ right := make(http.Header)
+ right.Set("X-API-Version", "0.0.1")
+
+ merged := MergeHeaders(left, right)
+ assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
+ assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
+ })
+}
+
+// newTestServer returns a new *httptest.Server configured with the
+// given test parameters.
+func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tc.giveMethod, r.Method)
+ assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
+ for header, value := range tc.giveHeader {
+ assert.Equal(t, value, r.Header.Values(header))
+ }
+
+ request := new(InternalTestRequest)
+
+ bytes, err := io.ReadAll(r.Body)
+ if tc.giveRequest == nil {
+ require.Empty(t, bytes)
+ w.WriteHeader(http.StatusBadRequest)
+ _, err = w.Write([]byte("invalid request"))
+ require.NoError(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+
+ switch request.Id {
+ case strconv.Itoa(http.StatusNotFound):
+ notFoundError := &InternalTestNotFoundError{
+ APIError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ Message: fmt.Sprintf("ID %q not found", request.Id),
+ }
+ bytes, err = json.Marshal(notFoundError)
+ require.NoError(t, err)
+
+ w.WriteHeader(http.StatusNotFound)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ return
+
+ case strconv.Itoa(http.StatusInternalServerError):
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = w.Write([]byte("failed to process request"))
+ require.NoError(t, err)
+ return
+ }
+
+ if tc.giveResponseIsOptional {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ extraBodyProperties := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
+ delete(extraBodyProperties, "id")
+
+ response := &InternalTestResponse{
+ Id: request.Id,
+ ExtraBodyProperties: extraBodyProperties,
+ QueryParameters: r.URL.Query(),
+ }
+ bytes, err = json.Marshal(response)
+ require.NoError(t, err)
+
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ },
+ ),
+ )
+}
+
+// newTestErrorDecoder returns an error decoder suitable for tests.
+func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ require.NoError(t, err)
+
+ var (
+ apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
+ decoder = json.NewDecoder(bytes.NewReader(raw))
+ )
+ if statusCode == http.StatusNotFound {
+ value := new(InternalTestNotFoundError)
+ value.APIError = apiError
+ require.NoError(t, decoder.Decode(value))
+
+ return value
+ }
+ return apiError
+ }
+}
diff --git a/seed/go-sdk/streaming/internal/error_decoder.go b/seed/go-sdk/streaming/internal/error_decoder.go
new file mode 100644
index 000000000000..e04ba8093068
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/error_decoder.go
@@ -0,0 +1,64 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/fern-api/stream-go/v2/core"
+)
+
+// ErrorCodes maps HTTP status codes to error constructors.
+type ErrorCodes map[int]func(*core.APIError) error
+
+// ErrorDecoder decodes *http.Response errors and returns a
+// typed API error (e.g. *core.APIError).
+type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
+
+// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
+// errorCodesOverrides is optional and will be merged with the default error codes,
+// with overrides taking precedence.
+func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
+ // Merge default error codes with overrides
+ mergedErrorCodes := make(ErrorCodes)
+
+ // Start with default error codes
+ for statusCode, errorFunc := range errorCodes {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+
+ // Apply overrides if provided
+ if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
+ for statusCode, errorFunc := range errorCodesOverrides[0] {
+ mergedErrorCodes[statusCode] = errorFunc
+ }
+ }
+
+ return func(statusCode int, header http.Header, body io.Reader) error {
+ raw, err := io.ReadAll(body)
+ if err != nil {
+ return fmt.Errorf("failed to read error from response body: %w", err)
+ }
+ apiError := core.NewAPIError(
+ statusCode,
+ header,
+ errors.New(string(raw)),
+ )
+ newErrorFunc, ok := mergedErrorCodes[statusCode]
+ if !ok {
+ // This status code isn't recognized, so we return
+ // the API error as-is.
+ return apiError
+ }
+ customError := newErrorFunc(apiError)
+ if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
+ // If we fail to decode the error, we return the
+ // API error as-is.
+ return apiError
+ }
+ return customError
+ }
+}
diff --git a/seed/go-sdk/streaming/internal/error_decoder_test.go b/seed/go-sdk/streaming/internal/error_decoder_test.go
new file mode 100644
index 000000000000..604c09120ac5
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/error_decoder_test.go
@@ -0,0 +1,59 @@
+package internal
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/fern-api/stream-go/v2/core"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorDecoder(t *testing.T) {
+ decoder := NewErrorDecoder(
+ ErrorCodes{
+ http.StatusNotFound: func(apiError *core.APIError) error {
+ return &InternalTestNotFoundError{APIError: apiError}
+ },
+ })
+
+ tests := []struct {
+ description string
+ giveStatusCode int
+ giveHeader http.Header
+ giveBody string
+ wantError error
+ }{
+ {
+ description: "unrecognized status code",
+ giveStatusCode: http.StatusInternalServerError,
+ giveHeader: http.Header{},
+ giveBody: "Internal Server Error",
+ wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
+ },
+ {
+ description: "not found with valid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `{"message": "Resource not found"}`,
+ wantError: &InternalTestNotFoundError{
+ APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
+ Message: "Resource not found",
+ },
+ },
+ {
+ description: "not found with invalid JSON",
+ giveStatusCode: http.StatusNotFound,
+ giveHeader: http.Header{},
+ giveBody: `Resource not found`,
+ wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
+ })
+ }
+}
diff --git a/seed/go-sdk/streaming/internal/explicit_fields.go b/seed/go-sdk/streaming/internal/explicit_fields.go
new file mode 100644
index 000000000000..4bdf34fc2b7c
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/explicit_fields.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+ "math/big"
+ "reflect"
+ "strings"
+)
+
+// HandleExplicitFields processes a struct to remove `omitempty` from
+// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
+// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
+// Returns an interface{} that can be passed to json.Marshal.
+func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
+ val := reflect.ValueOf(marshaler)
+ typ := reflect.TypeOf(marshaler)
+
+ // Handle pointer types
+ if val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil
+ }
+ val = val.Elem()
+ typ = typ.Elem()
+ }
+
+ // Only handle struct types
+ if val.Kind() != reflect.Struct {
+ return marshaler
+ }
+
+ // Handle embedded struct pattern
+ var sourceVal reflect.Value
+ var sourceType reflect.Type
+
+ // Check if this is an embedded struct pattern
+ if typ.NumField() == 1 && typ.Field(0).Anonymous {
+ // This is likely an embedded struct, get the embedded value
+ embeddedField := val.Field(0)
+ sourceVal = embeddedField
+ sourceType = embeddedField.Type()
+ } else {
+ // Regular struct
+ sourceVal = val
+ sourceType = typ
+ }
+
+ // If no explicit fields set, use standard marshaling
+ if explicitFields == nil || explicitFields.Sign() == 0 {
+ return marshaler
+ }
+
+ // Create a new struct type with modified tags
+ fields := make([]reflect.StructField, 0, sourceType.NumField())
+
+ for i := 0; i < sourceType.NumField(); i++ {
+ field := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !field.IsExported() || field.Name == "explicitFields" {
+ continue
+ }
+
+ // Check if this field has been explicitly set
+ fieldBit := big.NewInt(1)
+ fieldBit.Lsh(fieldBit, uint(i))
+ if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
+ // Remove omitempty from the json tag
+ tag := field.Tag.Get("json")
+ if tag != "" && tag != "-" {
+ // Parse the json tag, remove omitempty from options
+ parts := strings.Split(tag, ",")
+ if len(parts) > 1 {
+ var newParts []string
+ newParts = append(newParts, parts[0]) // Keep the field name
+ for _, part := range parts[1:] {
+ if strings.TrimSpace(part) != "omitempty" {
+ newParts = append(newParts, part)
+ }
+ }
+ tag = strings.Join(newParts, ",")
+ }
+
+ // Reconstruct the struct tag
+ newTag := `json:"` + tag + `"`
+ if urlTag := field.Tag.Get("url"); urlTag != "" {
+ newTag += ` url:"` + urlTag + `"`
+ }
+
+ field.Tag = reflect.StructTag(newTag)
+ }
+ }
+
+ fields = append(fields, field)
+ }
+
+ // Create new struct type with modified tags
+ newType := reflect.StructOf(fields)
+ newVal := reflect.New(newType).Elem()
+
+ // Copy field values from original struct to new struct
+ fieldIndex := 0
+ for i := 0; i < sourceType.NumField(); i++ {
+ originalField := sourceType.Field(i)
+
+ // Skip unexported fields and the explicitFields field itself
+ if !originalField.IsExported() || originalField.Name == "explicitFields" {
+ continue
+ }
+
+ originalValue := sourceVal.Field(i)
+ newVal.Field(fieldIndex).Set(originalValue)
+ fieldIndex++
+ }
+
+ return newVal.Interface()
+}
diff --git a/seed/go-sdk/streaming/internal/explicit_fields_test.go b/seed/go-sdk/streaming/internal/explicit_fields_test.go
new file mode 100644
index 000000000000..3d05e88a2ce9
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/explicit_fields_test.go
@@ -0,0 +1,497 @@
+package internal
+
+import (
+ "encoding/json"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testExplicitFieldsStruct struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+ Count *int `json:"count,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ //lint:ignore unused this field is intentionally unused for testing
+ unexported string `json:"-"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ testFieldName = big.NewInt(1 << 0)
+ testFieldCode = big.NewInt(1 << 1)
+ testFieldCount = big.NewInt(1 << 2)
+ testFieldEnabled = big.NewInt(1 << 3)
+ testFieldTags = big.NewInt(1 << 4)
+)
+
+func (t *testExplicitFieldsStruct) require(field *big.Int) {
+ if t.explicitFields == nil {
+ t.explicitFields = big.NewInt(0)
+ }
+ t.explicitFields.Or(t.explicitFields, field)
+}
+
+func (t *testExplicitFieldsStruct) SetName(name *string) {
+ t.Name = name
+ t.require(testFieldName)
+}
+
+func (t *testExplicitFieldsStruct) SetCode(code *string) {
+ t.Code = code
+ t.require(testFieldCode)
+}
+
+func (t *testExplicitFieldsStruct) SetCount(count *int) {
+ t.Count = count
+ t.require(testFieldCount)
+}
+
+func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
+ t.Enabled = enabled
+ t.require(testFieldEnabled)
+}
+
+func (t *testExplicitFieldsStruct) SetTags(tags []string) {
+ t.Tags = tags
+ t.require(testFieldTags)
+}
+
+func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*t),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
+}
+
+type testStructWithoutExplicitFields struct {
+ Name *string `json:"name,omitempty"`
+ Code *string `json:"code,omitempty"`
+}
+
+func TestHandleExplicitFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveInput interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "nil input",
+ giveInput: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "non-struct input",
+ giveInput: "string",
+ wantBytes: []byte(`"string"`),
+ },
+ {
+ desc: "slice input",
+ giveInput: []string{"a", "b"},
+ wantBytes: []byte(`["a","b"]`),
+ },
+ {
+ desc: "map input",
+ giveInput: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "struct without explicitFields field",
+ giveInput: &testStructWithoutExplicitFields{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with no explicit fields set",
+ giveInput: &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ Code: nil,
+ },
+ wantBytes: []byte(`{"name":"test"}`),
+ },
+ {
+ desc: "struct with explicit nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null}`),
+ },
+ {
+ desc: "struct with explicit non-nil field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("explicit"))
+ s.SetCode(stringPtr("also-explicit"))
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
+ },
+ {
+ desc: "struct with mixed explicit and implicit fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Count: intPtr(42),
+ }
+ s.SetCode(nil) // explicit nil
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
+ },
+ {
+ desc: "struct with multiple explicit nil fields",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("test"),
+ }
+ s.SetCode(nil)
+ s.SetCount(nil)
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
+ },
+ {
+ desc: "struct with slice field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{
+ Tags: []string{"tag1", "tag2"},
+ }
+ s.SetTags(nil) // explicit nil slice
+ return s
+ }(),
+ wantBytes: []byte(`{"tags":null}`),
+ },
+ {
+ desc: "struct with boolean field",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetEnabled(boolPtr(false)) // explicit false
+ return s
+ }(),
+ wantBytes: []byte(`{"enabled":false}`),
+ },
+ {
+ desc: "struct with all fields explicit",
+ giveInput: func() *testExplicitFieldsStruct {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(stringPtr("test"))
+ s.SetCode(nil)
+ s.SetCount(intPtr(0))
+ s.SetEnabled(boolPtr(false))
+ s.SetTags([]string{})
+ return s
+ }(),
+ wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ var explicitFields *big.Int
+ if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
+ explicitFields = s.explicitFields
+ }
+ bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
+ t.Run("custom marshaler with explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ bytes, err := s.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
+ t.Run("nil pointer", func(t *testing.T) {
+ var s *testExplicitFieldsStruct
+ bytes, err := json.Marshal(HandleExplicitFields(s, nil))
+ require.NoError(t, err)
+ assert.Equal(t, []byte(`null`), bytes)
+ })
+
+ t.Run("pointer to struct", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"name":null}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
+ t.Run("embedded struct with explicit fields", func(t *testing.T) {
+ // Create a struct similar to what MarshalJSON creates
+ s := &testExplicitFieldsStruct{}
+ s.SetName(nil)
+ s.SetCode(stringPtr("test-code"))
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include both explicit fields (name as null, code as "test-code")
+ assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with no explicit fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Name: stringPtr("implicit"),
+ Code: stringPtr("also-implicit"),
+ }
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should only include non-nil fields (omitempty behavior)
+ assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
+ })
+
+ t.Run("embedded struct with mixed fields", func(t *testing.T) {
+ s := &testExplicitFieldsStruct{
+ Count: intPtr(42), // implicit field
+ }
+ s.SetName(nil) // explicit nil
+ s.SetCode(stringPtr("explicit")) // explicit value
+
+ type embed testExplicitFieldsStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*s),
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
+ require.NoError(t, err)
+ // Should include explicit null, explicit value, and implicit value
+ assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
+ })
+}
+
+func TestHandleExplicitFieldsTagHandling(t *testing.T) {
+ type testStructWithComplexTags struct {
+ Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
+ Field2 *string `json:"field2,omitempty,string" url:"field2"`
+ Field3 *string `json:"-"`
+ Field4 *string `json:"field4"`
+ explicitFields *big.Int `json:"-"`
+ }
+
+ s := &testStructWithComplexTags{
+ Field1: stringPtr("test1"),
+ Field4: stringPtr("test4"),
+ explicitFields: big.NewInt(1), // Only first field is explicit
+ }
+
+ bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
+ require.NoError(t, err)
+
+ // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
+ assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
+}
+
+// Test types for nested struct explicit fields testing
+type testNestedStruct struct {
+ NestedName *string `json:"nested_name,omitempty"`
+ NestedCode *string `json:"nested_code,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+type testParentStruct struct {
+ ParentName *string `json:"parent_name,omitempty"`
+ Nested *testNestedStruct `json:"nested,omitempty"`
+ explicitFields *big.Int `json:"-"`
+}
+
+var (
+ nestedFieldName = big.NewInt(1 << 0)
+ nestedFieldCode = big.NewInt(1 << 1)
+)
+
+var (
+ parentFieldName = big.NewInt(1 << 0)
+ parentFieldNested = big.NewInt(1 << 1)
+)
+
+func (n *testNestedStruct) require(field *big.Int) {
+ if n.explicitFields == nil {
+ n.explicitFields = big.NewInt(0)
+ }
+ n.explicitFields.Or(n.explicitFields, field)
+}
+
+func (n *testNestedStruct) SetNestedName(name *string) {
+ n.NestedName = name
+ n.require(nestedFieldName)
+}
+
+func (n *testNestedStruct) SetNestedCode(code *string) {
+ n.NestedCode = code
+ n.require(nestedFieldCode)
+}
+
+func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
+ type embed testNestedStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*n),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
+}
+
+func (p *testParentStruct) require(field *big.Int) {
+ if p.explicitFields == nil {
+ p.explicitFields = big.NewInt(0)
+ }
+ p.explicitFields.Or(p.explicitFields, field)
+}
+
+func (p *testParentStruct) SetParentName(name *string) {
+ p.ParentName = name
+ p.require(parentFieldName)
+}
+
+func (p *testParentStruct) SetNested(nested *testNestedStruct) {
+ p.Nested = nested
+ p.require(parentFieldNested)
+}
+
+func (p *testParentStruct) MarshalJSON() ([]byte, error) {
+ type embed testParentStruct
+ var marshaler = struct {
+ embed
+ }{
+ embed: embed(*p),
+ }
+ return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
+}
+
+func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
+ tests := []struct {
+ desc string
+ setupFunc func() *testParentStruct
+ wantBytes []byte
+ }{
+ {
+ desc: "nested struct with explicit nil in nested object",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{
+ NestedName: stringPtr("implicit-nested"),
+ }
+ nested.SetNestedCode(nil) // explicit nil
+
+ return &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ Nested: nested,
+ }
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
+ },
+ {
+ desc: "parent with explicit nil nested struct",
+ setupFunc: func() *testParentStruct {
+ parent := &testParentStruct{
+ ParentName: stringPtr("implicit-parent"),
+ }
+ parent.SetNested(nil) // explicit nil nested struct
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
+ },
+ {
+ desc: "all explicit fields in nested structure",
+ setupFunc: func() *testParentStruct {
+ nested := &testNestedStruct{}
+ nested.SetNestedName(stringPtr("explicit-nested"))
+ nested.SetNestedCode(nil) // explicit nil
+
+ parent := &testParentStruct{}
+ parent.SetParentName(nil) // explicit nil
+ parent.SetNested(nested) // explicit nested struct
+
+ return parent
+ },
+ wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ parent := tt.setupFunc()
+ bytes, err := parent.MarshalJSON()
+ require.NoError(t, err)
+ assert.JSONEq(t, string(tt.wantBytes), string(bytes))
+
+ // Verify it's valid JSON
+ var value interface{}
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+// Helper functions
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+func boolPtr(b bool) *bool {
+ return &b
+}
diff --git a/seed/go-sdk/streaming/internal/extra_properties.go b/seed/go-sdk/streaming/internal/extra_properties.go
new file mode 100644
index 000000000000..540c3fd89eeb
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/extra_properties.go
@@ -0,0 +1,141 @@
+package internal
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
+func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
+ return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
+}
+
+// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
+func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
+ bytes, err := json.Marshal(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ if len(extraProperties) == 0 {
+ return bytes, nil
+ }
+ keys, err := getKeys(marshaler)
+ if err != nil {
+ return nil, err
+ }
+ for _, key := range keys {
+ if _, ok := extraProperties[key]; ok {
+ return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
+ }
+ }
+ extraBytes, err := json.Marshal(extraProperties)
+ if err != nil {
+ return nil, err
+ }
+ if isEmptyJSON(bytes) {
+ if isEmptyJSON(extraBytes) {
+ return bytes, nil
+ }
+ return extraBytes, nil
+ }
+ result := bytes[:len(bytes)-1]
+ result = append(result, ',')
+ result = append(result, extraBytes[1:len(extraBytes)-1]...)
+ result = append(result, '}')
+ return result, nil
+}
+
+// ExtractExtraProperties extracts any extra properties from the given value.
+func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
+ val := reflect.ValueOf(value)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return nil, fmt.Errorf("value must be non-nil to extract extra properties")
+ }
+ val = val.Elem()
+ }
+ if err := json.Unmarshal(bytes, &value); err != nil {
+ return nil, err
+ }
+ var extraProperties map[string]interface{}
+ if err := json.Unmarshal(bytes, &extraProperties); err != nil {
+ return nil, err
+ }
+ for i := 0; i < val.Type().NumField(); i++ {
+ key := jsonKey(val.Type().Field(i))
+ if key == "" || key == "-" {
+ continue
+ }
+ delete(extraProperties, key)
+ }
+ for _, key := range exclude {
+ delete(extraProperties, key)
+ }
+ if len(extraProperties) == 0 {
+ return nil, nil
+ }
+ return extraProperties, nil
+}
+
+// getKeys returns the keys associated with the given value. The value must be a
+// a struct or a map with string keys.
+func getKeys(value interface{}) ([]string, error) {
+ val := reflect.ValueOf(value)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if !val.IsValid() {
+ return nil, nil
+ }
+ switch val.Kind() {
+ case reflect.Struct:
+ return getKeysForStructType(val.Type()), nil
+ case reflect.Map:
+ var keys []string
+ if val.Type().Key().Kind() != reflect.String {
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+ for _, key := range val.MapKeys() {
+ keys = append(keys, key.String())
+ }
+ return keys, nil
+ default:
+ return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
+ }
+}
+
+// getKeysForStructType returns all the keys associated with the given struct type,
+// visiting embedded fields recursively.
+func getKeysForStructType(structType reflect.Type) []string {
+ if structType.Kind() == reflect.Pointer {
+ structType = structType.Elem()
+ }
+ if structType.Kind() != reflect.Struct {
+ return nil
+ }
+ var keys []string
+ for i := 0; i < structType.NumField(); i++ {
+ field := structType.Field(i)
+ if field.Anonymous {
+ keys = append(keys, getKeysForStructType(field.Type)...)
+ continue
+ }
+ keys = append(keys, jsonKey(field))
+ }
+ return keys
+}
+
+// jsonKey returns the JSON key from the struct tag of the given field,
+// excluding the omitempty flag (if any).
+func jsonKey(field reflect.StructField) string {
+ return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
+}
+
+// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
+// an explicit null.
+func isEmptyJSON(data []byte) bool {
+ return len(data) <= 2 || bytes.Equal(data, []byte("null"))
+}
diff --git a/seed/go-sdk/streaming/internal/extra_properties_test.go b/seed/go-sdk/streaming/internal/extra_properties_test.go
new file mode 100644
index 000000000000..aa2510ee5121
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/extra_properties_test.go
@@ -0,0 +1,228 @@
+package internal
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testMarshaler struct {
+ Name string `json:"name"`
+ BirthDate time.Time `json:"birthDate"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func (t *testMarshaler) MarshalJSON() ([]byte, error) {
+ type embed testMarshaler
+ var marshaler = struct {
+ embed
+ BirthDate string `json:"birthDate"`
+ CreatedAt string `json:"created_at"`
+ }{
+ embed: embed(*t),
+ BirthDate: t.BirthDate.Format("2006-01-02"),
+ CreatedAt: t.CreatedAt.Format(time.RFC3339),
+ }
+ return MarshalJSONWithExtraProperty(marshaler, "type", "test")
+}
+
+func TestMarshalJSONWithExtraProperties(t *testing.T) {
+ tests := []struct {
+ desc string
+ giveMarshaler interface{}
+ giveExtraProperties map[string]interface{}
+ wantBytes []byte
+ wantError string
+ }{
+ {
+ desc: "invalid type",
+ giveMarshaler: []string{"invalid"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid key type",
+ giveMarshaler: map[int]interface{}{42: "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
+ },
+ {
+ desc: "invalid map overwrite",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"key": "overwrite"},
+ wantError: `cannot add extra property "key" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
+ wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
+ },
+ {
+ desc: "invalid struct overwrite embedded type",
+ giveMarshaler: new(testMarshaler),
+ giveExtraProperties: map[string]interface{}{"name": "bob"},
+ wantError: `cannot add extra property "name" because it is already defined on the type`,
+ },
+ {
+ desc: "nil",
+ giveMarshaler: nil,
+ giveExtraProperties: nil,
+ wantBytes: []byte(`null`),
+ },
+ {
+ desc: "empty",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{}`),
+ },
+ {
+ desc: "no extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "only extra properties",
+ giveMarshaler: map[string]interface{}{},
+ giveExtraProperties: map[string]interface{}{"key": "value"},
+ wantBytes: []byte(`{"key":"value"}`),
+ },
+ {
+ desc: "single extra property",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"extra": "property"},
+ wantBytes: []byte(`{"key":"value","extra":"property"}`),
+ },
+ {
+ desc: "multiple extra properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
+ wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
+ },
+ {
+ desc: "nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "multiple nested properties",
+ giveMarshaler: map[string]interface{}{"key": "value"},
+ giveExtraProperties: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "ip": "127.0.0.1",
+ },
+ "user": map[string]interface{}{
+ "age": 42,
+ "name": "alice",
+ },
+ },
+ wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
+ },
+ {
+ desc: "custom marshaler",
+ giveMarshaler: &testMarshaler{
+ Name: "alice",
+ BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
+ CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
+ },
+ giveExtraProperties: map[string]interface{}{
+ "extra": "property",
+ },
+ wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
+ if tt.wantError != "" {
+ require.EqualError(t, err, tt.wantError)
+ assert.Nil(t, tt.wantBytes)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBytes, bytes)
+
+ value := make(map[string]interface{})
+ require.NoError(t, json.Unmarshal(bytes, &value))
+ })
+ }
+}
+
+func TestExtractExtraProperties(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+
+ t.Run("non-nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("nil pointer", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value *user
+ _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ assert.EqualError(t, err, "value must be non-nil to extract extra properties")
+ })
+
+ t.Run("non-zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("zero value", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ var value user
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
+ })
+
+ t.Run("exclude", func(t *testing.T) {
+ type user struct {
+ Name string `json:"name"`
+ }
+ value := &user{
+ Name: "alice",
+ }
+ extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
+ require.NoError(t, err)
+ assert.Nil(t, extraProperties)
+ })
+}
diff --git a/seed/go-sdk/streaming/internal/http.go b/seed/go-sdk/streaming/internal/http.go
new file mode 100644
index 000000000000..77863752bb58
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/http.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "reflect"
+)
+
+// HTTPClient is an interface for a subset of the *http.Client.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// ResolveBaseURL resolves the base URL from the given arguments,
+// preferring the first non-empty value.
+func ResolveBaseURL(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+// EncodeURL encodes the given arguments into the URL, escaping
+// values as needed. Pointer arguments are dereferenced before processing.
+func EncodeURL(urlFormat string, args ...interface{}) string {
+ escapedArgs := make([]interface{}, 0, len(args))
+ for _, arg := range args {
+ // Dereference the argument if it's a pointer
+ value := dereferenceArg(arg)
+ escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
+ }
+ return fmt.Sprintf(urlFormat, escapedArgs...)
+}
+
+// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
+// If the argument is not a pointer or is nil, it returns the argument as-is.
+func dereferenceArg(arg interface{}) interface{} {
+ if arg == nil {
+ return arg
+ }
+
+ v := reflect.ValueOf(arg)
+
+ // Keep dereferencing until we get to a non-pointer value or hit nil
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return nil
+ }
+ v = v.Elem()
+ }
+
+ return v.Interface()
+}
+
+// MergeHeaders merges the given headers together, where the right
+// takes precedence over the left.
+func MergeHeaders(left, right http.Header) http.Header {
+ for key, values := range right {
+ if len(values) > 1 {
+ left[key] = values
+ continue
+ }
+ if value := right.Get(key); value != "" {
+ left.Set(key, value)
+ }
+ }
+ return left
+}
diff --git a/seed/go-sdk/streaming/internal/query.go b/seed/go-sdk/streaming/internal/query.go
new file mode 100644
index 000000000000..1cbaf7fe1c02
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/query.go
@@ -0,0 +1,353 @@
+package internal
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+var (
+ bytesType = reflect.TypeOf([]byte{})
+ queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
+ timeType = reflect.TypeOf(time.Time{})
+ uuidType = reflect.TypeOf(uuid.UUID{})
+)
+
+// QueryEncoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type QueryEncoder interface {
+ EncodeQueryValues(key string, v *url.Values) error
+}
+
+// prepareValue handles common validation and unwrapping logic for both functions
+func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return reflect.Value{}, values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return reflect.Value{}, values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ if err != nil {
+ return reflect.Value{}, nil, err
+ }
+
+ return val, values, nil
+}
+
+// QueryValues encodes url.Values from request objects.
+//
+// Note: This type is inspired by Google's query encoding library, but
+// supports far less customization and is tailored to fit this SDK's use case.
+//
+// Ref: https://github.com/google/go-querystring
+func QueryValues(v interface{}) (url.Values, error) {
+ _, values, err := prepareValue(v)
+ return values, err
+}
+
+// QueryValuesWithDefaults encodes url.Values from request objects
+// and default values, merging the defaults into the request.
+// It's expected that the values of defaults are wire names.
+func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
+ val, values, err := prepareValue(v)
+ if err != nil {
+ return values, err
+ }
+ if !val.IsValid() {
+ return values, nil
+ }
+
+ // apply defaults to zero-value fields directly on the original struct
+ valType := val.Type()
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Field(i)
+ fieldType := valType.Field(i)
+ fieldName := fieldType.Name
+
+ if fieldType.PkgPath != "" && !fieldType.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ // check if field is zero value and we have a default for it
+ if field.CanSet() && field.IsZero() {
+ tag := fieldType.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+ wireName, _ := parseTag(tag)
+ if wireName == "" {
+ wireName = fieldName
+ }
+ if defaultVal, exists := defaults[wireName]; exists {
+ values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
+ }
+ }
+ }
+
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous {
+ // Skip unexported fields.
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "" || tag == "-" {
+ continue
+ }
+
+ name, opts := parseTag(tag)
+ if name == "" {
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(queryEncoderType) {
+ // If sv is a nil pointer and the custom encoder is defined on a non-pointer
+ // method receiver, set sv to the zero value of the underlying type
+ if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(QueryEncoder)
+ if err := m.EncodeQueryValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ // Recursively dereference pointers, but stop at nil pointers.
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts, sf))
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ if sv.Len() == 0 {
+ // Skip if slice or array is empty.
+ continue
+ }
+ for i := 0; i < sv.Len(); i++ {
+ value := sv.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), name); err != nil {
+ return err
+ }
+ } else {
+ values.Add(name, valueString(value, opts, sf))
+ }
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Map {
+ if err := reflectMap(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ if err := reflectValue(values, sv, name); err != nil {
+ return err
+ }
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts, sf))
+ }
+
+ return nil
+}
+
+// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
+func reflectMap(values url.Values, val reflect.Value, scope string) error {
+ if val.IsNil() {
+ return nil
+ }
+
+ iter := val.MapRange()
+ for iter.Next() {
+ k := iter.Key()
+ v := iter.Value()
+
+ key := fmt.Sprint(k.Interface())
+ paramName := scope + "[" + key + "]"
+
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ break
+ }
+ v = v.Elem()
+ }
+
+ for v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Map {
+ if err := reflectMap(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Struct {
+ if err := reflectValue(values, v, paramName); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
+ if v.Len() == 0 {
+ continue
+ }
+ for i := 0; i < v.Len(); i++ {
+ value := v.Index(i)
+ if isStructPointer(value) && !value.IsNil() {
+ if err := reflectValue(values, value.Elem(), paramName); err != nil {
+ return err
+ }
+ } else {
+ values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
+ }
+ }
+ continue
+ }
+
+ values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if format := sf.Tag.Get("format"); format == "date" {
+ return t.Format("2006-01-02")
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ if v.Type() == uuidType {
+ u := v.Interface().(uuid.UUID)
+ return u.String()
+ }
+
+ if v.Type() == bytesType {
+ b := v.Interface().([]byte)
+ return base64.StdEncoding.EncodeToString(b)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ type zeroable interface {
+ IsZero() bool
+ }
+
+ if !v.IsZero() {
+ if z, ok := v.Interface().(zeroable); ok {
+ return z.IsZero()
+ }
+ }
+
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
+ return false
+ }
+
+ return false
+}
+
+// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
+func isStructPointer(v reflect.Value) bool {
+ return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/seed/go-sdk/streaming/internal/query_test.go b/seed/go-sdk/streaming/internal/query_test.go
new file mode 100644
index 000000000000..2c28cb8acf68
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/query_test.go
@@ -0,0 +1,395 @@
+package internal
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestQueryValues(t *testing.T) {
+ t.Run("empty optional", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+
+ t.Run("empty required", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ values, err := QueryValues(&example{})
+ require.NoError(t, err)
+ assert.Equal(t, "required=", values.Encode())
+ })
+
+ t.Run("allow multiple", func(t *testing.T) {
+ type example struct {
+ Values []string `json:"values" url:"values"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Values: []string{"foo", "bar", "baz"},
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
+ })
+
+ t.Run("nested object", func(t *testing.T) {
+ type nested struct {
+ Value *string `json:"value,omitempty" url:"value,omitempty"`
+ }
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
+ }
+
+ nestedValue := "nestedValue"
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ Nested: &nested{
+ Value: &nestedValue,
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
+ })
+
+ t.Run("url unspecified", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("url ignored", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ NotFound string `json:"notFound" url:"-"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Required: "requiredValue",
+ NotFound: "notFound",
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "required=requiredValue", values.Encode())
+ })
+
+ t.Run("datetime", func(t *testing.T) {
+ type example struct {
+ DateTime time.Time `json:"dateTime" url:"dateTime"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
+ })
+
+ t.Run("date", func(t *testing.T) {
+ type example struct {
+ Date time.Time `json:"date" url:"date" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "date=1994-03-16", values.Encode())
+ })
+
+ t.Run("optional time", func(t *testing.T) {
+ type example struct {
+ Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
+ type enum string
+
+ type example struct {
+ Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{},
+ )
+ require.NoError(t, err)
+ assert.Empty(t, values.Encode())
+ })
+
+ t.Run("object array", func(t *testing.T) {
+ type object struct {
+ Key string `json:"key" url:"key"`
+ Value string `json:"value" url:"value"`
+ }
+ type example struct {
+ Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
+ }
+
+ values, err := QueryValues(
+ &example{
+ Objects: []*object{
+ {
+ Key: "hello",
+ Value: "world",
+ },
+ {
+ Key: "foo",
+ Value: "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
+ })
+
+ t.Run("map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "foo": "bar",
+ "baz": "qux",
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
+ })
+
+ t.Run("nested map array", func(t *testing.T) {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata" url:"metadata"`
+ }
+ values, err := QueryValues(
+ &request{
+ Metadata: map[string]interface{}{
+ "inner": []string{
+ "one",
+ "two",
+ "three",
+ },
+ },
+ },
+ )
+ require.NoError(t, err)
+ assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
+ })
+}
+
+func TestQueryValuesWithDefaults(t *testing.T) {
+ t.Run("apply defaults to zero values", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ })
+
+ t.Run("preserve non-zero values over defaults", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ Enabled bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Name: "actual-name",
+ Age: 30,
+ // Enabled remains false (zero value), should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
+ })
+
+ t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
+ type example struct {
+ Name string `json:"name" url:"name"`
+ Age int `json:"age" url:"age"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "nonexistent": "should-be-ignored",
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&name=default-name", values.Encode())
+ })
+
+ t.Run("type conversion for compatible defaults", func(t *testing.T) {
+ type example struct {
+ Count int64 `json:"count" url:"count"`
+ Rate float64 `json:"rate" url:"rate"`
+ Message string `json:"message" url:"message"`
+ }
+
+ defaults := map[string]interface{}{
+ "count": int(100), // int -> int64 conversion
+ "rate": float32(2.5), // float32 -> float64 conversion
+ "message": "hello", // string -> string (no conversion needed)
+ }
+
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
+ })
+
+ t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
+ type example struct {
+ Required string `json:"required" url:"required"`
+ Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
+ Count int `json:"count,omitempty" url:"count,omitempty"`
+ }
+
+ defaultOptional := "default-optional"
+ defaults := map[string]interface{}{
+ "required": "default-required",
+ "optional": &defaultOptional, // pointer type
+ "count": 42,
+ }
+
+ values, err := QueryValuesWithDefaults(&example{
+ Required: "custom-required", // should override default
+ // Optional is nil, should get default
+ // Count is 0, should get default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
+ })
+
+ t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
+ type example struct {
+ Name *string `json:"name" url:"name"`
+ Age *int `json:"age" url:"age"`
+ Enabled *bool `json:"enabled" url:"enabled"`
+ }
+
+ defaults := map[string]interface{}{
+ "name": "default-name",
+ "age": 25,
+ "enabled": true,
+ }
+
+ // first, test that a properly empty request is overridden:
+ {
+ values, err := QueryValuesWithDefaults(&example{}, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
+ }
+
+ // second, test that a request that contains zeros is not overridden:
+ var (
+ name = ""
+ age = 0
+ enabled = false
+ )
+ values, err := QueryValuesWithDefaults(&example{
+ Name: &name, // explicit empty string should override default
+ Age: &age, // explicit zero should override default
+ Enabled: &enabled, // explicit false should override default
+ }, defaults)
+ require.NoError(t, err)
+ assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
+ })
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
+}
diff --git a/seed/go-sdk/streaming/internal/retrier.go b/seed/go-sdk/streaming/internal/retrier.go
new file mode 100644
index 000000000000..4efae1b4c286
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/retrier.go
@@ -0,0 +1,230 @@
+package internal
+
+import (
+ "crypto/rand"
+ "math/big"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+const (
+ defaultRetryAttempts = 2
+ minRetryDelay = 1000 * time.Millisecond
+ maxRetryDelay = 60000 * time.Millisecond
+)
+
+// RetryOption adapts the behavior the *Retrier.
+type RetryOption func(*retryOptions)
+
+// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
+type RetryFunc func(*http.Request) (*http.Response, error)
+
+// WithMaxAttempts configures the maximum number of attempts
+// of the *Retrier.
+func WithMaxAttempts(attempts uint) RetryOption {
+ return func(opts *retryOptions) {
+ opts.attempts = attempts
+ }
+}
+
+// Retrier retries failed requests a configurable number of times with an
+// exponential back-off between each retry.
+type Retrier struct {
+ attempts uint
+}
+
+// NewRetrier constructs a new *Retrier with the given options, if any.
+func NewRetrier(opts ...RetryOption) *Retrier {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ attempts := uint(defaultRetryAttempts)
+ if options.attempts > 0 {
+ attempts = options.attempts
+ }
+ return &Retrier{
+ attempts: attempts,
+ }
+}
+
+// Run issues the request and, upon failure, retries the request if possible.
+//
+// The request will be retried as long as the request is deemed retryable and the
+// number of retry attempts has not grown larger than the configured retry limit.
+func (r *Retrier) Run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ opts ...RetryOption,
+) (*http.Response, error) {
+ options := new(retryOptions)
+ for _, opt := range opts {
+ opt(options)
+ }
+ maxRetryAttempts := r.attempts
+ if options.attempts > 0 {
+ maxRetryAttempts = options.attempts
+ }
+ var (
+ retryAttempt uint
+ previousError error
+ )
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt,
+ previousError,
+ )
+}
+
+func (r *Retrier) run(
+ fn RetryFunc,
+ request *http.Request,
+ errorDecoder ErrorDecoder,
+ maxRetryAttempts uint,
+ retryAttempt uint,
+ previousError error,
+) (*http.Response, error) {
+ if retryAttempt >= maxRetryAttempts {
+ return nil, previousError
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := request.Context().Err(); err != nil {
+ return nil, err
+ }
+
+ response, err := fn(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.shouldRetry(response) {
+ defer response.Body.Close()
+
+ delay, err := r.retryDelay(response, retryAttempt)
+ if err != nil {
+ return nil, err
+ }
+
+ time.Sleep(delay)
+
+ return r.run(
+ fn,
+ request,
+ errorDecoder,
+ maxRetryAttempts,
+ retryAttempt+1,
+ decodeError(response, errorDecoder),
+ )
+ }
+
+ return response, nil
+}
+
+// shouldRetry returns true if the request should be retried based on the given
+// response status code.
+func (r *Retrier) shouldRetry(response *http.Response) bool {
+ return response.StatusCode == http.StatusTooManyRequests ||
+ response.StatusCode == http.StatusRequestTimeout ||
+ response.StatusCode >= http.StatusInternalServerError
+}
+
+// retryDelay calculates the delay time based on response headers,
+// falling back to exponential backoff if no headers are present.
+func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
+ // Check for Retry-After header first (RFC 7231), applying no jitter
+ if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
+ // Parse as number of seconds...
+ if seconds, err := strconv.Atoi(retryAfter); err == nil {
+ delay := time.Duration(seconds) * time.Second
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+
+ // ...or as an HTTP date; both are valid
+ if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
+ delay := time.Until(retryTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return delay, nil
+ }
+ }
+ }
+
+ // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
+ if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
+ if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
+ // Assume Unix timestamp in seconds
+ resetTime := time.Unix(resetTimestamp, 0)
+ delay := time.Until(resetTime)
+ if delay > 0 {
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+ return r.addPositiveJitter(delay)
+ }
+ }
+ }
+
+ // Fall back to exponential backoff
+ return r.exponentialBackoff(retryAttempt)
+}
+
+// exponentialBackoff calculates the delay time based on the retry attempt
+// and applies symmetric jitter (±10% around the delay).
+func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
+ if retryAttempt > 63 { // 2^63+ would overflow uint64
+ retryAttempt = 63
+ }
+
+ delay := minRetryDelay << retryAttempt
+ if delay > maxRetryDelay {
+ delay = maxRetryDelay
+ }
+
+ return r.addSymmetricJitter(delay)
+}
+
+// addJitterWithRange applies jitter to the given delay.
+// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
+func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
+ jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
+ jitter, err := rand.Int(rand.Reader, jitterRange)
+ if err != nil {
+ return 0, err
+ }
+
+ jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
+ if jitteredDelay < minRetryDelay {
+ jitteredDelay = minRetryDelay
+ }
+ if jitteredDelay > maxRetryDelay {
+ jitteredDelay = maxRetryDelay
+ }
+ return jitteredDelay, nil
+}
+
+// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
+func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 100, 120)
+}
+
+// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
+func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
+ return r.addJitterWithRange(delay, 90, 110)
+}
+
+type retryOptions struct {
+ attempts uint
+}
diff --git a/seed/go-sdk/streaming/internal/retrier_test.go b/seed/go-sdk/streaming/internal/retrier_test.go
new file mode 100644
index 000000000000..15242fb5062d
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/retrier_test.go
@@ -0,0 +1,300 @@
+package internal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/fern-api/stream-go/v2/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type RetryTestCase struct {
+ description string
+
+ giveAttempts uint
+ giveStatusCodes []int
+ giveResponse *InternalTestResponse
+
+ wantResponse *InternalTestResponse
+ wantError *core.APIError
+}
+
+func TestRetrier(t *testing.T) {
+ tests := []*RetryTestCase{
+ {
+ description: "retry request succeeds after multiple failures",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ giveResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ wantResponse: &InternalTestResponse{
+ Id: "1",
+ },
+ },
+ {
+ description: "retry request fails if MaxAttempts is exceeded",
+ giveAttempts: 3,
+ giveStatusCodes: []int{
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusRequestTimeout,
+ http.StatusOK,
+ },
+ wantError: &core.APIError{
+ StatusCode: http.StatusRequestTimeout,
+ },
+ },
+ {
+ description: "retry durations increase exponentially and stay within the min and max delay values",
+ giveAttempts: 4,
+ giveStatusCodes: []int{
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusServiceUnavailable,
+ http.StatusOK,
+ },
+ },
+ {
+ description: "retry does not occur on status code 404",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
+ wantError: &core.APIError{
+ StatusCode: http.StatusNotFound,
+ },
+ },
+ {
+ description: "retries occur on status code 429",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 408",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
+ },
+ {
+ description: "retries occur on status code 500",
+ giveAttempts: 2,
+ giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.description, func(t *testing.T) {
+ var (
+ test = tc
+ server = newTestRetryServer(t, test)
+ client = server.Client()
+ )
+
+ t.Parallel()
+
+ caller := NewCaller(
+ &CallerParams{
+ Client: client,
+ },
+ )
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: test.giveAttempts,
+ ResponseIsOptional: true,
+ },
+ )
+
+ if test.wantError != nil {
+ require.IsType(t, err, &core.APIError{})
+ expectedErrorCode := test.wantError.StatusCode
+ actualErrorCode := err.(*core.APIError).StatusCode
+ assert.Equal(t, expectedErrorCode, actualErrorCode)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.wantResponse, response)
+ })
+ }
+}
+
+// newTestRetryServer returns a new *httptest.Server configured with the
+// given test parameters, suitable for testing retries.
+func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
+ var index int
+ timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
+
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if index > 0 && index < len(expectedRetryDurations) {
+ // Ensure that the duration between retries increases exponentially,
+ // and that it is within the minimum and maximum retry delay values.
+ actualDuration := timestamps[index].Sub(timestamps[index-1])
+ expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
+ expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
+ assert.True(
+ t,
+ actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
+ "expected duration to be in range [%v, %v], got %v",
+ expectedDurationMin,
+ expectedDurationMax,
+ actualDuration,
+ )
+ assert.LessOrEqual(
+ t,
+ actualDuration,
+ maxRetryDelay,
+ "expected duration to be less than the maxRetryDelay (%v), got %v",
+ maxRetryDelay,
+ actualDuration,
+ )
+ assert.GreaterOrEqual(
+ t,
+ actualDuration,
+ minRetryDelay,
+ "expected duration to be greater than the minRetryDelay (%v), got %v",
+ minRetryDelay,
+ actualDuration,
+ )
+ }
+
+ request := new(InternalTestRequest)
+ bytes, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ require.NoError(t, json.Unmarshal(bytes, request))
+ require.LessOrEqual(t, index, len(tc.giveStatusCodes))
+
+ statusCode := tc.giveStatusCodes[index]
+
+ w.WriteHeader(statusCode)
+
+ if tc.giveResponse != nil && statusCode == http.StatusOK {
+ bytes, err = json.Marshal(tc.giveResponse)
+ require.NoError(t, err)
+ _, err = w.Write(bytes)
+ require.NoError(t, err)
+ }
+
+ index++
+ },
+ ),
+ )
+}
+
+// expectedRetryDurations holds an array of calculated retry durations,
+// where the index of the array should correspond to the retry attempt.
+//
+// Values are calculated based off of `minRetryDelay * 2^i`.
+var expectedRetryDurations = []time.Duration{
+ 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
+ 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
+ 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
+ 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
+}
+
+func TestRetryDelayTiming(t *testing.T) {
+ tests := []struct {
+ name string
+ headerName string
+ headerValueFunc func() string
+ expectedMinMs int64
+ expectedMaxMs int64
+ }{
+ {
+ name: "retry-after with seconds value",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return "1"
+ },
+ expectedMinMs: 500,
+ expectedMaxMs: 1500,
+ },
+ {
+ name: "retry-after with HTTP date",
+ headerName: "retry-after",
+ headerValueFunc: func() string {
+ return time.Now().Add(3 * time.Second).Format(time.RFC1123)
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ {
+ name: "x-ratelimit-reset with future timestamp",
+ headerName: "x-ratelimit-reset",
+ headerValueFunc: func() string {
+ return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
+ },
+ expectedMinMs: 1500,
+ expectedMaxMs: 4500,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ var timestamps []time.Time
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ timestamps = append(timestamps, time.Now())
+ if len(timestamps) == 1 {
+ // First request - return retryable error with header
+ w.Header().Set(tt.headerName, tt.headerValueFunc())
+ w.WriteHeader(http.StatusTooManyRequests)
+ } else {
+ // Second request - return success
+ w.WriteHeader(http.StatusOK)
+ response := &InternalTestResponse{Id: "success"}
+ bytes, _ := json.Marshal(response)
+ w.Write(bytes)
+ }
+ }))
+ defer server.Close()
+
+ caller := NewCaller(&CallerParams{
+ Client: server.Client(),
+ })
+
+ var response *InternalTestResponse
+ _, err := caller.Call(
+ context.Background(),
+ &CallParams{
+ URL: server.URL,
+ Method: http.MethodGet,
+ Request: &InternalTestRequest{},
+ Response: &response,
+ MaxAttempts: 2,
+ ResponseIsOptional: true,
+ },
+ )
+
+ require.NoError(t, err)
+ require.Len(t, timestamps, 2, "Expected exactly 2 requests")
+
+ actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
+
+ assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
+ "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
+ assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
+ "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
+ })
+ }
+}
diff --git a/seed/go-sdk/streaming/internal/streamer.go b/seed/go-sdk/streaming/internal/streamer.go
new file mode 100644
index 000000000000..205dff896cab
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/streamer.go
@@ -0,0 +1,118 @@
+package internal
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/fern-api/stream-go/v2/core"
+)
+
+const (
+ // DefaultDataPrefix is the default prefix used for SSE streaming.
+ DefaultSSEDataPrefix = "data: "
+
+ // DefaultTerminator is the default terminator used for SSE streaming.
+ DefaultSSETerminator = "[DONE]"
+)
+
+// Streamer calls APIs and streams responses using a *Stream.
+type Streamer[T any] struct {
+ client HTTPClient
+ retrier *Retrier
+}
+
+// NewStreamer returns a new *Streamer backed by the given caller's HTTP client.
+func NewStreamer[T any](caller *Caller) *Streamer[T] {
+ return &Streamer[T]{
+ client: caller.client,
+ retrier: caller.retrier,
+ }
+}
+
+// StreamParams represents the parameters used to issue an API streaming call.
+type StreamParams struct {
+ URL string
+ Method string
+ Prefix string
+ Delimiter string
+ Terminator string
+ MaxAttempts uint
+ Headers http.Header
+ BodyProperties map[string]interface{}
+ QueryParameters url.Values
+ Client HTTPClient
+ Request interface{}
+ ErrorDecoder ErrorDecoder
+ Format core.StreamFormat
+}
+
+// Stream issues an API streaming call according to the given stream parameters.
+func (s *Streamer[T]) Stream(ctx context.Context, params *StreamParams) (*core.Stream[T], error) {
+ url := buildURL(params.URL, params.QueryParameters)
+ req, err := newRequest(
+ ctx,
+ url,
+ params.Method,
+ params.Headers,
+ params.Request,
+ params.BodyProperties,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // If the call has been cancelled, don't issue the request.
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+
+ client := s.client
+ if params.Client != nil {
+ // Use the HTTP client scoped to the request.
+ client = params.Client
+ }
+
+ var retryOptions []RetryOption
+ if params.MaxAttempts > 0 {
+ retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
+ }
+
+ resp, err := s.retrier.Run(
+ client.Do,
+ req,
+ params.ErrorDecoder,
+ retryOptions...,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if the call was cancelled before we return the error
+ // associated with the call and/or unmarshal the response data.
+ if err := ctx.Err(); err != nil {
+ defer resp.Body.Close()
+ return nil, err
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ defer resp.Body.Close()
+ return nil, decodeError(resp, params.ErrorDecoder)
+ }
+
+ var opts []core.StreamOption
+ if params.Delimiter != "" {
+ opts = append(opts, core.WithDelimiter(params.Delimiter))
+ }
+ if params.Prefix != "" {
+ opts = append(opts, core.WithPrefix(params.Prefix))
+ }
+ if params.Terminator != "" {
+ opts = append(opts, core.WithTerminator(params.Terminator))
+ }
+ if params.Format != core.StreamFormatEmpty {
+ opts = append(opts, core.WithFormat(params.Format))
+ }
+
+ return core.NewStream[T](resp, opts...), nil
+}
diff --git a/seed/go-sdk/streaming/internal/stringer.go b/seed/go-sdk/streaming/internal/stringer.go
new file mode 100644
index 000000000000..312801851e0e
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/stringer.go
@@ -0,0 +1,13 @@
+package internal
+
+import "encoding/json"
+
+// StringifyJSON returns a pretty JSON string representation of
+// the given value.
+func StringifyJSON(value interface{}) (string, error) {
+ bytes, err := json.MarshalIndent(value, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return string(bytes), nil
+}
diff --git a/seed/go-sdk/streaming/internal/time.go b/seed/go-sdk/streaming/internal/time.go
new file mode 100644
index 000000000000..ab0e269fade3
--- /dev/null
+++ b/seed/go-sdk/streaming/internal/time.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "encoding/json"
+ "time"
+)
+
+const dateFormat = "2006-01-02"
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date (e.g. 2006-01-02).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type Date struct {
+ t *time.Time
+}
+
+// NewDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewDate(t time.Time) *Date {
+ return &Date{t: &t}
+}
+
+// NewOptionalDate returns a new *Date. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDate(t *time.Time) *Date {
+ if t == nil {
+ return nil
+ }
+ return &Date{t: t}
+}
+
+// Time returns the Date's underlying time, if any. If the
+// date is nil, the zero value is returned.
+func (d *Date) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the Date's underlying time.Time, if any.
+func (d *Date) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *Date) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(dateFormat, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = Date{t: &parsedTime}
+ return nil
+}
+
+// DateTime wraps time.Time and adapts its JSON representation
+// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
+//
+// Ref: https://ijmacd.github.io/rfc3339-iso8601
+type DateTime struct {
+ t *time.Time
+}
+
+// NewDateTime returns a new *DateTime.
+func NewDateTime(t time.Time) *DateTime {
+ return &DateTime{t: &t}
+}
+
+// NewOptionalDateTime returns a new *DateTime. If the given time.Time
+// is nil, nil will be returned.
+func NewOptionalDateTime(t *time.Time) *DateTime {
+ if t == nil {
+ return nil
+ }
+ return &DateTime{t: t}
+}
+
+// Time returns the DateTime's underlying time, if any. If the
+// date-time is nil, the zero value is returned.
+func (d *DateTime) Time() time.Time {
+ if d == nil || d.t == nil {
+ return time.Time{}
+ }
+ return *d.t
+}
+
+// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
+func (d *DateTime) TimePtr() *time.Time {
+ if d == nil || d.t == nil {
+ return nil
+ }
+ if d.t.IsZero() {
+ return nil
+ }
+ return d.t
+}
+
+func (d *DateTime) MarshalJSON() ([]byte, error) {
+ if d == nil || d.t == nil {
+ return nil, nil
+ }
+ return json.Marshal(d.t.Format(time.RFC3339))
+}
+
+func (d *DateTime) UnmarshalJSON(data []byte) error {
+ var raw string
+ if err := json.Unmarshal(data, &raw); err != nil {
+ return err
+ }
+
+ parsedTime, err := time.Parse(time.RFC3339, raw)
+ if err != nil {
+ return err
+ }
+
+ *d = DateTime{t: &parsedTime}
+ return nil
+}
diff --git a/seed/go-sdk/streaming/option/request_option.go b/seed/go-sdk/streaming/option/request_option.go
new file mode 100644
index 000000000000..52c6b5d8bdf9
--- /dev/null
+++ b/seed/go-sdk/streaming/option/request_option.go
@@ -0,0 +1,64 @@
+// Code generated by Fern. DO NOT EDIT.
+
+package option
+
+import (
+ core "github.com/fern-api/stream-go/v2/core"
+ http "net/http"
+ url "net/url"
+)
+
+// RequestOption adapts the behavior of an individual request.
+type RequestOption = core.RequestOption
+
+// WithBaseURL sets the base URL, overriding the default
+// environment, if any.
+func WithBaseURL(baseURL string) *core.BaseURLOption {
+ return &core.BaseURLOption{
+ BaseURL: baseURL,
+ }
+}
+
+// WithHTTPClient uses the given HTTPClient to issue the request.
+func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
+ return &core.HTTPClientOption{
+ HTTPClient: httpClient,
+ }
+}
+
+// WithHTTPHeader adds the given http.Header to the request.
+func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
+ return &core.HTTPHeaderOption{
+ // Clone the headers so they can't be modified after the option call.
+ HTTPHeader: httpHeader.Clone(),
+ }
+}
+
+// WithBodyProperties adds the given body properties to the request.
+func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
+ copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
+ for key, value := range bodyProperties {
+ copiedBodyProperties[key] = value
+ }
+ return &core.BodyPropertiesOption{
+ BodyProperties: copiedBodyProperties,
+ }
+}
+
+// WithQueryParameters adds the given query parameters to the request.
+func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
+ copiedQueryParameters := make(url.Values, len(queryParameters))
+ for key, values := range queryParameters {
+ copiedQueryParameters[key] = values
+ }
+ return &core.QueryParametersOption{
+ QueryParameters: copiedQueryParameters,
+ }
+}
+
+// WithMaxAttempts configures the maximum number of retry attempts.
+func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
+ return &core.MaxAttemptsOption{
+ MaxAttempts: attempts,
+ }
+}
diff --git a/seed/go-sdk/undiscriminated-union-with-response-property/internal/query.go b/seed/go-sdk/undiscriminated-union-with-response-property/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/undiscriminated-union-with-response-property/internal/query.go
+++ b/seed/go-sdk/undiscriminated-union-with-response-property/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/undiscriminated-union-with-response-property/internal/query_test.go b/seed/go-sdk/undiscriminated-union-with-response-property/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/undiscriminated-union-with-response-property/internal/query_test.go
+++ b/seed/go-sdk/undiscriminated-union-with-response-property/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query.go b/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query.go
+++ b/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query_test.go b/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/undiscriminated-unions/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/undiscriminated-unions/v0/internal/query.go b/seed/go-sdk/undiscriminated-unions/v0/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/undiscriminated-unions/v0/internal/query.go
+++ b/seed/go-sdk/undiscriminated-unions/v0/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/undiscriminated-unions/v0/internal/query_test.go b/seed/go-sdk/undiscriminated-unions/v0/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/undiscriminated-unions/v0/internal/query_test.go
+++ b/seed/go-sdk/undiscriminated-unions/v0/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/unions-with-local-date/internal/query.go b/seed/go-sdk/unions-with-local-date/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/unions-with-local-date/internal/query.go
+++ b/seed/go-sdk/unions-with-local-date/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/unions-with-local-date/internal/query_test.go b/seed/go-sdk/unions-with-local-date/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/unions-with-local-date/internal/query_test.go
+++ b/seed/go-sdk/unions-with-local-date/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/unions-with-local-date/types.go b/seed/go-sdk/unions-with-local-date/types.go
index 1274bbf06166..b61a24285803 100644
--- a/seed/go-sdk/unions-with-local-date/types.go
+++ b/seed/go-sdk/unions-with-local-date/types.go
@@ -397,11 +397,11 @@ func (u *Union) validate() error {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo *Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo *Foo
}
func (u *UnionWithBaseProperties) GetType() string {
@@ -425,11 +425,11 @@ func (u *UnionWithBaseProperties) GetInteger() int {
return u.Integer
}
-func (u *UnionWithBaseProperties) GetString() string {
+func (u *UnionWithBaseProperties) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithBaseProperties) GetFoo() *Foo {
@@ -463,12 +463,12 @@ func (u *UnionWithBaseProperties) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
case "foo":
value := new(Foo)
if err := json.Unmarshal(data, &value); err != nil {
@@ -495,15 +495,15 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- Id string `json:"id"`
- String string `json:"value"`
+ Type string `json:"type"`
+ Id string `json:"id"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- Id: u.Id,
- String: u.String,
+ Type: "string",
+ Id: u.Id,
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -515,7 +515,7 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
type UnionWithBasePropertiesVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
VisitFoo(*Foo) error
}
@@ -523,8 +523,8 @@ func (u *UnionWithBaseProperties) Accept(visitor UnionWithBasePropertiesVisitor)
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
if u.Foo != nil {
return visitor.VisitFoo(u.Foo)
@@ -540,7 +540,7 @@ func (u *UnionWithBaseProperties) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if u.Foo != nil {
@@ -1562,9 +1562,9 @@ func (u *UnionWithOptionalTime) validate() error {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
func (u *UnionWithPrimitive) GetType() string {
@@ -1581,11 +1581,11 @@ func (u *UnionWithPrimitive) GetInteger() int {
return u.Integer
}
-func (u *UnionWithPrimitive) GetString() string {
+func (u *UnionWithPrimitive) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
@@ -1610,12 +1610,12 @@ func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
}
return nil
}
@@ -1634,13 +1634,13 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: u.String,
+ Type: "string",
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -1649,15 +1649,15 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
type UnionWithPrimitiveVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
}
func (u *UnionWithPrimitive) Accept(visitor UnionWithPrimitiveVisitor) error {
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
return fmt.Errorf("type %T does not define a non-empty union type", u)
}
@@ -1670,7 +1670,7 @@ func (u *UnionWithPrimitive) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if len(fields) == 0 {
diff --git a/seed/go-sdk/unions/no-custom-config/internal/query.go b/seed/go-sdk/unions/no-custom-config/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/unions/no-custom-config/internal/query.go
+++ b/seed/go-sdk/unions/no-custom-config/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/unions/no-custom-config/internal/query_test.go b/seed/go-sdk/unions/no-custom-config/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/unions/no-custom-config/internal/query_test.go
+++ b/seed/go-sdk/unions/no-custom-config/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/unions/no-custom-config/types.go b/seed/go-sdk/unions/no-custom-config/types.go
index cefdedbea0cf..0793c93922d3 100644
--- a/seed/go-sdk/unions/no-custom-config/types.go
+++ b/seed/go-sdk/unions/no-custom-config/types.go
@@ -397,11 +397,11 @@ func (u *Union) validate() error {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo *Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo *Foo
}
func (u *UnionWithBaseProperties) GetType() string {
@@ -425,11 +425,11 @@ func (u *UnionWithBaseProperties) GetInteger() int {
return u.Integer
}
-func (u *UnionWithBaseProperties) GetString() string {
+func (u *UnionWithBaseProperties) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithBaseProperties) GetFoo() *Foo {
@@ -463,12 +463,12 @@ func (u *UnionWithBaseProperties) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
case "foo":
value := new(Foo)
if err := json.Unmarshal(data, &value); err != nil {
@@ -495,15 +495,15 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- Id string `json:"id"`
- String string `json:"value"`
+ Type string `json:"type"`
+ Id string `json:"id"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- Id: u.Id,
- String: u.String,
+ Type: "string",
+ Id: u.Id,
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -515,7 +515,7 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
type UnionWithBasePropertiesVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
VisitFoo(*Foo) error
}
@@ -523,8 +523,8 @@ func (u *UnionWithBaseProperties) Accept(visitor UnionWithBasePropertiesVisitor)
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
if u.Foo != nil {
return visitor.VisitFoo(u.Foo)
@@ -540,7 +540,7 @@ func (u *UnionWithBaseProperties) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if u.Foo != nil {
@@ -1562,9 +1562,9 @@ func (u *UnionWithOptionalTime) validate() error {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
func (u *UnionWithPrimitive) GetType() string {
@@ -1581,11 +1581,11 @@ func (u *UnionWithPrimitive) GetInteger() int {
return u.Integer
}
-func (u *UnionWithPrimitive) GetString() string {
+func (u *UnionWithPrimitive) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
@@ -1610,12 +1610,12 @@ func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
}
return nil
}
@@ -1634,13 +1634,13 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: u.String,
+ Type: "string",
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -1649,15 +1649,15 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
type UnionWithPrimitiveVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
}
func (u *UnionWithPrimitive) Accept(visitor UnionWithPrimitiveVisitor) error {
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
return fmt.Errorf("type %T does not define a non-empty union type", u)
}
@@ -1670,7 +1670,7 @@ func (u *UnionWithPrimitive) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if len(fields) == 0 {
diff --git a/seed/go-sdk/unions/package-name/internal/query.go b/seed/go-sdk/unions/package-name/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/unions/package-name/internal/query.go
+++ b/seed/go-sdk/unions/package-name/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/unions/package-name/internal/query_test.go b/seed/go-sdk/unions/package-name/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/unions/package-name/internal/query_test.go
+++ b/seed/go-sdk/unions/package-name/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/unions/package-name/types.go b/seed/go-sdk/unions/package-name/types.go
index cd939c68dfdd..34ee48998795 100644
--- a/seed/go-sdk/unions/package-name/types.go
+++ b/seed/go-sdk/unions/package-name/types.go
@@ -397,11 +397,11 @@ func (u *Union) validate() error {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo *Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo *Foo
}
func (u *UnionWithBaseProperties) GetType() string {
@@ -425,11 +425,11 @@ func (u *UnionWithBaseProperties) GetInteger() int {
return u.Integer
}
-func (u *UnionWithBaseProperties) GetString() string {
+func (u *UnionWithBaseProperties) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithBaseProperties) GetFoo() *Foo {
@@ -463,12 +463,12 @@ func (u *UnionWithBaseProperties) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
case "foo":
value := new(Foo)
if err := json.Unmarshal(data, &value); err != nil {
@@ -495,15 +495,15 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- Id string `json:"id"`
- String string `json:"value"`
+ Type string `json:"type"`
+ Id string `json:"id"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- Id: u.Id,
- String: u.String,
+ Type: "string",
+ Id: u.Id,
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -515,7 +515,7 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
type UnionWithBasePropertiesVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
VisitFoo(*Foo) error
}
@@ -523,8 +523,8 @@ func (u *UnionWithBaseProperties) Accept(visitor UnionWithBasePropertiesVisitor)
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
if u.Foo != nil {
return visitor.VisitFoo(u.Foo)
@@ -540,7 +540,7 @@ func (u *UnionWithBaseProperties) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if u.Foo != nil {
@@ -1562,9 +1562,9 @@ func (u *UnionWithOptionalTime) validate() error {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
func (u *UnionWithPrimitive) GetType() string {
@@ -1581,11 +1581,11 @@ func (u *UnionWithPrimitive) GetInteger() int {
return u.Integer
}
-func (u *UnionWithPrimitive) GetString() string {
+func (u *UnionWithPrimitive) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
@@ -1610,12 +1610,12 @@ func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
}
return nil
}
@@ -1634,13 +1634,13 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
}
return json.Marshal(marshaler)
}
- if u.String != "" {
+ if u.FieldString != "" {
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: u.String,
+ Type: "string",
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -1649,15 +1649,15 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
type UnionWithPrimitiveVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
}
func (u *UnionWithPrimitive) Accept(visitor UnionWithPrimitiveVisitor) error {
if u.Integer != 0 {
return visitor.VisitInteger(u.Integer)
}
- if u.String != "" {
- return visitor.VisitString(u.String)
+ if u.FieldString != "" {
+ return visitor.VisitFieldString(u.FieldString)
}
return fmt.Errorf("type %T does not define a non-empty union type", u)
}
@@ -1670,7 +1670,7 @@ func (u *UnionWithPrimitive) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if len(fields) == 0 {
diff --git a/seed/go-sdk/unions/v0/internal/query.go b/seed/go-sdk/unions/v0/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/unions/v0/internal/query.go
+++ b/seed/go-sdk/unions/v0/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/unions/v0/internal/query_test.go b/seed/go-sdk/unions/v0/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/unions/v0/internal/query_test.go
+++ b/seed/go-sdk/unions/v0/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/unions/v0/types.go b/seed/go-sdk/unions/v0/types.go
index 2b9e3602a6e2..894d6428bd8c 100644
--- a/seed/go-sdk/unions/v0/types.go
+++ b/seed/go-sdk/unions/v0/types.go
@@ -407,19 +407,19 @@ func (u *Union) validate() error {
}
type UnionWithBaseProperties struct {
- Type string
- Id string
- Integer int
- String string
- Foo *Foo
+ Type string
+ Id string
+ Integer int
+ FieldString string
+ Foo *Foo
}
func NewUnionWithBasePropertiesFromInteger(value int) *UnionWithBaseProperties {
return &UnionWithBaseProperties{Type: "integer", Integer: value}
}
-func NewUnionWithBasePropertiesFromString(value string) *UnionWithBaseProperties {
- return &UnionWithBaseProperties{Type: "string", String: value}
+func NewUnionWithBasePropertiesFromFieldString(value string) *UnionWithBaseProperties {
+ return &UnionWithBaseProperties{Type: "string", FieldString: value}
}
func NewUnionWithBasePropertiesFromFoo(value *Foo) *UnionWithBaseProperties {
@@ -447,11 +447,11 @@ func (u *UnionWithBaseProperties) GetInteger() int {
return u.Integer
}
-func (u *UnionWithBaseProperties) GetString() string {
+func (u *UnionWithBaseProperties) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithBaseProperties) GetFoo() *Foo {
@@ -485,12 +485,12 @@ func (u *UnionWithBaseProperties) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
case "foo":
value := new(Foo)
if err := json.Unmarshal(data, &value); err != nil {
@@ -521,13 +521,13 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
return json.Marshal(marshaler)
case "string":
var marshaler = struct {
- Type string `json:"type"`
- Id string `json:"id"`
- String string `json:"value"`
+ Type string `json:"type"`
+ Id string `json:"id"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- Id: u.Id,
- String: u.String,
+ Type: "string",
+ Id: u.Id,
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
case "foo":
@@ -537,7 +537,7 @@ func (u UnionWithBaseProperties) MarshalJSON() ([]byte, error) {
type UnionWithBasePropertiesVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
VisitFoo(*Foo) error
}
@@ -548,7 +548,7 @@ func (u *UnionWithBaseProperties) Accept(visitor UnionWithBasePropertiesVisitor)
case "integer":
return visitor.VisitInteger(u.Integer)
case "string":
- return visitor.VisitString(u.String)
+ return visitor.VisitFieldString(u.FieldString)
case "foo":
return visitor.VisitFoo(u.Foo)
}
@@ -562,7 +562,7 @@ func (u *UnionWithBaseProperties) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if u.Foo != nil {
@@ -1654,17 +1654,17 @@ func (u *UnionWithOptionalTime) validate() error {
}
type UnionWithPrimitive struct {
- Type string
- Integer int
- String string
+ Type string
+ Integer int
+ FieldString string
}
func NewUnionWithPrimitiveFromInteger(value int) *UnionWithPrimitive {
return &UnionWithPrimitive{Type: "integer", Integer: value}
}
-func NewUnionWithPrimitiveFromString(value string) *UnionWithPrimitive {
- return &UnionWithPrimitive{Type: "string", String: value}
+func NewUnionWithPrimitiveFromFieldString(value string) *UnionWithPrimitive {
+ return &UnionWithPrimitive{Type: "string", FieldString: value}
}
func (u *UnionWithPrimitive) GetType() string {
@@ -1681,11 +1681,11 @@ func (u *UnionWithPrimitive) GetInteger() int {
return u.Integer
}
-func (u *UnionWithPrimitive) GetString() string {
+func (u *UnionWithPrimitive) GetFieldString() string {
if u == nil {
return ""
}
- return u.String
+ return u.FieldString
}
func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
@@ -1710,12 +1710,12 @@ func (u *UnionWithPrimitive) UnmarshalJSON(data []byte) error {
u.Integer = valueUnmarshaler.Integer
case "string":
var valueUnmarshaler struct {
- String string `json:"value"`
+ FieldString string `json:"value"`
}
if err := json.Unmarshal(data, &valueUnmarshaler); err != nil {
return err
}
- u.String = valueUnmarshaler.String
+ u.FieldString = valueUnmarshaler.FieldString
}
return nil
}
@@ -1738,11 +1738,11 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
return json.Marshal(marshaler)
case "string":
var marshaler = struct {
- Type string `json:"type"`
- String string `json:"value"`
+ Type string `json:"type"`
+ FieldString string `json:"value"`
}{
- Type: "string",
- String: u.String,
+ Type: "string",
+ FieldString: u.FieldString,
}
return json.Marshal(marshaler)
}
@@ -1750,7 +1750,7 @@ func (u UnionWithPrimitive) MarshalJSON() ([]byte, error) {
type UnionWithPrimitiveVisitor interface {
VisitInteger(int) error
- VisitString(string) error
+ VisitFieldString(string) error
}
func (u *UnionWithPrimitive) Accept(visitor UnionWithPrimitiveVisitor) error {
@@ -1760,7 +1760,7 @@ func (u *UnionWithPrimitive) Accept(visitor UnionWithPrimitiveVisitor) error {
case "integer":
return visitor.VisitInteger(u.Integer)
case "string":
- return visitor.VisitString(u.String)
+ return visitor.VisitFieldString(u.FieldString)
}
}
@@ -1772,7 +1772,7 @@ func (u *UnionWithPrimitive) validate() error {
if u.Integer != 0 {
fields = append(fields, "integer")
}
- if u.String != "" {
+ if u.FieldString != "" {
fields = append(fields, "string")
}
if len(fields) == 0 {
diff --git a/seed/go-sdk/unknown/internal/query.go b/seed/go-sdk/unknown/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/unknown/internal/query.go
+++ b/seed/go-sdk/unknown/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/unknown/internal/query_test.go b/seed/go-sdk/unknown/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/unknown/internal/query_test.go
+++ b/seed/go-sdk/unknown/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/url-form-encoded/internal/query.go b/seed/go-sdk/url-form-encoded/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/url-form-encoded/internal/query.go
+++ b/seed/go-sdk/url-form-encoded/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/url-form-encoded/internal/query_test.go b/seed/go-sdk/url-form-encoded/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/url-form-encoded/internal/query_test.go
+++ b/seed/go-sdk/url-form-encoded/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/validation/internal/query.go b/seed/go-sdk/validation/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/validation/internal/query.go
+++ b/seed/go-sdk/validation/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/validation/internal/query_test.go b/seed/go-sdk/validation/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/validation/internal/query_test.go
+++ b/seed/go-sdk/validation/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/variables/internal/query.go b/seed/go-sdk/variables/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/variables/internal/query.go
+++ b/seed/go-sdk/variables/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/variables/internal/query_test.go b/seed/go-sdk/variables/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/variables/internal/query_test.go
+++ b/seed/go-sdk/variables/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/version-no-default/internal/query.go b/seed/go-sdk/version-no-default/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/version-no-default/internal/query.go
+++ b/seed/go-sdk/version-no-default/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/version-no-default/internal/query_test.go b/seed/go-sdk/version-no-default/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/version-no-default/internal/query_test.go
+++ b/seed/go-sdk/version-no-default/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/version/internal/query.go b/seed/go-sdk/version/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/version/internal/query.go
+++ b/seed/go-sdk/version/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/version/internal/query_test.go b/seed/go-sdk/version/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/version/internal/query_test.go
+++ b/seed/go-sdk/version/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/websocket-bearer-auth/internal/query.go b/seed/go-sdk/websocket-bearer-auth/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/websocket-bearer-auth/internal/query.go
+++ b/seed/go-sdk/websocket-bearer-auth/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/websocket-bearer-auth/internal/query_test.go b/seed/go-sdk/websocket-bearer-auth/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/websocket-bearer-auth/internal/query_test.go
+++ b/seed/go-sdk/websocket-bearer-auth/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/websocket-inferred-auth/internal/query.go b/seed/go-sdk/websocket-inferred-auth/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/websocket-inferred-auth/internal/query.go
+++ b/seed/go-sdk/websocket-inferred-auth/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/websocket-inferred-auth/internal/query_test.go b/seed/go-sdk/websocket-inferred-auth/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/websocket-inferred-auth/internal/query_test.go
+++ b/seed/go-sdk/websocket-inferred-auth/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
diff --git a/seed/go-sdk/websocket/internal/query.go b/seed/go-sdk/websocket/internal/query.go
index 786318b5c9c2..1cbaf7fe1c02 100644
--- a/seed/go-sdk/websocket/internal/query.go
+++ b/seed/go-sdk/websocket/internal/query.go
@@ -70,6 +70,9 @@ func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (ur
if err != nil {
return values, err
}
+ if !val.IsValid() {
+ return values, nil
+ }
// apply defaults to zero-value fields directly on the original struct
valType := val.Type()
diff --git a/seed/go-sdk/websocket/internal/query_test.go b/seed/go-sdk/websocket/internal/query_test.go
index 1a4076d63235..2c28cb8acf68 100644
--- a/seed/go-sdk/websocket/internal/query_test.go
+++ b/seed/go-sdk/websocket/internal/query_test.go
@@ -371,4 +371,25 @@ func TestQueryValuesWithDefaults(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
})
+
+ t.Run("nil input returns empty values", func(t *testing.T) {
+ defaults := map[string]any{
+ "name": "default-name",
+ "age": 25,
+ }
+
+ // Test with nil
+ values, err := QueryValuesWithDefaults(nil, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+
+ // Test with nil pointer
+ type example struct {
+ Name string `json:"name" url:"name"`
+ }
+ var nilPtr *example
+ values, err = QueryValuesWithDefaults(nilPtr, defaults)
+ require.NoError(t, err)
+ assert.Empty(t, values)
+ })
}
From 0a7faa0b7ecc07923c9cbde18d44b453027f727b Mon Sep 17 00:00:00 2001
From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
Date: Sun, 30 Nov 2025 00:27:02 -0500
Subject: [PATCH 16/16] Remove obsolete query tests, retrier implementation,
and related utilities
- Deleted `query_test.go` which contained various tests for query value generation.
- Removed `retrier.go` and `retrier_test.go`, eliminating the retry logic and its associated tests.
- Cleared out `streamer.go`, which handled API streaming calls.
- Removed `stringer.go` that provided JSON stringification utilities.
- Deleted `time.go` which contained date and time handling structures.
- Removed `request_option.go`, which defined request options for HTTP calls.
---
.../idempotency-headers/.fern/metadata.json | 12 -
.../internal/extra_properties.go | 141 -----
.../internal/extra_properties_test.go | 228 --------
.../idempotency-headers/internal/stringer.go | 13 -
.../idempotency-headers/internal/time.go | 137 -----
seed/go-model/streaming/.fern/metadata.json | 11 -
.../streaming/internal/extra_properties.go | 141 -----
.../internal/extra_properties_test.go | 228 --------
seed/go-model/streaming/internal/stringer.go | 13 -
seed/go-model/streaming/internal/time.go | 137 -----
.../idempotency-headers/.fern/metadata.json | 13 -
.../.github/workflows/ci.yml | 35 --
.../idempotency-headers/client/client.go | 33 --
.../idempotency-headers/client/client_test.go | 45 --
.../idempotency-headers/client/options.go | 45 --
.../idempotency-headers/core/api_error.go | 47 --
seed/go-sdk/idempotency-headers/core/http.go | 15 -
.../core/idempotent_request_option.go | 72 ---
.../core/request_option.go | 153 ------
.../dynamic-snippets/example0/snippet.go | 27 -
.../dynamic-snippets/example1/snippet.go | 22 -
.../idempotency-headers/internal/caller.go | 250 ---------
.../internal/caller_test.go | 395 --------------
.../internal/error_decoder.go | 64 ---
.../internal/error_decoder_test.go | 59 ---
.../internal/explicit_fields.go | 116 ----
.../internal/explicit_fields_test.go | 497 ------------------
.../internal/extra_properties.go | 141 -----
.../internal/extra_properties_test.go | 228 --------
.../idempotency-headers/internal/http.go | 71 ---
.../idempotency-headers/internal/query.go | 353 -------------
.../internal/query_test.go | 395 --------------
.../idempotency-headers/internal/retrier.go | 230 --------
.../internal/retrier_test.go | 300 -----------
.../idempotency-headers/internal/stringer.go | 13 -
.../idempotency-headers/internal/time.go | 137 -----
.../option/idempotent_request_option.go | 24 -
.../option/request_option.go | 71 ---
.../idempotency-headers/payment/client.go | 66 ---
.../idempotency-headers/payment/raw_client.go | 114 ----
seed/go-sdk/streaming/.fern/metadata.json | 12 -
.../go-sdk/streaming/.github/workflows/ci.yml | 35 --
seed/go-sdk/streaming/client/client.go | 33 --
seed/go-sdk/streaming/client/client_test.go | 45 --
seed/go-sdk/streaming/core/api_error.go | 47 --
seed/go-sdk/streaming/core/http.go | 15 -
seed/go-sdk/streaming/core/request_option.go | 109 ----
seed/go-sdk/streaming/core/stream.go | 368 -------------
seed/go-sdk/streaming/dummy/client.go | 83 ---
seed/go-sdk/streaming/dummy/raw_client.go | 72 ---
.../dynamic-snippets/example0/snippet.go | 23 -
.../dynamic-snippets/example1/snippet.go | 23 -
.../dynamic-snippets/example2/snippet.go | 23 -
seed/go-sdk/streaming/internal/caller.go | 250 ---------
seed/go-sdk/streaming/internal/caller_test.go | 395 --------------
.../streaming/internal/error_decoder.go | 64 ---
.../streaming/internal/error_decoder_test.go | 59 ---
.../streaming/internal/explicit_fields.go | 116 ----
.../internal/explicit_fields_test.go | 497 ------------------
.../streaming/internal/extra_properties.go | 141 -----
.../internal/extra_properties_test.go | 228 --------
seed/go-sdk/streaming/internal/http.go | 71 ---
seed/go-sdk/streaming/internal/query.go | 353 -------------
seed/go-sdk/streaming/internal/query_test.go | 395 --------------
seed/go-sdk/streaming/internal/retrier.go | 230 --------
.../go-sdk/streaming/internal/retrier_test.go | 300 -----------
seed/go-sdk/streaming/internal/streamer.go | 118 -----
seed/go-sdk/streaming/internal/stringer.go | 13 -
seed/go-sdk/streaming/internal/time.go | 137 -----
.../go-sdk/streaming/option/request_option.go | 64 ---
70 files changed, 9411 deletions(-)
delete mode 100644 seed/go-model/idempotency-headers/.fern/metadata.json
delete mode 100644 seed/go-model/idempotency-headers/internal/extra_properties.go
delete mode 100644 seed/go-model/idempotency-headers/internal/extra_properties_test.go
delete mode 100644 seed/go-model/idempotency-headers/internal/stringer.go
delete mode 100644 seed/go-model/idempotency-headers/internal/time.go
delete mode 100644 seed/go-model/streaming/.fern/metadata.json
delete mode 100644 seed/go-model/streaming/internal/extra_properties.go
delete mode 100644 seed/go-model/streaming/internal/extra_properties_test.go
delete mode 100644 seed/go-model/streaming/internal/stringer.go
delete mode 100644 seed/go-model/streaming/internal/time.go
delete mode 100644 seed/go-sdk/idempotency-headers/.fern/metadata.json
delete mode 100644 seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
delete mode 100644 seed/go-sdk/idempotency-headers/client/client.go
delete mode 100644 seed/go-sdk/idempotency-headers/client/client_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/client/options.go
delete mode 100644 seed/go-sdk/idempotency-headers/core/api_error.go
delete mode 100644 seed/go-sdk/idempotency-headers/core/http.go
delete mode 100644 seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
delete mode 100644 seed/go-sdk/idempotency-headers/core/request_option.go
delete mode 100644 seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
delete mode 100644 seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/caller.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/caller_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/error_decoder.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/explicit_fields.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/extra_properties.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/http.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/query.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/query_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/retrier.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/retrier_test.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/stringer.go
delete mode 100644 seed/go-sdk/idempotency-headers/internal/time.go
delete mode 100644 seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
delete mode 100644 seed/go-sdk/idempotency-headers/option/request_option.go
delete mode 100644 seed/go-sdk/idempotency-headers/payment/client.go
delete mode 100644 seed/go-sdk/idempotency-headers/payment/raw_client.go
delete mode 100644 seed/go-sdk/streaming/.fern/metadata.json
delete mode 100644 seed/go-sdk/streaming/.github/workflows/ci.yml
delete mode 100644 seed/go-sdk/streaming/client/client.go
delete mode 100644 seed/go-sdk/streaming/client/client_test.go
delete mode 100644 seed/go-sdk/streaming/core/api_error.go
delete mode 100644 seed/go-sdk/streaming/core/http.go
delete mode 100644 seed/go-sdk/streaming/core/request_option.go
delete mode 100644 seed/go-sdk/streaming/core/stream.go
delete mode 100644 seed/go-sdk/streaming/dummy/client.go
delete mode 100644 seed/go-sdk/streaming/dummy/raw_client.go
delete mode 100644 seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
delete mode 100644 seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
delete mode 100644 seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
delete mode 100644 seed/go-sdk/streaming/internal/caller.go
delete mode 100644 seed/go-sdk/streaming/internal/caller_test.go
delete mode 100644 seed/go-sdk/streaming/internal/error_decoder.go
delete mode 100644 seed/go-sdk/streaming/internal/error_decoder_test.go
delete mode 100644 seed/go-sdk/streaming/internal/explicit_fields.go
delete mode 100644 seed/go-sdk/streaming/internal/explicit_fields_test.go
delete mode 100644 seed/go-sdk/streaming/internal/extra_properties.go
delete mode 100644 seed/go-sdk/streaming/internal/extra_properties_test.go
delete mode 100644 seed/go-sdk/streaming/internal/http.go
delete mode 100644 seed/go-sdk/streaming/internal/query.go
delete mode 100644 seed/go-sdk/streaming/internal/query_test.go
delete mode 100644 seed/go-sdk/streaming/internal/retrier.go
delete mode 100644 seed/go-sdk/streaming/internal/retrier_test.go
delete mode 100644 seed/go-sdk/streaming/internal/streamer.go
delete mode 100644 seed/go-sdk/streaming/internal/stringer.go
delete mode 100644 seed/go-sdk/streaming/internal/time.go
delete mode 100644 seed/go-sdk/streaming/option/request_option.go
diff --git a/seed/go-model/idempotency-headers/.fern/metadata.json b/seed/go-model/idempotency-headers/.fern/metadata.json
deleted file mode 100644
index 52154345880d..000000000000
--- a/seed/go-model/idempotency-headers/.fern/metadata.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "cliVersion": "DUMMY",
- "generatorName": "fernapi/fern-go-model",
- "generatorVersion": "latest",
- "generatorConfig": {
- "packageName": "fern",
- "module": {
- "path": "github.com/idempotency-headers/fern"
- },
- "includeLegacyClientOptions": true
- }
-}
\ No newline at end of file
diff --git a/seed/go-model/idempotency-headers/internal/extra_properties.go b/seed/go-model/idempotency-headers/internal/extra_properties.go
deleted file mode 100644
index 57517691f132..000000000000
--- a/seed/go-model/idempotency-headers/internal/extra_properties.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "reflect"
- "strings"
-)
-
-// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
-func MarshalJSONWithExtraProperty(marshaler any, key string, value any) ([]byte, error) {
- return MarshalJSONWithExtraProperties(marshaler, map[string]any{key: value})
-}
-
-// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
-func MarshalJSONWithExtraProperties(marshaler any, extraProperties map[string]any) ([]byte, error) {
- bytes, err := json.Marshal(marshaler)
- if err != nil {
- return nil, err
- }
- if len(extraProperties) == 0 {
- return bytes, nil
- }
- keys, err := getKeys(marshaler)
- if err != nil {
- return nil, err
- }
- for _, key := range keys {
- if _, ok := extraProperties[key]; ok {
- return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
- }
- }
- extraBytes, err := json.Marshal(extraProperties)
- if err != nil {
- return nil, err
- }
- if isEmptyJSON(bytes) {
- if isEmptyJSON(extraBytes) {
- return bytes, nil
- }
- return extraBytes, nil
- }
- result := bytes[:len(bytes)-1]
- result = append(result, ',')
- result = append(result, extraBytes[1:len(extraBytes)-1]...)
- result = append(result, '}')
- return result, nil
-}
-
-// ExtractExtraProperties extracts any extra properties from the given value.
-func ExtractExtraProperties(bytes []byte, value any, exclude ...string) (map[string]any, error) {
- val := reflect.ValueOf(value)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil, fmt.Errorf("value must be non-nil to extract extra properties")
- }
- val = val.Elem()
- }
- if err := json.Unmarshal(bytes, &value); err != nil {
- return nil, err
- }
- var extraProperties map[string]any
- if err := json.Unmarshal(bytes, &extraProperties); err != nil {
- return nil, err
- }
- for i := 0; i < val.Type().NumField(); i++ {
- key := jsonKey(val.Type().Field(i))
- if key == "" || key == "-" {
- continue
- }
- delete(extraProperties, key)
- }
- for _, key := range exclude {
- delete(extraProperties, key)
- }
- if len(extraProperties) == 0 {
- return nil, nil
- }
- return extraProperties, nil
-}
-
-// getKeys returns the keys associated with the given value. The value must be a
-// a struct or a map with string keys.
-func getKeys(value any) ([]string, error) {
- val := reflect.ValueOf(value)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- if !val.IsValid() {
- return nil, nil
- }
- switch val.Kind() {
- case reflect.Struct:
- return getKeysForStructType(val.Type()), nil
- case reflect.Map:
- var keys []string
- if val.Type().Key().Kind() != reflect.String {
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
- for _, key := range val.MapKeys() {
- keys = append(keys, key.String())
- }
- return keys, nil
- default:
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
-}
-
-// getKeysForStructType returns all the keys associated with the given struct type,
-// visiting embedded fields recursively.
-func getKeysForStructType(structType reflect.Type) []string {
- if structType.Kind() == reflect.Pointer {
- structType = structType.Elem()
- }
- if structType.Kind() != reflect.Struct {
- return nil
- }
- var keys []string
- for i := 0; i < structType.NumField(); i++ {
- field := structType.Field(i)
- if field.Anonymous {
- keys = append(keys, getKeysForStructType(field.Type)...)
- continue
- }
- keys = append(keys, jsonKey(field))
- }
- return keys
-}
-
-// jsonKey returns the JSON key from the struct tag of the given field,
-// excluding the omitempty flag (if any).
-func jsonKey(field reflect.StructField) string {
- return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
-}
-
-// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
-// an explicit null.
-func isEmptyJSON(data []byte) bool {
- return len(data) <= 2 || bytes.Equal(data, []byte("null"))
-}
diff --git a/seed/go-model/idempotency-headers/internal/extra_properties_test.go b/seed/go-model/idempotency-headers/internal/extra_properties_test.go
deleted file mode 100644
index 0d46257763fb..000000000000
--- a/seed/go-model/idempotency-headers/internal/extra_properties_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testMarshaler struct {
- Name string `json:"name"`
- BirthDate time.Time `json:"birthDate"`
- CreatedAt time.Time `json:"created_at"`
-}
-
-func (t *testMarshaler) MarshalJSON() ([]byte, error) {
- type embed testMarshaler
- var marshaler = struct {
- embed
- BirthDate string `json:"birthDate"`
- CreatedAt string `json:"created_at"`
- }{
- embed: embed(*t),
- BirthDate: t.BirthDate.Format("2006-01-02"),
- CreatedAt: t.CreatedAt.Format(time.RFC3339),
- }
- return MarshalJSONWithExtraProperty(marshaler, "type", "test")
-}
-
-func TestMarshalJSONWithExtraProperties(t *testing.T) {
- tests := []struct {
- desc string
- giveMarshaler any
- giveExtraProperties map[string]any
- wantBytes []byte
- wantError string
- }{
- {
- desc: "invalid type",
- giveMarshaler: []string{"invalid"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid key type",
- giveMarshaler: map[int]any{42: "value"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid map overwrite",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot add extra property "key" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]any{"birthDate": "2000-01-01"},
- wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite embedded type",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]any{"name": "bob"},
- wantError: `cannot add extra property "name" because it is already defined on the type`,
- },
- {
- desc: "nil",
- giveMarshaler: nil,
- giveExtraProperties: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "empty",
- giveMarshaler: map[string]any{},
- giveExtraProperties: map[string]any{},
- wantBytes: []byte(`{}`),
- },
- {
- desc: "no extra properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "only extra properties",
- giveMarshaler: map[string]any{},
- giveExtraProperties: map[string]any{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "single extra property",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"extra": "property"},
- wantBytes: []byte(`{"key":"value","extra":"property"}`),
- },
- {
- desc: "multiple extra properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"one": 1, "two": 2},
- wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
- },
- {
- desc: "nested properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{
- "user": map[string]any{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "multiple nested properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{
- "metadata": map[string]any{
- "ip": "127.0.0.1",
- },
- "user": map[string]any{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "custom marshaler",
- giveMarshaler: &testMarshaler{
- Name: "alice",
- BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
- CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- giveExtraProperties: map[string]any{
- "extra": "property",
- },
- wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, tt.wantBytes, bytes)
-
- value := make(map[string]any)
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestExtractExtraProperties(t *testing.T) {
- t.Run("none", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-
- t.Run("non-nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value *user
- _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- assert.EqualError(t, err, "value must be non-nil to extract extra properties")
- })
-
- t.Run("non-zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value user
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("exclude", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-}
diff --git a/seed/go-model/idempotency-headers/internal/stringer.go b/seed/go-model/idempotency-headers/internal/stringer.go
deleted file mode 100644
index 0be54d1b5359..000000000000
--- a/seed/go-model/idempotency-headers/internal/stringer.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package internal
-
-import "encoding/json"
-
-// StringifyJSON returns a pretty JSON string representation of
-// the given value.
-func StringifyJSON(value any) (string, error) {
- bytes, err := json.MarshalIndent(value, "", " ")
- if err != nil {
- return "", err
- }
- return string(bytes), nil
-}
diff --git a/seed/go-model/idempotency-headers/internal/time.go b/seed/go-model/idempotency-headers/internal/time.go
deleted file mode 100644
index ab0e269fade3..000000000000
--- a/seed/go-model/idempotency-headers/internal/time.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "time"
-)
-
-const dateFormat = "2006-01-02"
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date (e.g. 2006-01-02).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type Date struct {
- t *time.Time
-}
-
-// NewDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewDate(t time.Time) *Date {
- return &Date{t: &t}
-}
-
-// NewOptionalDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDate(t *time.Time) *Date {
- if t == nil {
- return nil
- }
- return &Date{t: t}
-}
-
-// Time returns the Date's underlying time, if any. If the
-// date is nil, the zero value is returned.
-func (d *Date) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the Date's underlying time.Time, if any.
-func (d *Date) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *Date) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(dateFormat))
-}
-
-func (d *Date) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(dateFormat, raw)
- if err != nil {
- return err
- }
-
- *d = Date{t: &parsedTime}
- return nil
-}
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type DateTime struct {
- t *time.Time
-}
-
-// NewDateTime returns a new *DateTime.
-func NewDateTime(t time.Time) *DateTime {
- return &DateTime{t: &t}
-}
-
-// NewOptionalDateTime returns a new *DateTime. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDateTime(t *time.Time) *DateTime {
- if t == nil {
- return nil
- }
- return &DateTime{t: t}
-}
-
-// Time returns the DateTime's underlying time, if any. If the
-// date-time is nil, the zero value is returned.
-func (d *DateTime) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
-func (d *DateTime) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *DateTime) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(time.RFC3339))
-}
-
-func (d *DateTime) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(time.RFC3339, raw)
- if err != nil {
- return err
- }
-
- *d = DateTime{t: &parsedTime}
- return nil
-}
diff --git a/seed/go-model/streaming/.fern/metadata.json b/seed/go-model/streaming/.fern/metadata.json
deleted file mode 100644
index 0feece7292ec..000000000000
--- a/seed/go-model/streaming/.fern/metadata.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "cliVersion": "DUMMY",
- "generatorName": "fernapi/fern-go-model",
- "generatorVersion": "latest",
- "generatorConfig": {
- "packageName": "stream",
- "module": {
- "path": "github.com/fern-api/stream-go"
- }
- }
-}
\ No newline at end of file
diff --git a/seed/go-model/streaming/internal/extra_properties.go b/seed/go-model/streaming/internal/extra_properties.go
deleted file mode 100644
index 57517691f132..000000000000
--- a/seed/go-model/streaming/internal/extra_properties.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "reflect"
- "strings"
-)
-
-// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
-func MarshalJSONWithExtraProperty(marshaler any, key string, value any) ([]byte, error) {
- return MarshalJSONWithExtraProperties(marshaler, map[string]any{key: value})
-}
-
-// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
-func MarshalJSONWithExtraProperties(marshaler any, extraProperties map[string]any) ([]byte, error) {
- bytes, err := json.Marshal(marshaler)
- if err != nil {
- return nil, err
- }
- if len(extraProperties) == 0 {
- return bytes, nil
- }
- keys, err := getKeys(marshaler)
- if err != nil {
- return nil, err
- }
- for _, key := range keys {
- if _, ok := extraProperties[key]; ok {
- return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
- }
- }
- extraBytes, err := json.Marshal(extraProperties)
- if err != nil {
- return nil, err
- }
- if isEmptyJSON(bytes) {
- if isEmptyJSON(extraBytes) {
- return bytes, nil
- }
- return extraBytes, nil
- }
- result := bytes[:len(bytes)-1]
- result = append(result, ',')
- result = append(result, extraBytes[1:len(extraBytes)-1]...)
- result = append(result, '}')
- return result, nil
-}
-
-// ExtractExtraProperties extracts any extra properties from the given value.
-func ExtractExtraProperties(bytes []byte, value any, exclude ...string) (map[string]any, error) {
- val := reflect.ValueOf(value)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil, fmt.Errorf("value must be non-nil to extract extra properties")
- }
- val = val.Elem()
- }
- if err := json.Unmarshal(bytes, &value); err != nil {
- return nil, err
- }
- var extraProperties map[string]any
- if err := json.Unmarshal(bytes, &extraProperties); err != nil {
- return nil, err
- }
- for i := 0; i < val.Type().NumField(); i++ {
- key := jsonKey(val.Type().Field(i))
- if key == "" || key == "-" {
- continue
- }
- delete(extraProperties, key)
- }
- for _, key := range exclude {
- delete(extraProperties, key)
- }
- if len(extraProperties) == 0 {
- return nil, nil
- }
- return extraProperties, nil
-}
-
-// getKeys returns the keys associated with the given value. The value must be a
-// a struct or a map with string keys.
-func getKeys(value any) ([]string, error) {
- val := reflect.ValueOf(value)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- if !val.IsValid() {
- return nil, nil
- }
- switch val.Kind() {
- case reflect.Struct:
- return getKeysForStructType(val.Type()), nil
- case reflect.Map:
- var keys []string
- if val.Type().Key().Kind() != reflect.String {
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
- for _, key := range val.MapKeys() {
- keys = append(keys, key.String())
- }
- return keys, nil
- default:
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
-}
-
-// getKeysForStructType returns all the keys associated with the given struct type,
-// visiting embedded fields recursively.
-func getKeysForStructType(structType reflect.Type) []string {
- if structType.Kind() == reflect.Pointer {
- structType = structType.Elem()
- }
- if structType.Kind() != reflect.Struct {
- return nil
- }
- var keys []string
- for i := 0; i < structType.NumField(); i++ {
- field := structType.Field(i)
- if field.Anonymous {
- keys = append(keys, getKeysForStructType(field.Type)...)
- continue
- }
- keys = append(keys, jsonKey(field))
- }
- return keys
-}
-
-// jsonKey returns the JSON key from the struct tag of the given field,
-// excluding the omitempty flag (if any).
-func jsonKey(field reflect.StructField) string {
- return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
-}
-
-// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
-// an explicit null.
-func isEmptyJSON(data []byte) bool {
- return len(data) <= 2 || bytes.Equal(data, []byte("null"))
-}
diff --git a/seed/go-model/streaming/internal/extra_properties_test.go b/seed/go-model/streaming/internal/extra_properties_test.go
deleted file mode 100644
index 0d46257763fb..000000000000
--- a/seed/go-model/streaming/internal/extra_properties_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testMarshaler struct {
- Name string `json:"name"`
- BirthDate time.Time `json:"birthDate"`
- CreatedAt time.Time `json:"created_at"`
-}
-
-func (t *testMarshaler) MarshalJSON() ([]byte, error) {
- type embed testMarshaler
- var marshaler = struct {
- embed
- BirthDate string `json:"birthDate"`
- CreatedAt string `json:"created_at"`
- }{
- embed: embed(*t),
- BirthDate: t.BirthDate.Format("2006-01-02"),
- CreatedAt: t.CreatedAt.Format(time.RFC3339),
- }
- return MarshalJSONWithExtraProperty(marshaler, "type", "test")
-}
-
-func TestMarshalJSONWithExtraProperties(t *testing.T) {
- tests := []struct {
- desc string
- giveMarshaler any
- giveExtraProperties map[string]any
- wantBytes []byte
- wantError string
- }{
- {
- desc: "invalid type",
- giveMarshaler: []string{"invalid"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid key type",
- giveMarshaler: map[int]any{42: "value"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid map overwrite",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"key": "overwrite"},
- wantError: `cannot add extra property "key" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]any{"birthDate": "2000-01-01"},
- wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite embedded type",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]any{"name": "bob"},
- wantError: `cannot add extra property "name" because it is already defined on the type`,
- },
- {
- desc: "nil",
- giveMarshaler: nil,
- giveExtraProperties: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "empty",
- giveMarshaler: map[string]any{},
- giveExtraProperties: map[string]any{},
- wantBytes: []byte(`{}`),
- },
- {
- desc: "no extra properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "only extra properties",
- giveMarshaler: map[string]any{},
- giveExtraProperties: map[string]any{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "single extra property",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"extra": "property"},
- wantBytes: []byte(`{"key":"value","extra":"property"}`),
- },
- {
- desc: "multiple extra properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{"one": 1, "two": 2},
- wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
- },
- {
- desc: "nested properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{
- "user": map[string]any{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "multiple nested properties",
- giveMarshaler: map[string]any{"key": "value"},
- giveExtraProperties: map[string]any{
- "metadata": map[string]any{
- "ip": "127.0.0.1",
- },
- "user": map[string]any{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "custom marshaler",
- giveMarshaler: &testMarshaler{
- Name: "alice",
- BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
- CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- giveExtraProperties: map[string]any{
- "extra": "property",
- },
- wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, tt.wantBytes, bytes)
-
- value := make(map[string]any)
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestExtractExtraProperties(t *testing.T) {
- t.Run("none", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-
- t.Run("non-nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value *user
- _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- assert.EqualError(t, err, "value must be non-nil to extract extra properties")
- })
-
- t.Run("non-zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value user
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]any{"age": float64(42)}, extraProperties)
- })
-
- t.Run("exclude", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-}
diff --git a/seed/go-model/streaming/internal/stringer.go b/seed/go-model/streaming/internal/stringer.go
deleted file mode 100644
index 0be54d1b5359..000000000000
--- a/seed/go-model/streaming/internal/stringer.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package internal
-
-import "encoding/json"
-
-// StringifyJSON returns a pretty JSON string representation of
-// the given value.
-func StringifyJSON(value any) (string, error) {
- bytes, err := json.MarshalIndent(value, "", " ")
- if err != nil {
- return "", err
- }
- return string(bytes), nil
-}
diff --git a/seed/go-model/streaming/internal/time.go b/seed/go-model/streaming/internal/time.go
deleted file mode 100644
index ab0e269fade3..000000000000
--- a/seed/go-model/streaming/internal/time.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "time"
-)
-
-const dateFormat = "2006-01-02"
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date (e.g. 2006-01-02).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type Date struct {
- t *time.Time
-}
-
-// NewDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewDate(t time.Time) *Date {
- return &Date{t: &t}
-}
-
-// NewOptionalDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDate(t *time.Time) *Date {
- if t == nil {
- return nil
- }
- return &Date{t: t}
-}
-
-// Time returns the Date's underlying time, if any. If the
-// date is nil, the zero value is returned.
-func (d *Date) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the Date's underlying time.Time, if any.
-func (d *Date) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *Date) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(dateFormat))
-}
-
-func (d *Date) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(dateFormat, raw)
- if err != nil {
- return err
- }
-
- *d = Date{t: &parsedTime}
- return nil
-}
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type DateTime struct {
- t *time.Time
-}
-
-// NewDateTime returns a new *DateTime.
-func NewDateTime(t time.Time) *DateTime {
- return &DateTime{t: &t}
-}
-
-// NewOptionalDateTime returns a new *DateTime. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDateTime(t *time.Time) *DateTime {
- if t == nil {
- return nil
- }
- return &DateTime{t: t}
-}
-
-// Time returns the DateTime's underlying time, if any. If the
-// date-time is nil, the zero value is returned.
-func (d *DateTime) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
-func (d *DateTime) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *DateTime) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(time.RFC3339))
-}
-
-func (d *DateTime) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(time.RFC3339, raw)
- if err != nil {
- return err
- }
-
- *d = DateTime{t: &parsedTime}
- return nil
-}
diff --git a/seed/go-sdk/idempotency-headers/.fern/metadata.json b/seed/go-sdk/idempotency-headers/.fern/metadata.json
deleted file mode 100644
index 66bd514ed2c0..000000000000
--- a/seed/go-sdk/idempotency-headers/.fern/metadata.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "cliVersion": "DUMMY",
- "generatorName": "fernapi/fern-go-sdk",
- "generatorVersion": "latest",
- "generatorConfig": {
- "enableWireTests": false,
- "packageName": "fern",
- "module": {
- "path": "github.com/idempotency-headers/fern"
- },
- "includeLegacyClientOptions": true
- }
-}
\ No newline at end of file
diff --git a/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml b/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
deleted file mode 100644
index 56310d69624b..000000000000
--- a/seed/go-sdk/idempotency-headers/.github/workflows/ci.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: ci
-
-on: [push]
-
-jobs:
- compile:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repo
- uses: actions/checkout@v4
-
- - name: Set up go
- uses: actions/setup-go@v4
-
- - name: Compile
- run: go build ./...
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repo
- uses: actions/checkout@v4
-
- - name: Set up go
- uses: actions/setup-go@v4
-
- - name: Setup wiremock server
- run: |
- if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
-
- - name: Test
- run: go test ./...
-
- - name: Teardown wiremock server
- run: |
- if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/idempotency-headers/client/client.go b/seed/go-sdk/idempotency-headers/client/client.go
deleted file mode 100644
index df29cd1e96bd..000000000000
--- a/seed/go-sdk/idempotency-headers/client/client.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package client
-
-import (
- core "github.com/idempotency-headers/fern/core"
- internal "github.com/idempotency-headers/fern/internal"
- option "github.com/idempotency-headers/fern/option"
- payment "github.com/idempotency-headers/fern/payment"
-)
-
-type Client struct {
- Payment *payment.Client
-
- options *core.RequestOptions
- baseURL string
- caller *internal.Caller
-}
-
-func NewClient(opts ...option.RequestOption) *Client {
- options := core.NewRequestOptions(opts...)
- return &Client{
- Payment: payment.NewClient(options),
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/client/client_test.go b/seed/go-sdk/idempotency-headers/client/client_test.go
deleted file mode 100644
index 984e654452ff..000000000000
--- a/seed/go-sdk/idempotency-headers/client/client_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package client
-
-import (
- option "github.com/idempotency-headers/fern/option"
- assert "github.com/stretchr/testify/assert"
- http "net/http"
- testing "testing"
- time "time"
-)
-
-func TestNewClient(t *testing.T) {
- t.Run("default", func(t *testing.T) {
- c := NewClient()
- assert.Empty(t, c.baseURL)
- })
-
- t.Run("base url", func(t *testing.T) {
- c := NewClient(
- option.WithBaseURL("test.co"),
- )
- assert.Equal(t, "test.co", c.baseURL)
- })
-
- t.Run("http client", func(t *testing.T) {
- httpClient := &http.Client{
- Timeout: 5 * time.Second,
- }
- c := NewClient(
- option.WithHTTPClient(httpClient),
- )
- assert.Empty(t, c.baseURL)
- })
-
- t.Run("http header", func(t *testing.T) {
- header := make(http.Header)
- header.Set("X-API-Tenancy", "test")
- c := NewClient(
- option.WithHTTPHeader(header),
- )
- assert.Empty(t, c.baseURL)
- assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
- })
-}
diff --git a/seed/go-sdk/idempotency-headers/client/options.go b/seed/go-sdk/idempotency-headers/client/options.go
deleted file mode 100644
index 1a2d4df5ad59..000000000000
--- a/seed/go-sdk/idempotency-headers/client/options.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package client
-
-import (
- core "github.com/idempotency-headers/fern/core"
- option "github.com/idempotency-headers/fern/option"
- http "net/http"
-)
-
-// WithBaseURL sets the base URL, overriding the default
-// environment, if any.
-func WithBaseURL(baseURL string) *core.BaseURLOption {
- return option.WithBaseURL(baseURL)
-}
-
-// WithHTTPClient uses the given HTTPClient to issue the request.
-func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
- return option.WithHTTPClient(httpClient)
-}
-
-// WithHTTPHeader adds the given http.Header to the request.
-func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
- return option.WithHTTPHeader(httpHeader)
-}
-
-// WithMaxAttempts configures the maximum number of retry attempts.
-func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
- return option.WithMaxAttempts(attempts)
-}
-
-// WithToken sets the 'Authorization: Bearer ' request header.
-func WithToken(token string) *core.TokenOption {
- return option.WithToken(token)
-}
-
-// WithIdempotencyKey sets the idempotencyKey request header.
-func WithIdempotencyKey(idempotencyKey string) *core.IdempotencyKeyOption {
- return option.WithIdempotencyKey(idempotencyKey)
-}
-
-// WithIdempotencyExpiration sets the idempotencyExpiration request header.
-func WithIdempotencyExpiration(idempotencyExpiration int) *core.IdempotencyExpirationOption {
- return option.WithIdempotencyExpiration(idempotencyExpiration)
-}
diff --git a/seed/go-sdk/idempotency-headers/core/api_error.go b/seed/go-sdk/idempotency-headers/core/api_error.go
deleted file mode 100644
index 6168388541b4..000000000000
--- a/seed/go-sdk/idempotency-headers/core/api_error.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package core
-
-import (
- "fmt"
- "net/http"
-)
-
-// APIError is a lightweight wrapper around the standard error
-// interface that preserves the status code from the RPC, if any.
-type APIError struct {
- err error
-
- StatusCode int `json:"-"`
- Header http.Header `json:"-"`
-}
-
-// NewAPIError constructs a new API error.
-func NewAPIError(statusCode int, header http.Header, err error) *APIError {
- return &APIError{
- err: err,
- Header: header,
- StatusCode: statusCode,
- }
-}
-
-// Unwrap returns the underlying error. This also makes the error compatible
-// with errors.As and errors.Is.
-func (a *APIError) Unwrap() error {
- if a == nil {
- return nil
- }
- return a.err
-}
-
-// Error returns the API error's message.
-func (a *APIError) Error() string {
- if a == nil || (a.err == nil && a.StatusCode == 0) {
- return ""
- }
- if a.err == nil {
- return fmt.Sprintf("%d", a.StatusCode)
- }
- if a.StatusCode == 0 {
- return a.err.Error()
- }
- return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
-}
diff --git a/seed/go-sdk/idempotency-headers/core/http.go b/seed/go-sdk/idempotency-headers/core/http.go
deleted file mode 100644
index 92c435692940..000000000000
--- a/seed/go-sdk/idempotency-headers/core/http.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package core
-
-import "net/http"
-
-// HTTPClient is an interface for a subset of the *http.Client.
-type HTTPClient interface {
- Do(*http.Request) (*http.Response, error)
-}
-
-// Response is an HTTP response from an HTTP client.
-type Response[T any] struct {
- StatusCode int
- Header http.Header
- Body T
-}
diff --git a/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go b/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
deleted file mode 100644
index 48e4e5628411..000000000000
--- a/seed/go-sdk/idempotency-headers/core/idempotent_request_option.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package core
-
-import (
- fmt "fmt"
- http "net/http"
-)
-
-// IdempotentRequestOption adapts the behavior of an individual request.
-type IdempotentRequestOption interface {
- applyIdempotentRequestOptions(*IdempotentRequestOptions)
-}
-
-// IdempotentRequestOptions defines all of the possible idempotent request options.
-//
-// This type is primarily used by the generated code and is not meant
-// to be used directly; use the option package instead.
-type IdempotentRequestOptions struct {
- *RequestOptions
-
- IdempotencyKey string
- IdempotencyExpiration int
-}
-
-// NewIdempotentRequestOptions returns a new *IdempotentRequestOptions value.
-//
-// This function is primarily used by the generated code and is not meant
-// to be used directly; use IdempotentRequestOption instead.
-func NewIdempotentRequestOptions(opts ...IdempotentRequestOption) *IdempotentRequestOptions {
- options := &IdempotentRequestOptions{
- RequestOptions: NewRequestOptions(),
- }
- for _, opt := range opts {
- if requestOption, ok := opt.(RequestOption); ok {
- requestOption.applyRequestOptions(options.RequestOptions)
- }
- opt.applyIdempotentRequestOptions(options)
- }
- return options
-}
-
-// IdempotencyKeyOption implements the RequestOption interface.
-type IdempotencyKeyOption struct {
- IdempotencyKey string
-}
-
-func (i *IdempotencyKeyOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.IdempotencyKey = i.IdempotencyKey
-}
-
-// IdempotencyExpirationOption implements the RequestOption interface.
-type IdempotencyExpirationOption struct {
- IdempotencyExpiration int
-}
-
-func (i *IdempotencyExpirationOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.IdempotencyExpiration = i.IdempotencyExpiration
-}
-
-// ToHeader maps the configured request options into a http.Header used
-// for the request.
-func (i *IdempotentRequestOptions) ToHeader() http.Header {
- header := i.RequestOptions.ToHeader()
- if i.IdempotencyKey != "" {
- header.Set("Idempotency-Key", fmt.Sprintf("%v", i.IdempotencyKey))
- }
- if i.IdempotencyExpiration != 0 {
- header.Set("Idempotency-Expiration", fmt.Sprintf("%v", i.IdempotencyExpiration))
- }
- return header
-}
diff --git a/seed/go-sdk/idempotency-headers/core/request_option.go b/seed/go-sdk/idempotency-headers/core/request_option.go
deleted file mode 100644
index 99b12cc2f920..000000000000
--- a/seed/go-sdk/idempotency-headers/core/request_option.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package core
-
-import (
- http "net/http"
- url "net/url"
-)
-
-// RequestOption adapts the behavior of the client or an individual request.
-type RequestOption interface {
- applyRequestOptions(*RequestOptions)
-}
-
-// RequestOptions defines all of the possible request options.
-//
-// This type is primarily used by the generated code and is not meant
-// to be used directly; use the option package instead.
-type RequestOptions struct {
- BaseURL string
- HTTPClient HTTPClient
- HTTPHeader http.Header
- BodyProperties map[string]interface{}
- QueryParameters url.Values
- MaxAttempts uint
- Token string
-}
-
-// NewRequestOptions returns a new *RequestOptions value.
-//
-// This function is primarily used by the generated code and is not meant
-// to be used directly; use RequestOption instead.
-func NewRequestOptions(opts ...RequestOption) *RequestOptions {
- options := &RequestOptions{
- HTTPHeader: make(http.Header),
- BodyProperties: make(map[string]interface{}),
- QueryParameters: make(url.Values),
- }
- for _, opt := range opts {
- opt.applyRequestOptions(options)
- }
- return options
-}
-
-// ToHeader maps the configured request options into a http.Header used
-// for the request(s).
-func (r *RequestOptions) ToHeader() http.Header {
- header := r.cloneHeader()
- if r.Token != "" {
- header.Set("Authorization", "Bearer "+r.Token)
- }
- return header
-}
-
-func (r *RequestOptions) cloneHeader() http.Header {
- headers := r.HTTPHeader.Clone()
- headers.Set("X-Fern-Language", "Go")
- headers.Set("X-Fern-SDK-Name", "github.com/idempotency-headers/fern")
- headers.Set("X-Fern-SDK-Version", "v0.0.1")
- headers.Set("User-Agent", "github.com/idempotency-headers/fern/0.0.1")
- return headers
-}
-
-// BaseURLOption implements the RequestOption interface.
-type BaseURLOption struct {
- BaseURL string
-}
-
-func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
- opts.BaseURL = b.BaseURL
-}
-
-func (b *BaseURLOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.BaseURL = b.BaseURL
-}
-
-// HTTPClientOption implements the RequestOption interface.
-type HTTPClientOption struct {
- HTTPClient HTTPClient
-}
-
-func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
- opts.HTTPClient = h.HTTPClient
-}
-
-func (h *HTTPClientOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.HTTPClient = h.HTTPClient
-}
-
-// HTTPHeaderOption implements the RequestOption interface.
-type HTTPHeaderOption struct {
- HTTPHeader http.Header
-}
-
-func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
- opts.HTTPHeader = h.HTTPHeader
-}
-
-func (h *HTTPHeaderOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.HTTPHeader = h.HTTPHeader
-}
-
-// BodyPropertiesOption implements the RequestOption interface.
-type BodyPropertiesOption struct {
- BodyProperties map[string]interface{}
-}
-
-func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
- opts.BodyProperties = b.BodyProperties
-}
-
-func (b *BodyPropertiesOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.BodyProperties = b.BodyProperties
-}
-
-// QueryParametersOption implements the RequestOption interface.
-type QueryParametersOption struct {
- QueryParameters url.Values
-}
-
-func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
- opts.QueryParameters = q.QueryParameters
-}
-
-func (q *QueryParametersOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.QueryParameters = q.QueryParameters
-}
-
-// MaxAttemptsOption implements the RequestOption interface.
-type MaxAttemptsOption struct {
- MaxAttempts uint
-}
-
-func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
- opts.MaxAttempts = m.MaxAttempts
-}
-
-func (m *MaxAttemptsOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.MaxAttempts = m.MaxAttempts
-}
-
-// TokenOption implements the RequestOption interface.
-type TokenOption struct {
- Token string
-}
-
-func (t *TokenOption) applyRequestOptions(opts *RequestOptions) {
- opts.Token = t.Token
-}
-
-func (t *TokenOption) applyIdempotentRequestOptions(opts *IdempotentRequestOptions) {
- opts.Token = t.Token
-}
diff --git a/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go b/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
deleted file mode 100644
index a4d13a04d72e..000000000000
--- a/seed/go-sdk/idempotency-headers/dynamic-snippets/example0/snippet.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package example
-
-import (
- client "github.com/idempotency-headers/fern/client"
- option "github.com/idempotency-headers/fern/option"
- fern "github.com/idempotency-headers/fern"
- context "context"
-)
-
-func do() {
- client := client.NewClient(
- option.WithBaseURL(
- "https://api.fern.com",
- ),
- option.WithToken(
- "",
- ),
- )
- request := &fern.CreatePaymentRequest{
- Amount: 1,
- Currency: fern.CurrencyUsd,
- }
- client.Payment.Create(
- context.TODO(),
- request,
- )
-}
diff --git a/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go b/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
deleted file mode 100644
index 45e01220fe25..000000000000
--- a/seed/go-sdk/idempotency-headers/dynamic-snippets/example1/snippet.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package example
-
-import (
- client "github.com/idempotency-headers/fern/client"
- option "github.com/idempotency-headers/fern/option"
- context "context"
-)
-
-func do() {
- client := client.NewClient(
- option.WithBaseURL(
- "https://api.fern.com",
- ),
- option.WithToken(
- "",
- ),
- )
- client.Payment.Delete(
- context.TODO(),
- "paymentId",
- )
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/caller.go b/seed/go-sdk/idempotency-headers/internal/caller.go
deleted file mode 100644
index 6cc9c680f1bb..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/caller.go
+++ /dev/null
@@ -1,250 +0,0 @@
-package internal
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "reflect"
- "strings"
-
- "github.com/idempotency-headers/fern/core"
-)
-
-const (
- // contentType specifies the JSON Content-Type header value.
- contentType = "application/json"
- contentTypeHeader = "Content-Type"
-)
-
-// Caller calls APIs and deserializes their response, if any.
-type Caller struct {
- client core.HTTPClient
- retrier *Retrier
-}
-
-// CallerParams represents the parameters used to constrcut a new *Caller.
-type CallerParams struct {
- Client core.HTTPClient
- MaxAttempts uint
-}
-
-// NewCaller returns a new *Caller backed by the given parameters.
-func NewCaller(params *CallerParams) *Caller {
- var httpClient core.HTTPClient = http.DefaultClient
- if params.Client != nil {
- httpClient = params.Client
- }
- var retryOptions []RetryOption
- if params.MaxAttempts > 0 {
- retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
- }
- return &Caller{
- client: httpClient,
- retrier: NewRetrier(retryOptions...),
- }
-}
-
-// CallParams represents the parameters used to issue an API call.
-type CallParams struct {
- URL string
- Method string
- MaxAttempts uint
- Headers http.Header
- BodyProperties map[string]interface{}
- QueryParameters url.Values
- Client core.HTTPClient
- Request interface{}
- Response interface{}
- ResponseIsOptional bool
- ErrorDecoder ErrorDecoder
-}
-
-// CallResponse is a parsed HTTP response from an API call.
-type CallResponse struct {
- StatusCode int
- Header http.Header
-}
-
-// Call issues an API call according to the given call parameters.
-func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
- url := buildURL(params.URL, params.QueryParameters)
- req, err := newRequest(
- ctx,
- url,
- params.Method,
- params.Headers,
- params.Request,
- params.BodyProperties,
- )
- if err != nil {
- return nil, err
- }
-
- // If the call has been cancelled, don't issue the request.
- if err := ctx.Err(); err != nil {
- return nil, err
- }
-
- client := c.client
- if params.Client != nil {
- // Use the HTTP client scoped to the request.
- client = params.Client
- }
-
- var retryOptions []RetryOption
- if params.MaxAttempts > 0 {
- retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
- }
-
- resp, err := c.retrier.Run(
- client.Do,
- req,
- params.ErrorDecoder,
- retryOptions...,
- )
- if err != nil {
- return nil, err
- }
-
- // Close the response body after we're done.
- defer resp.Body.Close()
-
- // Check if the call was cancelled before we return the error
- // associated with the call and/or unmarshal the response data.
- if err := ctx.Err(); err != nil {
- return nil, err
- }
-
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- return nil, decodeError(resp, params.ErrorDecoder)
- }
-
- // Mutate the response parameter in-place.
- if params.Response != nil {
- if writer, ok := params.Response.(io.Writer); ok {
- _, err = io.Copy(writer, resp.Body)
- } else {
- err = json.NewDecoder(resp.Body).Decode(params.Response)
- }
- if err != nil {
- if err == io.EOF {
- if params.ResponseIsOptional {
- // The response is optional, so we should ignore the
- // io.EOF error
- return &CallResponse{
- StatusCode: resp.StatusCode,
- Header: resp.Header,
- }, nil
- }
- return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
- }
- return nil, err
- }
- }
-
- return &CallResponse{
- StatusCode: resp.StatusCode,
- Header: resp.Header,
- }, nil
-}
-
-// buildURL constructs the final URL by appending the given query parameters (if any).
-func buildURL(
- url string,
- queryParameters url.Values,
-) string {
- if len(queryParameters) == 0 {
- return url
- }
- if strings.ContainsRune(url, '?') {
- url += "&"
- } else {
- url += "?"
- }
- url += queryParameters.Encode()
- return url
-}
-
-// newRequest returns a new *http.Request with all of the fields
-// required to issue the call.
-func newRequest(
- ctx context.Context,
- url string,
- method string,
- endpointHeaders http.Header,
- request interface{},
- bodyProperties map[string]interface{},
-) (*http.Request, error) {
- requestBody, err := newRequestBody(request, bodyProperties)
- if err != nil {
- return nil, err
- }
- req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- req.Header.Set(contentTypeHeader, contentType)
- for name, values := range endpointHeaders {
- req.Header[name] = values
- }
- return req, nil
-}
-
-// newRequestBody returns a new io.Reader that represents the HTTP request body.
-func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
- if isNil(request) {
- if len(bodyProperties) == 0 {
- return nil, nil
- }
- requestBytes, err := json.Marshal(bodyProperties)
- if err != nil {
- return nil, err
- }
- return bytes.NewReader(requestBytes), nil
- }
- if body, ok := request.(io.Reader); ok {
- return body, nil
- }
- requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
- if err != nil {
- return nil, err
- }
- return bytes.NewReader(requestBytes), nil
-}
-
-// decodeError decodes the error from the given HTTP response. Note that
-// it's the caller's responsibility to close the response body.
-func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
- if errorDecoder != nil {
- // This endpoint has custom errors, so we'll
- // attempt to unmarshal the error into a structured
- // type based on the status code.
- return errorDecoder(response.StatusCode, response.Header, response.Body)
- }
- // This endpoint doesn't have any custom error
- // types, so we just read the body as-is, and
- // put it into a normal error.
- bytes, err := io.ReadAll(response.Body)
- if err != nil && err != io.EOF {
- return err
- }
- if err == io.EOF {
- // The error didn't have a response body,
- // so all we can do is return an error
- // with the status code.
- return core.NewAPIError(response.StatusCode, response.Header, nil)
- }
- return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
-}
-
-// isNil is used to determine if the request value is equal to nil (i.e. an interface
-// value that holds a nil concrete value is itself non-nil).
-func isNil(value interface{}) bool {
- return value == nil || reflect.ValueOf(value).IsNil()
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/caller_test.go b/seed/go-sdk/idempotency-headers/internal/caller_test.go
deleted file mode 100644
index 6a9c0950126d..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/caller_test.go
+++ /dev/null
@@ -1,395 +0,0 @@
-package internal
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "net/url"
- "strconv"
- "testing"
-
- "github.com/idempotency-headers/fern/core"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// InternalTestCase represents a single test case.
-type InternalTestCase struct {
- description string
-
- // Server-side assertions.
- givePathSuffix string
- giveMethod string
- giveResponseIsOptional bool
- giveHeader http.Header
- giveErrorDecoder ErrorDecoder
- giveRequest *InternalTestRequest
- giveQueryParams url.Values
- giveBodyProperties map[string]interface{}
-
- // Client-side assertions.
- wantResponse *InternalTestResponse
- wantHeaders http.Header
- wantError error
-}
-
-// InternalTestRequest a simple request body.
-type InternalTestRequest struct {
- Id string `json:"id"`
-}
-
-// InternalTestResponse a simple response body.
-type InternalTestResponse struct {
- Id string `json:"id"`
- ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
- QueryParameters url.Values `json:"queryParameters,omitempty"`
-}
-
-// InternalTestNotFoundError represents a 404.
-type InternalTestNotFoundError struct {
- *core.APIError
-
- Message string `json:"message"`
-}
-
-func TestCall(t *testing.T) {
- tests := []*InternalTestCase{
- {
- description: "GET success",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- },
- },
- {
- description: "GET success with query",
- givePathSuffix: "?limit=1",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "limit": []string{"1"},
- },
- },
- },
- {
- description: "GET not found",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: &InternalTestRequest{
- Id: strconv.Itoa(http.StatusNotFound),
- },
- giveErrorDecoder: newTestErrorDecoder(t),
- wantError: &InternalTestNotFoundError{
- APIError: core.NewAPIError(
- http.StatusNotFound,
- http.Header{},
- errors.New(`{"message":"ID \"404\" not found"}`),
- ),
- },
- },
- {
- description: "POST empty body",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: nil,
- wantError: core.NewAPIError(
- http.StatusBadRequest,
- http.Header{},
- errors.New("invalid request"),
- ),
- },
- {
- description: "POST optional response",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- giveResponseIsOptional: true,
- },
- {
- description: "POST API error",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: &InternalTestRequest{
- Id: strconv.Itoa(http.StatusInternalServerError),
- },
- wantError: core.NewAPIError(
- http.StatusInternalServerError,
- http.Header{},
- errors.New("failed to process request"),
- ),
- },
- {
- description: "POST extra properties",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: new(InternalTestRequest),
- giveBodyProperties: map[string]interface{}{
- "key": "value",
- },
- wantResponse: &InternalTestResponse{
- ExtraBodyProperties: map[string]interface{}{
- "key": "value",
- },
- },
- },
- {
- description: "GET extra query parameters",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveQueryParams: url.Values{
- "extra": []string{"true"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "extra": []string{"true"},
- },
- },
- },
- {
- description: "GET merge extra query parameters",
- givePathSuffix: "?limit=1",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- giveQueryParams: url.Values{
- "extra": []string{"true"},
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "limit": []string{"1"},
- "extra": []string{"true"},
- },
- },
- },
- }
- for _, test := range tests {
- t.Run(test.description, func(t *testing.T) {
- var (
- server = newTestServer(t, test)
- client = server.Client()
- )
- caller := NewCaller(
- &CallerParams{
- Client: client,
- },
- )
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL + test.givePathSuffix,
- Method: test.giveMethod,
- Headers: test.giveHeader,
- BodyProperties: test.giveBodyProperties,
- QueryParameters: test.giveQueryParams,
- Request: test.giveRequest,
- Response: &response,
- ResponseIsOptional: test.giveResponseIsOptional,
- ErrorDecoder: test.giveErrorDecoder,
- },
- )
- if test.wantError != nil {
- assert.EqualError(t, err, test.wantError.Error())
- return
- }
- require.NoError(t, err)
- assert.Equal(t, test.wantResponse, response)
- })
- }
-}
-
-func TestMergeHeaders(t *testing.T) {
- t.Run("both empty", func(t *testing.T) {
- merged := MergeHeaders(make(http.Header), make(http.Header))
- assert.Empty(t, merged)
- })
-
- t.Run("empty left", func(t *testing.T) {
- left := make(http.Header)
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
- })
-
- t.Run("empty right", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Version", "0.0.1")
-
- right := make(http.Header)
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
- })
-
- t.Run("single value override", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Version", "0.0.0")
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
- })
-
- t.Run("multiple value override", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Versions", "0.0.0")
-
- right := make(http.Header)
- right.Add("X-API-Versions", "0.0.1")
- right.Add("X-API-Versions", "0.0.2")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
- })
-
- t.Run("disjoint merge", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Tenancy", "test")
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
- assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
- })
-}
-
-// newTestServer returns a new *httptest.Server configured with the
-// given test parameters.
-func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
- return httptest.NewServer(
- http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, tc.giveMethod, r.Method)
- assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
- for header, value := range tc.giveHeader {
- assert.Equal(t, value, r.Header.Values(header))
- }
-
- request := new(InternalTestRequest)
-
- bytes, err := io.ReadAll(r.Body)
- if tc.giveRequest == nil {
- require.Empty(t, bytes)
- w.WriteHeader(http.StatusBadRequest)
- _, err = w.Write([]byte("invalid request"))
- require.NoError(t, err)
- return
- }
- require.NoError(t, err)
- require.NoError(t, json.Unmarshal(bytes, request))
-
- switch request.Id {
- case strconv.Itoa(http.StatusNotFound):
- notFoundError := &InternalTestNotFoundError{
- APIError: &core.APIError{
- StatusCode: http.StatusNotFound,
- },
- Message: fmt.Sprintf("ID %q not found", request.Id),
- }
- bytes, err = json.Marshal(notFoundError)
- require.NoError(t, err)
-
- w.WriteHeader(http.StatusNotFound)
- _, err = w.Write(bytes)
- require.NoError(t, err)
- return
-
- case strconv.Itoa(http.StatusInternalServerError):
- w.WriteHeader(http.StatusInternalServerError)
- _, err = w.Write([]byte("failed to process request"))
- require.NoError(t, err)
- return
- }
-
- if tc.giveResponseIsOptional {
- w.WriteHeader(http.StatusOK)
- return
- }
-
- extraBodyProperties := make(map[string]interface{})
- require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
- delete(extraBodyProperties, "id")
-
- response := &InternalTestResponse{
- Id: request.Id,
- ExtraBodyProperties: extraBodyProperties,
- QueryParameters: r.URL.Query(),
- }
- bytes, err = json.Marshal(response)
- require.NoError(t, err)
-
- _, err = w.Write(bytes)
- require.NoError(t, err)
- },
- ),
- )
-}
-
-// newTestErrorDecoder returns an error decoder suitable for tests.
-func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
- return func(statusCode int, header http.Header, body io.Reader) error {
- raw, err := io.ReadAll(body)
- require.NoError(t, err)
-
- var (
- apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
- decoder = json.NewDecoder(bytes.NewReader(raw))
- )
- if statusCode == http.StatusNotFound {
- value := new(InternalTestNotFoundError)
- value.APIError = apiError
- require.NoError(t, decoder.Decode(value))
-
- return value
- }
- return apiError
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/error_decoder.go b/seed/go-sdk/idempotency-headers/internal/error_decoder.go
deleted file mode 100644
index 07dd5e0c787c..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/error_decoder.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
-
- "github.com/idempotency-headers/fern/core"
-)
-
-// ErrorCodes maps HTTP status codes to error constructors.
-type ErrorCodes map[int]func(*core.APIError) error
-
-// ErrorDecoder decodes *http.Response errors and returns a
-// typed API error (e.g. *core.APIError).
-type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
-
-// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
-// errorCodesOverrides is optional and will be merged with the default error codes,
-// with overrides taking precedence.
-func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
- // Merge default error codes with overrides
- mergedErrorCodes := make(ErrorCodes)
-
- // Start with default error codes
- for statusCode, errorFunc := range errorCodes {
- mergedErrorCodes[statusCode] = errorFunc
- }
-
- // Apply overrides if provided
- if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
- for statusCode, errorFunc := range errorCodesOverrides[0] {
- mergedErrorCodes[statusCode] = errorFunc
- }
- }
-
- return func(statusCode int, header http.Header, body io.Reader) error {
- raw, err := io.ReadAll(body)
- if err != nil {
- return fmt.Errorf("failed to read error from response body: %w", err)
- }
- apiError := core.NewAPIError(
- statusCode,
- header,
- errors.New(string(raw)),
- )
- newErrorFunc, ok := mergedErrorCodes[statusCode]
- if !ok {
- // This status code isn't recognized, so we return
- // the API error as-is.
- return apiError
- }
- customError := newErrorFunc(apiError)
- if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
- // If we fail to decode the error, we return the
- // API error as-is.
- return apiError
- }
- return customError
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go b/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
deleted file mode 100644
index 1cefd210fcb8..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/error_decoder_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package internal
-
-import (
- "bytes"
- "errors"
- "net/http"
- "testing"
-
- "github.com/idempotency-headers/fern/core"
- "github.com/stretchr/testify/assert"
-)
-
-func TestErrorDecoder(t *testing.T) {
- decoder := NewErrorDecoder(
- ErrorCodes{
- http.StatusNotFound: func(apiError *core.APIError) error {
- return &InternalTestNotFoundError{APIError: apiError}
- },
- })
-
- tests := []struct {
- description string
- giveStatusCode int
- giveHeader http.Header
- giveBody string
- wantError error
- }{
- {
- description: "unrecognized status code",
- giveStatusCode: http.StatusInternalServerError,
- giveHeader: http.Header{},
- giveBody: "Internal Server Error",
- wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
- },
- {
- description: "not found with valid JSON",
- giveStatusCode: http.StatusNotFound,
- giveHeader: http.Header{},
- giveBody: `{"message": "Resource not found"}`,
- wantError: &InternalTestNotFoundError{
- APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
- Message: "Resource not found",
- },
- },
- {
- description: "not found with invalid JSON",
- giveStatusCode: http.StatusNotFound,
- giveHeader: http.Header{},
- giveBody: `Resource not found`,
- wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.description, func(t *testing.T) {
- assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
- })
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/explicit_fields.go b/seed/go-sdk/idempotency-headers/internal/explicit_fields.go
deleted file mode 100644
index 4bdf34fc2b7c..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/explicit_fields.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package internal
-
-import (
- "math/big"
- "reflect"
- "strings"
-)
-
-// HandleExplicitFields processes a struct to remove `omitempty` from
-// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
-// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
-// Returns an interface{} that can be passed to json.Marshal.
-func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
- val := reflect.ValueOf(marshaler)
- typ := reflect.TypeOf(marshaler)
-
- // Handle pointer types
- if val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil
- }
- val = val.Elem()
- typ = typ.Elem()
- }
-
- // Only handle struct types
- if val.Kind() != reflect.Struct {
- return marshaler
- }
-
- // Handle embedded struct pattern
- var sourceVal reflect.Value
- var sourceType reflect.Type
-
- // Check if this is an embedded struct pattern
- if typ.NumField() == 1 && typ.Field(0).Anonymous {
- // This is likely an embedded struct, get the embedded value
- embeddedField := val.Field(0)
- sourceVal = embeddedField
- sourceType = embeddedField.Type()
- } else {
- // Regular struct
- sourceVal = val
- sourceType = typ
- }
-
- // If no explicit fields set, use standard marshaling
- if explicitFields == nil || explicitFields.Sign() == 0 {
- return marshaler
- }
-
- // Create a new struct type with modified tags
- fields := make([]reflect.StructField, 0, sourceType.NumField())
-
- for i := 0; i < sourceType.NumField(); i++ {
- field := sourceType.Field(i)
-
- // Skip unexported fields and the explicitFields field itself
- if !field.IsExported() || field.Name == "explicitFields" {
- continue
- }
-
- // Check if this field has been explicitly set
- fieldBit := big.NewInt(1)
- fieldBit.Lsh(fieldBit, uint(i))
- if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
- // Remove omitempty from the json tag
- tag := field.Tag.Get("json")
- if tag != "" && tag != "-" {
- // Parse the json tag, remove omitempty from options
- parts := strings.Split(tag, ",")
- if len(parts) > 1 {
- var newParts []string
- newParts = append(newParts, parts[0]) // Keep the field name
- for _, part := range parts[1:] {
- if strings.TrimSpace(part) != "omitempty" {
- newParts = append(newParts, part)
- }
- }
- tag = strings.Join(newParts, ",")
- }
-
- // Reconstruct the struct tag
- newTag := `json:"` + tag + `"`
- if urlTag := field.Tag.Get("url"); urlTag != "" {
- newTag += ` url:"` + urlTag + `"`
- }
-
- field.Tag = reflect.StructTag(newTag)
- }
- }
-
- fields = append(fields, field)
- }
-
- // Create new struct type with modified tags
- newType := reflect.StructOf(fields)
- newVal := reflect.New(newType).Elem()
-
- // Copy field values from original struct to new struct
- fieldIndex := 0
- for i := 0; i < sourceType.NumField(); i++ {
- originalField := sourceType.Field(i)
-
- // Skip unexported fields and the explicitFields field itself
- if !originalField.IsExported() || originalField.Name == "explicitFields" {
- continue
- }
-
- originalValue := sourceVal.Field(i)
- newVal.Field(fieldIndex).Set(originalValue)
- fieldIndex++
- }
-
- return newVal.Interface()
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go b/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
deleted file mode 100644
index 3d05e88a2ce9..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/explicit_fields_test.go
+++ /dev/null
@@ -1,497 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "math/big"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testExplicitFieldsStruct struct {
- Name *string `json:"name,omitempty"`
- Code *string `json:"code,omitempty"`
- Count *int `json:"count,omitempty"`
- Enabled *bool `json:"enabled,omitempty"`
- Tags []string `json:"tags,omitempty"`
- //lint:ignore unused this field is intentionally unused for testing
- unexported string `json:"-"`
- explicitFields *big.Int `json:"-"`
-}
-
-var (
- testFieldName = big.NewInt(1 << 0)
- testFieldCode = big.NewInt(1 << 1)
- testFieldCount = big.NewInt(1 << 2)
- testFieldEnabled = big.NewInt(1 << 3)
- testFieldTags = big.NewInt(1 << 4)
-)
-
-func (t *testExplicitFieldsStruct) require(field *big.Int) {
- if t.explicitFields == nil {
- t.explicitFields = big.NewInt(0)
- }
- t.explicitFields.Or(t.explicitFields, field)
-}
-
-func (t *testExplicitFieldsStruct) SetName(name *string) {
- t.Name = name
- t.require(testFieldName)
-}
-
-func (t *testExplicitFieldsStruct) SetCode(code *string) {
- t.Code = code
- t.require(testFieldCode)
-}
-
-func (t *testExplicitFieldsStruct) SetCount(count *int) {
- t.Count = count
- t.require(testFieldCount)
-}
-
-func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
- t.Enabled = enabled
- t.require(testFieldEnabled)
-}
-
-func (t *testExplicitFieldsStruct) SetTags(tags []string) {
- t.Tags = tags
- t.require(testFieldTags)
-}
-
-func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*t),
- }
- return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
-}
-
-type testStructWithoutExplicitFields struct {
- Name *string `json:"name,omitempty"`
- Code *string `json:"code,omitempty"`
-}
-
-func TestHandleExplicitFields(t *testing.T) {
- tests := []struct {
- desc string
- giveInput interface{}
- wantBytes []byte
- wantError string
- }{
- {
- desc: "nil input",
- giveInput: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "non-struct input",
- giveInput: "string",
- wantBytes: []byte(`"string"`),
- },
- {
- desc: "slice input",
- giveInput: []string{"a", "b"},
- wantBytes: []byte(`["a","b"]`),
- },
- {
- desc: "map input",
- giveInput: map[string]interface{}{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "struct without explicitFields field",
- giveInput: &testStructWithoutExplicitFields{
- Name: stringPtr("test"),
- Code: nil,
- },
- wantBytes: []byte(`{"name":"test"}`),
- },
- {
- desc: "struct with no explicit fields set",
- giveInput: &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- Code: nil,
- },
- wantBytes: []byte(`{"name":"test"}`),
- },
- {
- desc: "struct with explicit nil field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- }
- s.SetCode(nil)
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null}`),
- },
- {
- desc: "struct with explicit non-nil field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetName(stringPtr("explicit"))
- s.SetCode(stringPtr("also-explicit"))
- return s
- }(),
- wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
- },
- {
- desc: "struct with mixed explicit and implicit fields",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Count: intPtr(42),
- }
- s.SetCode(nil) // explicit nil
- return s
- }(),
- wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
- },
- {
- desc: "struct with multiple explicit nil fields",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- }
- s.SetCode(nil)
- s.SetCount(nil)
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
- },
- {
- desc: "struct with slice field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Tags: []string{"tag1", "tag2"},
- }
- s.SetTags(nil) // explicit nil slice
- return s
- }(),
- wantBytes: []byte(`{"tags":null}`),
- },
- {
- desc: "struct with boolean field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetEnabled(boolPtr(false)) // explicit false
- return s
- }(),
- wantBytes: []byte(`{"enabled":false}`),
- },
- {
- desc: "struct with all fields explicit",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetName(stringPtr("test"))
- s.SetCode(nil)
- s.SetCount(intPtr(0))
- s.SetEnabled(boolPtr(false))
- s.SetTags([]string{})
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- var explicitFields *big.Int
- if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
- explicitFields = s.explicitFields
- }
- bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.JSONEq(t, string(tt.wantBytes), string(bytes))
-
- // Verify it's valid JSON
- var value interface{}
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
- t.Run("custom marshaler with explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
- s.SetCode(stringPtr("test-code"))
-
- bytes, err := s.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
- })
-
- t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Code: stringPtr("also-implicit"),
- }
-
- bytes, err := s.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
- t.Run("nil pointer", func(t *testing.T) {
- var s *testExplicitFieldsStruct
- bytes, err := json.Marshal(HandleExplicitFields(s, nil))
- require.NoError(t, err)
- assert.Equal(t, []byte(`null`), bytes)
- })
-
- t.Run("pointer to struct", func(t *testing.T) {
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
-
- bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":null}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
- t.Run("embedded struct with explicit fields", func(t *testing.T) {
- // Create a struct similar to what MarshalJSON creates
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
- s.SetCode(stringPtr("test-code"))
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should include both explicit fields (name as null, code as "test-code")
- assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
- })
-
- t.Run("embedded struct with no explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Code: stringPtr("also-implicit"),
- }
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should only include non-nil fields (omitempty behavior)
- assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
- })
-
- t.Run("embedded struct with mixed fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Count: intPtr(42), // implicit field
- }
- s.SetName(nil) // explicit nil
- s.SetCode(stringPtr("explicit")) // explicit value
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should include explicit null, explicit value, and implicit value
- assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsTagHandling(t *testing.T) {
- type testStructWithComplexTags struct {
- Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
- Field2 *string `json:"field2,omitempty,string" url:"field2"`
- Field3 *string `json:"-"`
- Field4 *string `json:"field4"`
- explicitFields *big.Int `json:"-"`
- }
-
- s := &testStructWithComplexTags{
- Field1: stringPtr("test1"),
- Field4: stringPtr("test4"),
- explicitFields: big.NewInt(1), // Only first field is explicit
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
- require.NoError(t, err)
-
- // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
- assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
-}
-
-// Test types for nested struct explicit fields testing
-type testNestedStruct struct {
- NestedName *string `json:"nested_name,omitempty"`
- NestedCode *string `json:"nested_code,omitempty"`
- explicitFields *big.Int `json:"-"`
-}
-
-type testParentStruct struct {
- ParentName *string `json:"parent_name,omitempty"`
- Nested *testNestedStruct `json:"nested,omitempty"`
- explicitFields *big.Int `json:"-"`
-}
-
-var (
- nestedFieldName = big.NewInt(1 << 0)
- nestedFieldCode = big.NewInt(1 << 1)
-)
-
-var (
- parentFieldName = big.NewInt(1 << 0)
- parentFieldNested = big.NewInt(1 << 1)
-)
-
-func (n *testNestedStruct) require(field *big.Int) {
- if n.explicitFields == nil {
- n.explicitFields = big.NewInt(0)
- }
- n.explicitFields.Or(n.explicitFields, field)
-}
-
-func (n *testNestedStruct) SetNestedName(name *string) {
- n.NestedName = name
- n.require(nestedFieldName)
-}
-
-func (n *testNestedStruct) SetNestedCode(code *string) {
- n.NestedCode = code
- n.require(nestedFieldCode)
-}
-
-func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
- type embed testNestedStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*n),
- }
- return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
-}
-
-func (p *testParentStruct) require(field *big.Int) {
- if p.explicitFields == nil {
- p.explicitFields = big.NewInt(0)
- }
- p.explicitFields.Or(p.explicitFields, field)
-}
-
-func (p *testParentStruct) SetParentName(name *string) {
- p.ParentName = name
- p.require(parentFieldName)
-}
-
-func (p *testParentStruct) SetNested(nested *testNestedStruct) {
- p.Nested = nested
- p.require(parentFieldNested)
-}
-
-func (p *testParentStruct) MarshalJSON() ([]byte, error) {
- type embed testParentStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*p),
- }
- return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
-}
-
-func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
- tests := []struct {
- desc string
- setupFunc func() *testParentStruct
- wantBytes []byte
- }{
- {
- desc: "nested struct with explicit nil in nested object",
- setupFunc: func() *testParentStruct {
- nested := &testNestedStruct{
- NestedName: stringPtr("implicit-nested"),
- }
- nested.SetNestedCode(nil) // explicit nil
-
- return &testParentStruct{
- ParentName: stringPtr("implicit-parent"),
- Nested: nested,
- }
- },
- wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
- },
- {
- desc: "parent with explicit nil nested struct",
- setupFunc: func() *testParentStruct {
- parent := &testParentStruct{
- ParentName: stringPtr("implicit-parent"),
- }
- parent.SetNested(nil) // explicit nil nested struct
- return parent
- },
- wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
- },
- {
- desc: "all explicit fields in nested structure",
- setupFunc: func() *testParentStruct {
- nested := &testNestedStruct{}
- nested.SetNestedName(stringPtr("explicit-nested"))
- nested.SetNestedCode(nil) // explicit nil
-
- parent := &testParentStruct{}
- parent.SetParentName(nil) // explicit nil
- parent.SetNested(nested) // explicit nested struct
-
- return parent
- },
- wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- parent := tt.setupFunc()
- bytes, err := parent.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, string(tt.wantBytes), string(bytes))
-
- // Verify it's valid JSON
- var value interface{}
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-// Helper functions
-func stringPtr(s string) *string {
- return &s
-}
-
-func intPtr(i int) *int {
- return &i
-}
-
-func boolPtr(b bool) *bool {
- return &b
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/extra_properties.go b/seed/go-sdk/idempotency-headers/internal/extra_properties.go
deleted file mode 100644
index 540c3fd89eeb..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/extra_properties.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "reflect"
- "strings"
-)
-
-// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
-func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
- return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
-}
-
-// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
-func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
- bytes, err := json.Marshal(marshaler)
- if err != nil {
- return nil, err
- }
- if len(extraProperties) == 0 {
- return bytes, nil
- }
- keys, err := getKeys(marshaler)
- if err != nil {
- return nil, err
- }
- for _, key := range keys {
- if _, ok := extraProperties[key]; ok {
- return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
- }
- }
- extraBytes, err := json.Marshal(extraProperties)
- if err != nil {
- return nil, err
- }
- if isEmptyJSON(bytes) {
- if isEmptyJSON(extraBytes) {
- return bytes, nil
- }
- return extraBytes, nil
- }
- result := bytes[:len(bytes)-1]
- result = append(result, ',')
- result = append(result, extraBytes[1:len(extraBytes)-1]...)
- result = append(result, '}')
- return result, nil
-}
-
-// ExtractExtraProperties extracts any extra properties from the given value.
-func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
- val := reflect.ValueOf(value)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil, fmt.Errorf("value must be non-nil to extract extra properties")
- }
- val = val.Elem()
- }
- if err := json.Unmarshal(bytes, &value); err != nil {
- return nil, err
- }
- var extraProperties map[string]interface{}
- if err := json.Unmarshal(bytes, &extraProperties); err != nil {
- return nil, err
- }
- for i := 0; i < val.Type().NumField(); i++ {
- key := jsonKey(val.Type().Field(i))
- if key == "" || key == "-" {
- continue
- }
- delete(extraProperties, key)
- }
- for _, key := range exclude {
- delete(extraProperties, key)
- }
- if len(extraProperties) == 0 {
- return nil, nil
- }
- return extraProperties, nil
-}
-
-// getKeys returns the keys associated with the given value. The value must be a
-// a struct or a map with string keys.
-func getKeys(value interface{}) ([]string, error) {
- val := reflect.ValueOf(value)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- if !val.IsValid() {
- return nil, nil
- }
- switch val.Kind() {
- case reflect.Struct:
- return getKeysForStructType(val.Type()), nil
- case reflect.Map:
- var keys []string
- if val.Type().Key().Kind() != reflect.String {
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
- for _, key := range val.MapKeys() {
- keys = append(keys, key.String())
- }
- return keys, nil
- default:
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
-}
-
-// getKeysForStructType returns all the keys associated with the given struct type,
-// visiting embedded fields recursively.
-func getKeysForStructType(structType reflect.Type) []string {
- if structType.Kind() == reflect.Pointer {
- structType = structType.Elem()
- }
- if structType.Kind() != reflect.Struct {
- return nil
- }
- var keys []string
- for i := 0; i < structType.NumField(); i++ {
- field := structType.Field(i)
- if field.Anonymous {
- keys = append(keys, getKeysForStructType(field.Type)...)
- continue
- }
- keys = append(keys, jsonKey(field))
- }
- return keys
-}
-
-// jsonKey returns the JSON key from the struct tag of the given field,
-// excluding the omitempty flag (if any).
-func jsonKey(field reflect.StructField) string {
- return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
-}
-
-// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
-// an explicit null.
-func isEmptyJSON(data []byte) bool {
- return len(data) <= 2 || bytes.Equal(data, []byte("null"))
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go b/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
deleted file mode 100644
index aa2510ee5121..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/extra_properties_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testMarshaler struct {
- Name string `json:"name"`
- BirthDate time.Time `json:"birthDate"`
- CreatedAt time.Time `json:"created_at"`
-}
-
-func (t *testMarshaler) MarshalJSON() ([]byte, error) {
- type embed testMarshaler
- var marshaler = struct {
- embed
- BirthDate string `json:"birthDate"`
- CreatedAt string `json:"created_at"`
- }{
- embed: embed(*t),
- BirthDate: t.BirthDate.Format("2006-01-02"),
- CreatedAt: t.CreatedAt.Format(time.RFC3339),
- }
- return MarshalJSONWithExtraProperty(marshaler, "type", "test")
-}
-
-func TestMarshalJSONWithExtraProperties(t *testing.T) {
- tests := []struct {
- desc string
- giveMarshaler interface{}
- giveExtraProperties map[string]interface{}
- wantBytes []byte
- wantError string
- }{
- {
- desc: "invalid type",
- giveMarshaler: []string{"invalid"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid key type",
- giveMarshaler: map[int]interface{}{42: "value"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid map overwrite",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot add extra property "key" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
- wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite embedded type",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]interface{}{"name": "bob"},
- wantError: `cannot add extra property "name" because it is already defined on the type`,
- },
- {
- desc: "nil",
- giveMarshaler: nil,
- giveExtraProperties: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "empty",
- giveMarshaler: map[string]interface{}{},
- giveExtraProperties: map[string]interface{}{},
- wantBytes: []byte(`{}`),
- },
- {
- desc: "no extra properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "only extra properties",
- giveMarshaler: map[string]interface{}{},
- giveExtraProperties: map[string]interface{}{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "single extra property",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"extra": "property"},
- wantBytes: []byte(`{"key":"value","extra":"property"}`),
- },
- {
- desc: "multiple extra properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
- wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
- },
- {
- desc: "nested properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{
- "user": map[string]interface{}{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "multiple nested properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{
- "metadata": map[string]interface{}{
- "ip": "127.0.0.1",
- },
- "user": map[string]interface{}{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "custom marshaler",
- giveMarshaler: &testMarshaler{
- Name: "alice",
- BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
- CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- giveExtraProperties: map[string]interface{}{
- "extra": "property",
- },
- wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, tt.wantBytes, bytes)
-
- value := make(map[string]interface{})
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestExtractExtraProperties(t *testing.T) {
- t.Run("none", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-
- t.Run("non-nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value *user
- _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- assert.EqualError(t, err, "value must be non-nil to extract extra properties")
- })
-
- t.Run("non-zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value user
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("exclude", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/http.go b/seed/go-sdk/idempotency-headers/internal/http.go
deleted file mode 100644
index 77863752bb58..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/http.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package internal
-
-import (
- "fmt"
- "net/http"
- "net/url"
- "reflect"
-)
-
-// HTTPClient is an interface for a subset of the *http.Client.
-type HTTPClient interface {
- Do(*http.Request) (*http.Response, error)
-}
-
-// ResolveBaseURL resolves the base URL from the given arguments,
-// preferring the first non-empty value.
-func ResolveBaseURL(values ...string) string {
- for _, value := range values {
- if value != "" {
- return value
- }
- }
- return ""
-}
-
-// EncodeURL encodes the given arguments into the URL, escaping
-// values as needed. Pointer arguments are dereferenced before processing.
-func EncodeURL(urlFormat string, args ...interface{}) string {
- escapedArgs := make([]interface{}, 0, len(args))
- for _, arg := range args {
- // Dereference the argument if it's a pointer
- value := dereferenceArg(arg)
- escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
- }
- return fmt.Sprintf(urlFormat, escapedArgs...)
-}
-
-// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
-// If the argument is not a pointer or is nil, it returns the argument as-is.
-func dereferenceArg(arg interface{}) interface{} {
- if arg == nil {
- return arg
- }
-
- v := reflect.ValueOf(arg)
-
- // Keep dereferencing until we get to a non-pointer value or hit nil
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- return nil
- }
- v = v.Elem()
- }
-
- return v.Interface()
-}
-
-// MergeHeaders merges the given headers together, where the right
-// takes precedence over the left.
-func MergeHeaders(left, right http.Header) http.Header {
- for key, values := range right {
- if len(values) > 1 {
- left[key] = values
- continue
- }
- if value := right.Get(key); value != "" {
- left.Set(key, value)
- }
- }
- return left
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/query.go b/seed/go-sdk/idempotency-headers/internal/query.go
deleted file mode 100644
index 1cbaf7fe1c02..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/query.go
+++ /dev/null
@@ -1,353 +0,0 @@
-package internal
-
-import (
- "encoding/base64"
- "fmt"
- "net/url"
- "reflect"
- "strings"
- "time"
-
- "github.com/google/uuid"
-)
-
-var (
- bytesType = reflect.TypeOf([]byte{})
- queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
- timeType = reflect.TypeOf(time.Time{})
- uuidType = reflect.TypeOf(uuid.UUID{})
-)
-
-// QueryEncoder is an interface implemented by any type that wishes to encode
-// itself into URL values in a non-standard way.
-type QueryEncoder interface {
- EncodeQueryValues(key string, v *url.Values) error
-}
-
-// prepareValue handles common validation and unwrapping logic for both functions
-func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
- values := make(url.Values)
- val := reflect.ValueOf(v)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return reflect.Value{}, values, nil
- }
- val = val.Elem()
- }
-
- if v == nil {
- return reflect.Value{}, values, nil
- }
-
- if val.Kind() != reflect.Struct {
- return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
- }
-
- err := reflectValue(values, val, "")
- if err != nil {
- return reflect.Value{}, nil, err
- }
-
- return val, values, nil
-}
-
-// QueryValues encodes url.Values from request objects.
-//
-// Note: This type is inspired by Google's query encoding library, but
-// supports far less customization and is tailored to fit this SDK's use case.
-//
-// Ref: https://github.com/google/go-querystring
-func QueryValues(v interface{}) (url.Values, error) {
- _, values, err := prepareValue(v)
- return values, err
-}
-
-// QueryValuesWithDefaults encodes url.Values from request objects
-// and default values, merging the defaults into the request.
-// It's expected that the values of defaults are wire names.
-func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
- val, values, err := prepareValue(v)
- if err != nil {
- return values, err
- }
- if !val.IsValid() {
- return values, nil
- }
-
- // apply defaults to zero-value fields directly on the original struct
- valType := val.Type()
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- fieldType := valType.Field(i)
- fieldName := fieldType.Name
-
- if fieldType.PkgPath != "" && !fieldType.Anonymous {
- // Skip unexported fields.
- continue
- }
-
- // check if field is zero value and we have a default for it
- if field.CanSet() && field.IsZero() {
- tag := fieldType.Tag.Get("url")
- if tag == "" || tag == "-" {
- continue
- }
- wireName, _ := parseTag(tag)
- if wireName == "" {
- wireName = fieldName
- }
- if defaultVal, exists := defaults[wireName]; exists {
- values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
- }
- }
- }
-
- return values, err
-}
-
-// reflectValue populates the values parameter from the struct fields in val.
-// Embedded structs are followed recursively (using the rules defined in the
-// Values function documentation) breadth-first.
-func reflectValue(values url.Values, val reflect.Value, scope string) error {
- typ := val.Type()
- for i := 0; i < typ.NumField(); i++ {
- sf := typ.Field(i)
- if sf.PkgPath != "" && !sf.Anonymous {
- // Skip unexported fields.
- continue
- }
-
- sv := val.Field(i)
- tag := sf.Tag.Get("url")
- if tag == "" || tag == "-" {
- continue
- }
-
- name, opts := parseTag(tag)
- if name == "" {
- name = sf.Name
- }
-
- if scope != "" {
- name = scope + "[" + name + "]"
- }
-
- if opts.Contains("omitempty") && isEmptyValue(sv) {
- continue
- }
-
- if sv.Type().Implements(queryEncoderType) {
- // If sv is a nil pointer and the custom encoder is defined on a non-pointer
- // method receiver, set sv to the zero value of the underlying type
- if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
- sv = reflect.New(sv.Type().Elem())
- }
-
- m := sv.Interface().(QueryEncoder)
- if err := m.EncodeQueryValues(name, &values); err != nil {
- return err
- }
- continue
- }
-
- // Recursively dereference pointers, but stop at nil pointers.
- for sv.Kind() == reflect.Ptr {
- if sv.IsNil() {
- break
- }
- sv = sv.Elem()
- }
-
- if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
- values.Add(name, valueString(sv, opts, sf))
- continue
- }
-
- if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
- if sv.Len() == 0 {
- // Skip if slice or array is empty.
- continue
- }
- for i := 0; i < sv.Len(); i++ {
- value := sv.Index(i)
- if isStructPointer(value) && !value.IsNil() {
- if err := reflectValue(values, value.Elem(), name); err != nil {
- return err
- }
- } else {
- values.Add(name, valueString(value, opts, sf))
- }
- }
- continue
- }
-
- if sv.Kind() == reflect.Map {
- if err := reflectMap(values, sv, name); err != nil {
- return err
- }
- continue
- }
-
- if sv.Kind() == reflect.Struct {
- if err := reflectValue(values, sv, name); err != nil {
- return err
- }
- continue
- }
-
- values.Add(name, valueString(sv, opts, sf))
- }
-
- return nil
-}
-
-// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
-func reflectMap(values url.Values, val reflect.Value, scope string) error {
- if val.IsNil() {
- return nil
- }
-
- iter := val.MapRange()
- for iter.Next() {
- k := iter.Key()
- v := iter.Value()
-
- key := fmt.Sprint(k.Interface())
- paramName := scope + "[" + key + "]"
-
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- break
- }
- v = v.Elem()
- }
-
- for v.Kind() == reflect.Interface {
- v = v.Elem()
- }
-
- if v.Kind() == reflect.Map {
- if err := reflectMap(values, v, paramName); err != nil {
- return err
- }
- continue
- }
-
- if v.Kind() == reflect.Struct {
- if err := reflectValue(values, v, paramName); err != nil {
- return err
- }
- continue
- }
-
- if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
- if v.Len() == 0 {
- continue
- }
- for i := 0; i < v.Len(); i++ {
- value := v.Index(i)
- if isStructPointer(value) && !value.IsNil() {
- if err := reflectValue(values, value.Elem(), paramName); err != nil {
- return err
- }
- } else {
- values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
- }
- }
- continue
- }
-
- values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
- }
-
- return nil
-}
-
-// valueString returns the string representation of a value.
-func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- return ""
- }
- v = v.Elem()
- }
-
- if v.Type() == timeType {
- t := v.Interface().(time.Time)
- if format := sf.Tag.Get("format"); format == "date" {
- return t.Format("2006-01-02")
- }
- return t.Format(time.RFC3339)
- }
-
- if v.Type() == uuidType {
- u := v.Interface().(uuid.UUID)
- return u.String()
- }
-
- if v.Type() == bytesType {
- b := v.Interface().([]byte)
- return base64.StdEncoding.EncodeToString(b)
- }
-
- return fmt.Sprint(v.Interface())
-}
-
-// isEmptyValue checks if a value should be considered empty for the purposes
-// of omitting fields with the "omitempty" option.
-func isEmptyValue(v reflect.Value) bool {
- type zeroable interface {
- IsZero() bool
- }
-
- if !v.IsZero() {
- if z, ok := v.Interface().(zeroable); ok {
- return z.IsZero()
- }
- }
-
- switch v.Kind() {
- case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
- return v.Len() == 0
- case reflect.Bool:
- return !v.Bool()
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return v.Int() == 0
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- return v.Uint() == 0
- case reflect.Float32, reflect.Float64:
- return v.Float() == 0
- case reflect.Interface, reflect.Ptr:
- return v.IsNil()
- case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
- return false
- }
-
- return false
-}
-
-// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
-func isStructPointer(v reflect.Value) bool {
- return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
-}
-
-// tagOptions is the string following a comma in a struct field's "url" tag, or
-// the empty string. It does not include the leading comma.
-type tagOptions []string
-
-// parseTag splits a struct field's url tag into its name and comma-separated
-// options.
-func parseTag(tag string) (string, tagOptions) {
- s := strings.Split(tag, ",")
- return s[0], s[1:]
-}
-
-// Contains checks whether the tagOptions contains the specified option.
-func (o tagOptions) Contains(option string) bool {
- for _, s := range o {
- if s == option {
- return true
- }
- }
- return false
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/query_test.go b/seed/go-sdk/idempotency-headers/internal/query_test.go
deleted file mode 100644
index 2c28cb8acf68..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/query_test.go
+++ /dev/null
@@ -1,395 +0,0 @@
-package internal
-
-import (
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestQueryValues(t *testing.T) {
- t.Run("empty optional", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- values, err := QueryValues(&example{})
- require.NoError(t, err)
- assert.Empty(t, values)
- })
-
- t.Run("empty required", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Required string `json:"required" url:"required"`
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- values, err := QueryValues(&example{})
- require.NoError(t, err)
- assert.Equal(t, "required=", values.Encode())
- })
-
- t.Run("allow multiple", func(t *testing.T) {
- type example struct {
- Values []string `json:"values" url:"values"`
- }
-
- values, err := QueryValues(
- &example{
- Values: []string{"foo", "bar", "baz"},
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
- })
-
- t.Run("nested object", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Required string `json:"required" url:"required"`
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- nestedValue := "nestedValue"
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- Nested: &nested{
- Value: &nestedValue,
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
- })
-
- t.Run("url unspecified", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- NotFound string `json:"notFound"`
- }
-
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- NotFound: "notFound",
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "required=requiredValue", values.Encode())
- })
-
- t.Run("url ignored", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- NotFound string `json:"notFound" url:"-"`
- }
-
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- NotFound: "notFound",
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "required=requiredValue", values.Encode())
- })
-
- t.Run("datetime", func(t *testing.T) {
- type example struct {
- DateTime time.Time `json:"dateTime" url:"dateTime"`
- }
-
- values, err := QueryValues(
- &example{
- DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
- })
-
- t.Run("date", func(t *testing.T) {
- type example struct {
- Date time.Time `json:"date" url:"date" format:"date"`
- }
-
- values, err := QueryValues(
- &example{
- Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "date=1994-03-16", values.Encode())
- })
-
- t.Run("optional time", func(t *testing.T) {
- type example struct {
- Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
- }
-
- values, err := QueryValues(
- &example{},
- )
- require.NoError(t, err)
- assert.Empty(t, values.Encode())
- })
-
- t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
- type enum string
-
- type example struct {
- Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
- }
-
- values, err := QueryValues(
- &example{},
- )
- require.NoError(t, err)
- assert.Empty(t, values.Encode())
- })
-
- t.Run("object array", func(t *testing.T) {
- type object struct {
- Key string `json:"key" url:"key"`
- Value string `json:"value" url:"value"`
- }
- type example struct {
- Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
- }
-
- values, err := QueryValues(
- &example{
- Objects: []*object{
- {
- Key: "hello",
- Value: "world",
- },
- {
- Key: "foo",
- Value: "bar",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
- })
-
- t.Run("map", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "foo": "bar",
- "baz": "qux",
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
- })
-
- t.Run("nested map", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "inner": map[string]interface{}{
- "foo": "bar",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
- })
-
- t.Run("nested map array", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "inner": []string{
- "one",
- "two",
- "three",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
- })
-}
-
-func TestQueryValuesWithDefaults(t *testing.T) {
- t.Run("apply defaults to zero values", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- Enabled bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
- })
-
- t.Run("preserve non-zero values over defaults", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- Enabled bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- values, err := QueryValuesWithDefaults(&example{
- Name: "actual-name",
- Age: 30,
- // Enabled remains false (zero value), should get default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
- })
-
- t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "nonexistent": "should-be-ignored",
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&name=default-name", values.Encode())
- })
-
- t.Run("type conversion for compatible defaults", func(t *testing.T) {
- type example struct {
- Count int64 `json:"count" url:"count"`
- Rate float64 `json:"rate" url:"rate"`
- Message string `json:"message" url:"message"`
- }
-
- defaults := map[string]interface{}{
- "count": int(100), // int -> int64 conversion
- "rate": float32(2.5), // float32 -> float64 conversion
- "message": "hello", // string -> string (no conversion needed)
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
- })
-
- t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
- Count int `json:"count,omitempty" url:"count,omitempty"`
- }
-
- defaultOptional := "default-optional"
- defaults := map[string]interface{}{
- "required": "default-required",
- "optional": &defaultOptional, // pointer type
- "count": 42,
- }
-
- values, err := QueryValuesWithDefaults(&example{
- Required: "custom-required", // should override default
- // Optional is nil, should get default
- // Count is 0, should get default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
- })
-
- t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
- type example struct {
- Name *string `json:"name" url:"name"`
- Age *int `json:"age" url:"age"`
- Enabled *bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- // first, test that a properly empty request is overridden:
- {
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
- }
-
- // second, test that a request that contains zeros is not overridden:
- var (
- name = ""
- age = 0
- enabled = false
- )
- values, err := QueryValuesWithDefaults(&example{
- Name: &name, // explicit empty string should override default
- Age: &age, // explicit zero should override default
- Enabled: &enabled, // explicit false should override default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
- })
-
- t.Run("nil input returns empty values", func(t *testing.T) {
- defaults := map[string]any{
- "name": "default-name",
- "age": 25,
- }
-
- // Test with nil
- values, err := QueryValuesWithDefaults(nil, defaults)
- require.NoError(t, err)
- assert.Empty(t, values)
-
- // Test with nil pointer
- type example struct {
- Name string `json:"name" url:"name"`
- }
- var nilPtr *example
- values, err = QueryValuesWithDefaults(nilPtr, defaults)
- require.NoError(t, err)
- assert.Empty(t, values)
- })
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/retrier.go b/seed/go-sdk/idempotency-headers/internal/retrier.go
deleted file mode 100644
index 4efae1b4c286..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/retrier.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package internal
-
-import (
- "crypto/rand"
- "math/big"
- "net/http"
- "strconv"
- "time"
-)
-
-const (
- defaultRetryAttempts = 2
- minRetryDelay = 1000 * time.Millisecond
- maxRetryDelay = 60000 * time.Millisecond
-)
-
-// RetryOption adapts the behavior the *Retrier.
-type RetryOption func(*retryOptions)
-
-// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
-type RetryFunc func(*http.Request) (*http.Response, error)
-
-// WithMaxAttempts configures the maximum number of attempts
-// of the *Retrier.
-func WithMaxAttempts(attempts uint) RetryOption {
- return func(opts *retryOptions) {
- opts.attempts = attempts
- }
-}
-
-// Retrier retries failed requests a configurable number of times with an
-// exponential back-off between each retry.
-type Retrier struct {
- attempts uint
-}
-
-// NewRetrier constructs a new *Retrier with the given options, if any.
-func NewRetrier(opts ...RetryOption) *Retrier {
- options := new(retryOptions)
- for _, opt := range opts {
- opt(options)
- }
- attempts := uint(defaultRetryAttempts)
- if options.attempts > 0 {
- attempts = options.attempts
- }
- return &Retrier{
- attempts: attempts,
- }
-}
-
-// Run issues the request and, upon failure, retries the request if possible.
-//
-// The request will be retried as long as the request is deemed retryable and the
-// number of retry attempts has not grown larger than the configured retry limit.
-func (r *Retrier) Run(
- fn RetryFunc,
- request *http.Request,
- errorDecoder ErrorDecoder,
- opts ...RetryOption,
-) (*http.Response, error) {
- options := new(retryOptions)
- for _, opt := range opts {
- opt(options)
- }
- maxRetryAttempts := r.attempts
- if options.attempts > 0 {
- maxRetryAttempts = options.attempts
- }
- var (
- retryAttempt uint
- previousError error
- )
- return r.run(
- fn,
- request,
- errorDecoder,
- maxRetryAttempts,
- retryAttempt,
- previousError,
- )
-}
-
-func (r *Retrier) run(
- fn RetryFunc,
- request *http.Request,
- errorDecoder ErrorDecoder,
- maxRetryAttempts uint,
- retryAttempt uint,
- previousError error,
-) (*http.Response, error) {
- if retryAttempt >= maxRetryAttempts {
- return nil, previousError
- }
-
- // If the call has been cancelled, don't issue the request.
- if err := request.Context().Err(); err != nil {
- return nil, err
- }
-
- response, err := fn(request)
- if err != nil {
- return nil, err
- }
-
- if r.shouldRetry(response) {
- defer response.Body.Close()
-
- delay, err := r.retryDelay(response, retryAttempt)
- if err != nil {
- return nil, err
- }
-
- time.Sleep(delay)
-
- return r.run(
- fn,
- request,
- errorDecoder,
- maxRetryAttempts,
- retryAttempt+1,
- decodeError(response, errorDecoder),
- )
- }
-
- return response, nil
-}
-
-// shouldRetry returns true if the request should be retried based on the given
-// response status code.
-func (r *Retrier) shouldRetry(response *http.Response) bool {
- return response.StatusCode == http.StatusTooManyRequests ||
- response.StatusCode == http.StatusRequestTimeout ||
- response.StatusCode >= http.StatusInternalServerError
-}
-
-// retryDelay calculates the delay time based on response headers,
-// falling back to exponential backoff if no headers are present.
-func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
- // Check for Retry-After header first (RFC 7231), applying no jitter
- if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
- // Parse as number of seconds...
- if seconds, err := strconv.Atoi(retryAfter); err == nil {
- delay := time.Duration(seconds) * time.Second
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return delay, nil
- }
- }
-
- // ...or as an HTTP date; both are valid
- if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
- delay := time.Until(retryTime)
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return delay, nil
- }
- }
- }
-
- // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
- if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
- if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
- // Assume Unix timestamp in seconds
- resetTime := time.Unix(resetTimestamp, 0)
- delay := time.Until(resetTime)
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return r.addPositiveJitter(delay)
- }
- }
- }
-
- // Fall back to exponential backoff
- return r.exponentialBackoff(retryAttempt)
-}
-
-// exponentialBackoff calculates the delay time based on the retry attempt
-// and applies symmetric jitter (±10% around the delay).
-func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
- if retryAttempt > 63 { // 2^63+ would overflow uint64
- retryAttempt = 63
- }
-
- delay := minRetryDelay << retryAttempt
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
-
- return r.addSymmetricJitter(delay)
-}
-
-// addJitterWithRange applies jitter to the given delay.
-// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
-func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
- jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
- jitter, err := rand.Int(rand.Reader, jitterRange)
- if err != nil {
- return 0, err
- }
-
- jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
- if jitteredDelay < minRetryDelay {
- jitteredDelay = minRetryDelay
- }
- if jitteredDelay > maxRetryDelay {
- jitteredDelay = maxRetryDelay
- }
- return jitteredDelay, nil
-}
-
-// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
-func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
- return r.addJitterWithRange(delay, 100, 120)
-}
-
-// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
-func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
- return r.addJitterWithRange(delay, 90, 110)
-}
-
-type retryOptions struct {
- attempts uint
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/retrier_test.go b/seed/go-sdk/idempotency-headers/internal/retrier_test.go
deleted file mode 100644
index 31768996da2b..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/retrier_test.go
+++ /dev/null
@@ -1,300 +0,0 @@
-package internal
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/idempotency-headers/fern/core"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type RetryTestCase struct {
- description string
-
- giveAttempts uint
- giveStatusCodes []int
- giveResponse *InternalTestResponse
-
- wantResponse *InternalTestResponse
- wantError *core.APIError
-}
-
-func TestRetrier(t *testing.T) {
- tests := []*RetryTestCase{
- {
- description: "retry request succeeds after multiple failures",
- giveAttempts: 3,
- giveStatusCodes: []int{
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusOK,
- },
- giveResponse: &InternalTestResponse{
- Id: "1",
- },
- wantResponse: &InternalTestResponse{
- Id: "1",
- },
- },
- {
- description: "retry request fails if MaxAttempts is exceeded",
- giveAttempts: 3,
- giveStatusCodes: []int{
- http.StatusRequestTimeout,
- http.StatusRequestTimeout,
- http.StatusRequestTimeout,
- http.StatusOK,
- },
- wantError: &core.APIError{
- StatusCode: http.StatusRequestTimeout,
- },
- },
- {
- description: "retry durations increase exponentially and stay within the min and max delay values",
- giveAttempts: 4,
- giveStatusCodes: []int{
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusOK,
- },
- },
- {
- description: "retry does not occur on status code 404",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
- wantError: &core.APIError{
- StatusCode: http.StatusNotFound,
- },
- },
- {
- description: "retries occur on status code 429",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
- },
- {
- description: "retries occur on status code 408",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
- },
- {
- description: "retries occur on status code 500",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.description, func(t *testing.T) {
- var (
- test = tc
- server = newTestRetryServer(t, test)
- client = server.Client()
- )
-
- t.Parallel()
-
- caller := NewCaller(
- &CallerParams{
- Client: client,
- },
- )
-
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL,
- Method: http.MethodGet,
- Request: &InternalTestRequest{},
- Response: &response,
- MaxAttempts: test.giveAttempts,
- ResponseIsOptional: true,
- },
- )
-
- if test.wantError != nil {
- require.IsType(t, err, &core.APIError{})
- expectedErrorCode := test.wantError.StatusCode
- actualErrorCode := err.(*core.APIError).StatusCode
- assert.Equal(t, expectedErrorCode, actualErrorCode)
- return
- }
-
- require.NoError(t, err)
- assert.Equal(t, test.wantResponse, response)
- })
- }
-}
-
-// newTestRetryServer returns a new *httptest.Server configured with the
-// given test parameters, suitable for testing retries.
-func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
- var index int
- timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
-
- return httptest.NewServer(
- http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- timestamps = append(timestamps, time.Now())
- if index > 0 && index < len(expectedRetryDurations) {
- // Ensure that the duration between retries increases exponentially,
- // and that it is within the minimum and maximum retry delay values.
- actualDuration := timestamps[index].Sub(timestamps[index-1])
- expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
- expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
- assert.True(
- t,
- actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
- "expected duration to be in range [%v, %v], got %v",
- expectedDurationMin,
- expectedDurationMax,
- actualDuration,
- )
- assert.LessOrEqual(
- t,
- actualDuration,
- maxRetryDelay,
- "expected duration to be less than the maxRetryDelay (%v), got %v",
- maxRetryDelay,
- actualDuration,
- )
- assert.GreaterOrEqual(
- t,
- actualDuration,
- minRetryDelay,
- "expected duration to be greater than the minRetryDelay (%v), got %v",
- minRetryDelay,
- actualDuration,
- )
- }
-
- request := new(InternalTestRequest)
- bytes, err := io.ReadAll(r.Body)
- require.NoError(t, err)
- require.NoError(t, json.Unmarshal(bytes, request))
- require.LessOrEqual(t, index, len(tc.giveStatusCodes))
-
- statusCode := tc.giveStatusCodes[index]
-
- w.WriteHeader(statusCode)
-
- if tc.giveResponse != nil && statusCode == http.StatusOK {
- bytes, err = json.Marshal(tc.giveResponse)
- require.NoError(t, err)
- _, err = w.Write(bytes)
- require.NoError(t, err)
- }
-
- index++
- },
- ),
- )
-}
-
-// expectedRetryDurations holds an array of calculated retry durations,
-// where the index of the array should correspond to the retry attempt.
-//
-// Values are calculated based off of `minRetryDelay * 2^i`.
-var expectedRetryDurations = []time.Duration{
- 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
- 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
- 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
- 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
-}
-
-func TestRetryDelayTiming(t *testing.T) {
- tests := []struct {
- name string
- headerName string
- headerValueFunc func() string
- expectedMinMs int64
- expectedMaxMs int64
- }{
- {
- name: "retry-after with seconds value",
- headerName: "retry-after",
- headerValueFunc: func() string {
- return "1"
- },
- expectedMinMs: 500,
- expectedMaxMs: 1500,
- },
- {
- name: "retry-after with HTTP date",
- headerName: "retry-after",
- headerValueFunc: func() string {
- return time.Now().Add(3 * time.Second).Format(time.RFC1123)
- },
- expectedMinMs: 1500,
- expectedMaxMs: 4500,
- },
- {
- name: "x-ratelimit-reset with future timestamp",
- headerName: "x-ratelimit-reset",
- headerValueFunc: func() string {
- return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
- },
- expectedMinMs: 1500,
- expectedMaxMs: 4500,
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- var timestamps []time.Time
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- timestamps = append(timestamps, time.Now())
- if len(timestamps) == 1 {
- // First request - return retryable error with header
- w.Header().Set(tt.headerName, tt.headerValueFunc())
- w.WriteHeader(http.StatusTooManyRequests)
- } else {
- // Second request - return success
- w.WriteHeader(http.StatusOK)
- response := &InternalTestResponse{Id: "success"}
- bytes, _ := json.Marshal(response)
- w.Write(bytes)
- }
- }))
- defer server.Close()
-
- caller := NewCaller(&CallerParams{
- Client: server.Client(),
- })
-
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL,
- Method: http.MethodGet,
- Request: &InternalTestRequest{},
- Response: &response,
- MaxAttempts: 2,
- ResponseIsOptional: true,
- },
- )
-
- require.NoError(t, err)
- require.Len(t, timestamps, 2, "Expected exactly 2 requests")
-
- actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
-
- assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
- "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
- assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
- "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
- })
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/stringer.go b/seed/go-sdk/idempotency-headers/internal/stringer.go
deleted file mode 100644
index 312801851e0e..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/stringer.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package internal
-
-import "encoding/json"
-
-// StringifyJSON returns a pretty JSON string representation of
-// the given value.
-func StringifyJSON(value interface{}) (string, error) {
- bytes, err := json.MarshalIndent(value, "", " ")
- if err != nil {
- return "", err
- }
- return string(bytes), nil
-}
diff --git a/seed/go-sdk/idempotency-headers/internal/time.go b/seed/go-sdk/idempotency-headers/internal/time.go
deleted file mode 100644
index ab0e269fade3..000000000000
--- a/seed/go-sdk/idempotency-headers/internal/time.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "time"
-)
-
-const dateFormat = "2006-01-02"
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date (e.g. 2006-01-02).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type Date struct {
- t *time.Time
-}
-
-// NewDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewDate(t time.Time) *Date {
- return &Date{t: &t}
-}
-
-// NewOptionalDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDate(t *time.Time) *Date {
- if t == nil {
- return nil
- }
- return &Date{t: t}
-}
-
-// Time returns the Date's underlying time, if any. If the
-// date is nil, the zero value is returned.
-func (d *Date) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the Date's underlying time.Time, if any.
-func (d *Date) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *Date) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(dateFormat))
-}
-
-func (d *Date) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(dateFormat, raw)
- if err != nil {
- return err
- }
-
- *d = Date{t: &parsedTime}
- return nil
-}
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type DateTime struct {
- t *time.Time
-}
-
-// NewDateTime returns a new *DateTime.
-func NewDateTime(t time.Time) *DateTime {
- return &DateTime{t: &t}
-}
-
-// NewOptionalDateTime returns a new *DateTime. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDateTime(t *time.Time) *DateTime {
- if t == nil {
- return nil
- }
- return &DateTime{t: t}
-}
-
-// Time returns the DateTime's underlying time, if any. If the
-// date-time is nil, the zero value is returned.
-func (d *DateTime) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
-func (d *DateTime) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *DateTime) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(time.RFC3339))
-}
-
-func (d *DateTime) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(time.RFC3339, raw)
- if err != nil {
- return err
- }
-
- *d = DateTime{t: &parsedTime}
- return nil
-}
diff --git a/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go b/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
deleted file mode 100644
index ca6d4d4b62c8..000000000000
--- a/seed/go-sdk/idempotency-headers/option/idempotent_request_option.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package option
-
-import (
- core "github.com/idempotency-headers/fern/core"
-)
-
-// IdempotentRequestOption adapts the behavior of an individual request.
-type IdempotentRequestOption = core.IdempotentRequestOption
-
-// WithIdempotencyKey sets the idempotencyKey request header.
-func WithIdempotencyKey(idempotencyKey string) *core.IdempotencyKeyOption {
- return &core.IdempotencyKeyOption{
- IdempotencyKey: idempotencyKey,
- }
-}
-
-// WithIdempotencyExpiration sets the idempotencyExpiration request header.
-func WithIdempotencyExpiration(idempotencyExpiration int) *core.IdempotencyExpirationOption {
- return &core.IdempotencyExpirationOption{
- IdempotencyExpiration: idempotencyExpiration,
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/option/request_option.go b/seed/go-sdk/idempotency-headers/option/request_option.go
deleted file mode 100644
index 395b097fbc69..000000000000
--- a/seed/go-sdk/idempotency-headers/option/request_option.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package option
-
-import (
- core "github.com/idempotency-headers/fern/core"
- http "net/http"
- url "net/url"
-)
-
-// RequestOption adapts the behavior of an individual request.
-type RequestOption = core.RequestOption
-
-// WithBaseURL sets the base URL, overriding the default
-// environment, if any.
-func WithBaseURL(baseURL string) *core.BaseURLOption {
- return &core.BaseURLOption{
- BaseURL: baseURL,
- }
-}
-
-// WithHTTPClient uses the given HTTPClient to issue the request.
-func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
- return &core.HTTPClientOption{
- HTTPClient: httpClient,
- }
-}
-
-// WithHTTPHeader adds the given http.Header to the request.
-func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
- return &core.HTTPHeaderOption{
- // Clone the headers so they can't be modified after the option call.
- HTTPHeader: httpHeader.Clone(),
- }
-}
-
-// WithBodyProperties adds the given body properties to the request.
-func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
- copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
- for key, value := range bodyProperties {
- copiedBodyProperties[key] = value
- }
- return &core.BodyPropertiesOption{
- BodyProperties: copiedBodyProperties,
- }
-}
-
-// WithQueryParameters adds the given query parameters to the request.
-func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
- copiedQueryParameters := make(url.Values, len(queryParameters))
- for key, values := range queryParameters {
- copiedQueryParameters[key] = values
- }
- return &core.QueryParametersOption{
- QueryParameters: copiedQueryParameters,
- }
-}
-
-// WithMaxAttempts configures the maximum number of retry attempts.
-func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
- return &core.MaxAttemptsOption{
- MaxAttempts: attempts,
- }
-}
-
-// WithToken sets the 'Authorization: Bearer ' request header.
-func WithToken(token string) *core.TokenOption {
- return &core.TokenOption{
- Token: token,
- }
-}
diff --git a/seed/go-sdk/idempotency-headers/payment/client.go b/seed/go-sdk/idempotency-headers/payment/client.go
deleted file mode 100644
index c254ebcafd77..000000000000
--- a/seed/go-sdk/idempotency-headers/payment/client.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package payment
-
-import (
- context "context"
- uuid "github.com/google/uuid"
- fern "github.com/idempotency-headers/fern"
- core "github.com/idempotency-headers/fern/core"
- internal "github.com/idempotency-headers/fern/internal"
- option "github.com/idempotency-headers/fern/option"
-)
-
-type Client struct {
- WithRawResponse *RawClient
-
- options *core.RequestOptions
- baseURL string
- caller *internal.Caller
-}
-
-func NewClient(options *core.RequestOptions) *Client {
- return &Client{
- WithRawResponse: NewRawClient(options),
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
-
-func (c *Client) Create(
- ctx context.Context,
- request *fern.CreatePaymentRequest,
- opts ...option.IdempotentRequestOption,
-) (uuid.UUID, error) {
- response, err := c.WithRawResponse.Create(
- ctx,
- request,
- opts...,
- )
- if err != nil {
- return uuid.UUID{}, err
- }
- return response.Body, nil
-}
-
-func (c *Client) Delete(
- ctx context.Context,
- paymentId string,
- opts ...option.RequestOption,
-) error {
- _, err := c.WithRawResponse.Delete(
- ctx,
- paymentId,
- opts...,
- )
- if err != nil {
- return err
- }
- return nil
-}
diff --git a/seed/go-sdk/idempotency-headers/payment/raw_client.go b/seed/go-sdk/idempotency-headers/payment/raw_client.go
deleted file mode 100644
index f86f02e8c882..000000000000
--- a/seed/go-sdk/idempotency-headers/payment/raw_client.go
+++ /dev/null
@@ -1,114 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package payment
-
-import (
- context "context"
- uuid "github.com/google/uuid"
- fern "github.com/idempotency-headers/fern"
- core "github.com/idempotency-headers/fern/core"
- internal "github.com/idempotency-headers/fern/internal"
- option "github.com/idempotency-headers/fern/option"
- http "net/http"
-)
-
-type RawClient struct {
- baseURL string
- caller *internal.Caller
- options *core.RequestOptions
-}
-
-func NewRawClient(options *core.RequestOptions) *RawClient {
- return &RawClient{
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
-
-func (r *RawClient) Create(
- ctx context.Context,
- request *fern.CreatePaymentRequest,
- opts ...option.IdempotentRequestOption,
-) (*core.Response[uuid.UUID], error) {
- options := core.NewIdempotentRequestOptions(opts...)
- baseURL := internal.ResolveBaseURL(
- options.BaseURL,
- r.baseURL,
- "",
- )
- endpointURL := baseURL + "/payment"
- headers := internal.MergeHeaders(
- r.options.ToHeader(),
- options.ToHeader(),
- )
- var response uuid.UUID
- raw, err := r.caller.Call(
- ctx,
- &internal.CallParams{
- URL: endpointURL,
- Method: http.MethodPost,
- Headers: headers,
- MaxAttempts: options.MaxAttempts,
- BodyProperties: options.BodyProperties,
- QueryParameters: options.QueryParameters,
- Client: options.HTTPClient,
- Request: request,
- Response: &response,
- },
- )
- if err != nil {
- return nil, err
- }
- return &core.Response[uuid.UUID]{
- StatusCode: raw.StatusCode,
- Header: raw.Header,
- Body: response,
- }, nil
-}
-
-func (r *RawClient) Delete(
- ctx context.Context,
- paymentId string,
- opts ...option.RequestOption,
-) (*core.Response[any], error) {
- options := core.NewRequestOptions(opts...)
- baseURL := internal.ResolveBaseURL(
- options.BaseURL,
- r.baseURL,
- "",
- )
- endpointURL := internal.EncodeURL(
- baseURL+"/payment/%v",
- paymentId,
- )
- headers := internal.MergeHeaders(
- r.options.ToHeader(),
- options.ToHeader(),
- )
- raw, err := r.caller.Call(
- ctx,
- &internal.CallParams{
- URL: endpointURL,
- Method: http.MethodDelete,
- Headers: headers,
- MaxAttempts: options.MaxAttempts,
- BodyProperties: options.BodyProperties,
- QueryParameters: options.QueryParameters,
- Client: options.HTTPClient,
- },
- )
- if err != nil {
- return nil, err
- }
- return &core.Response[any]{
- StatusCode: raw.StatusCode,
- Header: raw.Header,
- Body: nil,
- }, nil
-}
diff --git a/seed/go-sdk/streaming/.fern/metadata.json b/seed/go-sdk/streaming/.fern/metadata.json
deleted file mode 100644
index abf3778e9da9..000000000000
--- a/seed/go-sdk/streaming/.fern/metadata.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "cliVersion": "DUMMY",
- "generatorName": "fernapi/fern-go-sdk",
- "generatorVersion": "latest",
- "generatorConfig": {
- "enableWireTests": false,
- "packageName": "stream",
- "module": {
- "path": "github.com/fern-api/stream-go"
- }
- }
-}
\ No newline at end of file
diff --git a/seed/go-sdk/streaming/.github/workflows/ci.yml b/seed/go-sdk/streaming/.github/workflows/ci.yml
deleted file mode 100644
index 56310d69624b..000000000000
--- a/seed/go-sdk/streaming/.github/workflows/ci.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: ci
-
-on: [push]
-
-jobs:
- compile:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repo
- uses: actions/checkout@v4
-
- - name: Set up go
- uses: actions/setup-go@v4
-
- - name: Compile
- run: go build ./...
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repo
- uses: actions/checkout@v4
-
- - name: Set up go
- uses: actions/setup-go@v4
-
- - name: Setup wiremock server
- run: |
- if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down && docker compose -f wiremock/docker-compose.test.yml up -d; fi
-
- - name: Test
- run: go test ./...
-
- - name: Teardown wiremock server
- run: |
- if [ -f wiremock/docker-compose.test.yml ]; then docker compose -f wiremock/docker-compose.test.yml down; fi
diff --git a/seed/go-sdk/streaming/client/client.go b/seed/go-sdk/streaming/client/client.go
deleted file mode 100644
index 902525e73215..000000000000
--- a/seed/go-sdk/streaming/client/client.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package client
-
-import (
- core "github.com/fern-api/stream-go/v2/core"
- dummy "github.com/fern-api/stream-go/v2/dummy"
- internal "github.com/fern-api/stream-go/v2/internal"
- option "github.com/fern-api/stream-go/v2/option"
-)
-
-type Client struct {
- Dummy *dummy.Client
-
- options *core.RequestOptions
- baseURL string
- caller *internal.Caller
-}
-
-func NewClient(opts ...option.RequestOption) *Client {
- options := core.NewRequestOptions(opts...)
- return &Client{
- Dummy: dummy.NewClient(options),
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
diff --git a/seed/go-sdk/streaming/client/client_test.go b/seed/go-sdk/streaming/client/client_test.go
deleted file mode 100644
index 6b0ee5caf60e..000000000000
--- a/seed/go-sdk/streaming/client/client_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package client
-
-import (
- option "github.com/fern-api/stream-go/v2/option"
- assert "github.com/stretchr/testify/assert"
- http "net/http"
- testing "testing"
- time "time"
-)
-
-func TestNewClient(t *testing.T) {
- t.Run("default", func(t *testing.T) {
- c := NewClient()
- assert.Empty(t, c.baseURL)
- })
-
- t.Run("base url", func(t *testing.T) {
- c := NewClient(
- option.WithBaseURL("test.co"),
- )
- assert.Equal(t, "test.co", c.baseURL)
- })
-
- t.Run("http client", func(t *testing.T) {
- httpClient := &http.Client{
- Timeout: 5 * time.Second,
- }
- c := NewClient(
- option.WithHTTPClient(httpClient),
- )
- assert.Empty(t, c.baseURL)
- })
-
- t.Run("http header", func(t *testing.T) {
- header := make(http.Header)
- header.Set("X-API-Tenancy", "test")
- c := NewClient(
- option.WithHTTPHeader(header),
- )
- assert.Empty(t, c.baseURL)
- assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy"))
- })
-}
diff --git a/seed/go-sdk/streaming/core/api_error.go b/seed/go-sdk/streaming/core/api_error.go
deleted file mode 100644
index 6168388541b4..000000000000
--- a/seed/go-sdk/streaming/core/api_error.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package core
-
-import (
- "fmt"
- "net/http"
-)
-
-// APIError is a lightweight wrapper around the standard error
-// interface that preserves the status code from the RPC, if any.
-type APIError struct {
- err error
-
- StatusCode int `json:"-"`
- Header http.Header `json:"-"`
-}
-
-// NewAPIError constructs a new API error.
-func NewAPIError(statusCode int, header http.Header, err error) *APIError {
- return &APIError{
- err: err,
- Header: header,
- StatusCode: statusCode,
- }
-}
-
-// Unwrap returns the underlying error. This also makes the error compatible
-// with errors.As and errors.Is.
-func (a *APIError) Unwrap() error {
- if a == nil {
- return nil
- }
- return a.err
-}
-
-// Error returns the API error's message.
-func (a *APIError) Error() string {
- if a == nil || (a.err == nil && a.StatusCode == 0) {
- return ""
- }
- if a.err == nil {
- return fmt.Sprintf("%d", a.StatusCode)
- }
- if a.StatusCode == 0 {
- return a.err.Error()
- }
- return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error())
-}
diff --git a/seed/go-sdk/streaming/core/http.go b/seed/go-sdk/streaming/core/http.go
deleted file mode 100644
index 92c435692940..000000000000
--- a/seed/go-sdk/streaming/core/http.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package core
-
-import "net/http"
-
-// HTTPClient is an interface for a subset of the *http.Client.
-type HTTPClient interface {
- Do(*http.Request) (*http.Response, error)
-}
-
-// Response is an HTTP response from an HTTP client.
-type Response[T any] struct {
- StatusCode int
- Header http.Header
- Body T
-}
diff --git a/seed/go-sdk/streaming/core/request_option.go b/seed/go-sdk/streaming/core/request_option.go
deleted file mode 100644
index 119c02bfe449..000000000000
--- a/seed/go-sdk/streaming/core/request_option.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package core
-
-import (
- http "net/http"
- url "net/url"
-)
-
-// RequestOption adapts the behavior of the client or an individual request.
-type RequestOption interface {
- applyRequestOptions(*RequestOptions)
-}
-
-// RequestOptions defines all of the possible request options.
-//
-// This type is primarily used by the generated code and is not meant
-// to be used directly; use the option package instead.
-type RequestOptions struct {
- BaseURL string
- HTTPClient HTTPClient
- HTTPHeader http.Header
- BodyProperties map[string]interface{}
- QueryParameters url.Values
- MaxAttempts uint
-}
-
-// NewRequestOptions returns a new *RequestOptions value.
-//
-// This function is primarily used by the generated code and is not meant
-// to be used directly; use RequestOption instead.
-func NewRequestOptions(opts ...RequestOption) *RequestOptions {
- options := &RequestOptions{
- HTTPHeader: make(http.Header),
- BodyProperties: make(map[string]interface{}),
- QueryParameters: make(url.Values),
- }
- for _, opt := range opts {
- opt.applyRequestOptions(options)
- }
- return options
-}
-
-// ToHeader maps the configured request options into a http.Header used
-// for the request(s).
-func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() }
-
-func (r *RequestOptions) cloneHeader() http.Header {
- headers := r.HTTPHeader.Clone()
- headers.Set("X-Fern-Language", "Go")
- headers.Set("X-Fern-SDK-Name", "github.com/fern-api/stream-go/v2")
- headers.Set("X-Fern-SDK-Version", "v2.0.0")
- headers.Set("User-Agent", "github.com/streaming/fern/v2.0.0")
- return headers
-}
-
-// BaseURLOption implements the RequestOption interface.
-type BaseURLOption struct {
- BaseURL string
-}
-
-func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) {
- opts.BaseURL = b.BaseURL
-}
-
-// HTTPClientOption implements the RequestOption interface.
-type HTTPClientOption struct {
- HTTPClient HTTPClient
-}
-
-func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) {
- opts.HTTPClient = h.HTTPClient
-}
-
-// HTTPHeaderOption implements the RequestOption interface.
-type HTTPHeaderOption struct {
- HTTPHeader http.Header
-}
-
-func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) {
- opts.HTTPHeader = h.HTTPHeader
-}
-
-// BodyPropertiesOption implements the RequestOption interface.
-type BodyPropertiesOption struct {
- BodyProperties map[string]interface{}
-}
-
-func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) {
- opts.BodyProperties = b.BodyProperties
-}
-
-// QueryParametersOption implements the RequestOption interface.
-type QueryParametersOption struct {
- QueryParameters url.Values
-}
-
-func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) {
- opts.QueryParameters = q.QueryParameters
-}
-
-// MaxAttemptsOption implements the RequestOption interface.
-type MaxAttemptsOption struct {
- MaxAttempts uint
-}
-
-func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) {
- opts.MaxAttempts = m.MaxAttempts
-}
diff --git a/seed/go-sdk/streaming/core/stream.go b/seed/go-sdk/streaming/core/stream.go
deleted file mode 100644
index 25c528e89516..000000000000
--- a/seed/go-sdk/streaming/core/stream.go
+++ /dev/null
@@ -1,368 +0,0 @@
-package core
-
-import (
- "bufio"
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "slices"
- "strings"
-)
-
-type StreamFormat string
-
-const (
- StreamFormatSSE StreamFormat = "sse"
- StreamFormatEmpty StreamFormat = ""
-)
-
-const (
- sseEventSeparator = "\n\n"
- sseLineSeparator = "\n"
-)
-
-const (
- defaultMaxBufSize = 64 * 1024 // 64KB
-)
-
-// Stream represents a stream of messages sent from a server.
-type Stream[T any] struct {
- reader streamReader
- closer io.Closer
-}
-
-// StreamOption adapts the behavior of the Stream.
-type StreamOption func(*streamOptions)
-
-// WithDelimiter overrides the delimiter for the Stream.
-//
-// By default, the Stream is newline-delimited.
-func WithDelimiter(delimiter string) StreamOption {
- return func(opts *streamOptions) {
- opts.delimiter = delimiter
- }
-}
-
-// WithPrefix overrides the prefix for the Stream.
-//
-// By default, the Stream doesn't have a prefix.
-func WithPrefix(prefix string) StreamOption {
- return func(opts *streamOptions) {
- opts.prefix = prefix
- }
-}
-
-// WithTerminator overrides the terminator for the Stream.
-//
-// By default, the Stream terminates on EOF.
-func WithTerminator(terminator string) StreamOption {
- return func(opts *streamOptions) {
- opts.terminator = terminator
- }
-}
-
-// WithFormat overrides the isSSE flag for the Stream.
-//
-// By default, the Stream is not SSE.
-func WithFormat(format StreamFormat) StreamOption {
- return func(opts *streamOptions) {
- opts.format = format
- }
-}
-
-// NewStream constructs a new Stream from the given *http.Response.
-func NewStream[T any](response *http.Response, opts ...StreamOption) *Stream[T] {
- options := new(streamOptions)
- for _, opt := range opts {
- opt(options)
- }
- return &Stream[T]{
- reader: newStreamReader(response.Body, options),
- closer: response.Body,
- }
-}
-
-// Recv reads a message from the stream, returning io.EOF when
-// all the messages have been read.
-func (s Stream[T]) Recv() (T, error) {
- var value T
- bytes, err := s.reader.ReadFromStream()
- if err != nil {
- return value, err
- }
- if err := json.Unmarshal(bytes, &value); err != nil {
- return value, err
- }
- return value, nil
-}
-
-// Close closes the Stream.
-func (s Stream[T]) Close() error {
- return s.closer.Close()
-}
-
-// streamReader reads data from a stream.
-type streamReader interface {
- ReadFromStream() ([]byte, error)
-}
-
-// newStreamReader returns a new streamReader based on the given
-// delimiter.
-//
-// By default, the streamReader uses a simple a *bufio.Reader
-// which splits on newlines, and otherwise use a *bufio.Scanner to
-// split on custom delimiters.
-func newStreamReader(reader io.Reader, options *streamOptions) streamReader {
- if !options.isEmpty() {
- if options.maxBufSize == 0 {
- options.maxBufSize = defaultMaxBufSize
- }
- if options.format == StreamFormatSSE {
- return newSseStreamReader(reader, options)
- }
- return newScannerStreamReader(reader, options)
- }
- return newBufferStreamReader(reader)
-}
-
-// BufferStreamReader reads data from a *bufio.Reader, which splits
-// on newlines.
-type BufferStreamReader struct {
- reader *bufio.Reader
-}
-
-func newBufferStreamReader(reader io.Reader) *BufferStreamReader {
- return &BufferStreamReader{
- reader: bufio.NewReader(reader),
- }
-}
-
-func (b *BufferStreamReader) ReadFromStream() ([]byte, error) {
- line, err := b.reader.ReadBytes('\n')
- if err != nil {
- return nil, err
- }
- // Strip the trailing newline
- return bytes.TrimSuffix(line, []byte("\n")), nil
-}
-
-// ScannerStreamReader reads data from a *bufio.Scanner, which allows for
-// configurable delimiters.
-type ScannerStreamReader struct {
- scanner *bufio.Scanner
- options *streamOptions
-}
-
-func newScannerStreamReader(
- reader io.Reader,
- options *streamOptions,
-) *ScannerStreamReader {
- scanner := bufio.NewScanner(reader)
- stream := &ScannerStreamReader{
- scanner: scanner,
- options: options,
- }
- scanner.Split(func(bytes []byte, atEOF bool) (int, []byte, error) {
- if atEOF && len(bytes) == 0 {
- return 0, nil, nil
- }
- n, data, err := stream.parse(bytes)
- if stream.isTerminated(data) {
- return 0, nil, io.EOF
- }
- return n, data, err
- })
- return stream
-}
-
-func (s *ScannerStreamReader) ReadFromStream() ([]byte, error) {
- if s.scanner.Scan() {
- return s.scanner.Bytes(), nil
- }
- if err := s.scanner.Err(); err != nil {
- return nil, err
- }
- return nil, io.EOF
-}
-
-func (s *ScannerStreamReader) parse(bytes []byte) (int, []byte, error) {
- var startIndex int
- if s.options != nil && s.options.prefix != "" {
- if i := strings.Index(string(bytes), s.options.prefix); i >= 0 {
- startIndex = i + len(s.options.prefix)
- }
- }
- data := bytes[startIndex:]
- lineDelimiter := s.options.getLineDelimiter()
- delimIndex := strings.Index(string(data), lineDelimiter)
- if delimIndex < 0 {
- return startIndex + len(data), data, nil
- }
- endIndex := delimIndex + len(lineDelimiter)
- parsedData := data[:endIndex]
- n := startIndex + endIndex
- return n, parsedData, nil
-}
-
-func (s *ScannerStreamReader) isTerminated(bytes []byte) bool {
- if s.options == nil || s.options.terminator == "" {
- return false
- }
- return strings.Contains(string(bytes), s.options.terminator)
-}
-
-type streamOptions struct {
- delimiter string
- prefix string
- terminator string
- format StreamFormat
- maxBufSize int
-}
-
-func (s *streamOptions) isEmpty() bool {
- return s.delimiter == "" && s.prefix == "" && s.terminator == "" && s.format == StreamFormatEmpty
-}
-
-func (s *streamOptions) getLineDelimiter() string {
- if s.delimiter != "" {
- return s.delimiter
- }
- return sseLineSeparator
-}
-
-type SseStreamReader struct {
- scanner *bufio.Scanner
- options *streamOptions
-}
-
-func newSseStreamReader(
- reader io.Reader,
- options *streamOptions,
-) *SseStreamReader {
- scanner := bufio.NewScanner(reader)
- stream := &SseStreamReader{
- scanner: scanner,
- options: options,
- }
- scanner.Buffer(make([]byte, slices.Min([]int{4096, options.maxBufSize})), options.maxBufSize)
-
- // Configure scanner to split on SSE event separator (\n\n)
- // This is fixed by the SSE specification and cannot be changed
- scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- // SSE messages are always separated by blank lines (\n\n)
- if i := strings.Index(string(data), sseEventSeparator); i >= 0 {
- return i + len(sseEventSeparator), data[0:i], nil
- }
-
- if atEOF || stream.isTerminated(data) {
- return len(data), data, nil
- }
- return 0, nil, nil
- })
- return stream
-}
-
-func (s *SseStreamReader) isTerminated(bytes []byte) bool {
- if s.options == nil || s.options.terminator == "" {
- return false
- }
- return strings.Contains(string(bytes), s.options.terminator)
-}
-
-func (s *SseStreamReader) ReadFromStream() ([]byte, error) {
-
- event, err := s.nextEvent()
- if err != nil {
- return nil, err
- }
- return event.data, nil
-}
-
-func (s *SseStreamReader) nextEvent() (*SseEvent, error) {
-
- event := SseEvent{}
- if s.scanner.Scan() {
- rawEvent := s.scanner.Bytes()
-
- // Parse individual lines within the SSE message
- // Lines are always separated by \n within a message (SSE specification)
- lines := strings.Split(string(rawEvent), sseLineSeparator)
- for _, line := range lines {
- s.parseSseLine([]byte(line), &event)
- }
-
- if event.size() > s.options.maxBufSize {
- return nil, errors.New("SseStreamReader.ReadFromStream: buffer limit exceeded")
- }
- return &event, nil
- }
- return &event, io.EOF
-}
-
-func (s *SseStreamReader) parseSseLine(_bytes []byte, event *SseEvent) {
- // Try to parse with space first (standard format), then without space (lenient format)
- if value, ok := s.tryParseField(_bytes, sseDataPrefix, sseDataPrefixNoSpace); ok {
- if len(event.data) > 0 {
- // Join multiple data: lines using the configured delimiter
- // This allows customization of how multi-line data is concatenated:
- // - "\n" (default): preserves line breaks for multi-line JSON
- // - "": concatenates without separator
- // - Any other string: custom separator
- lineDelimiter := s.options.getLineDelimiter()
- event.data = append(event.data, lineDelimiter...)
- }
- event.data = append(event.data, value...)
- } else if value, ok := s.tryParseField(_bytes, sseIdPrefix, sseIdPrefixNoSpace); ok {
- event.id = append(event.id, value...)
- } else if value, ok := s.tryParseField(_bytes, sseEventPrefix, sseEventPrefixNoSpace); ok {
- event.event = append(event.event, value...)
- } else if value, ok := s.tryParseField(_bytes, sseRetryPrefix, sseRetryPrefixNoSpace); ok {
- event.retry = append(event.retry, value...)
- }
-}
-
-// tryParseField attempts to parse an SSE field by trying multiple prefix patterns in order.
-// This handles APIs that don't strictly follow the SSE specification by omitting the space after the colon.
-// It tries each prefix in the order provided and returns the value after the first matching prefix.
-func (s *SseStreamReader) tryParseField(line []byte, prefixes ...[]byte) ([]byte, bool) {
- for _, prefix := range prefixes {
- if bytes.HasPrefix(line, prefix) {
- return line[len(prefix):], true
- }
- }
- return nil, false
-}
-
-func (event *SseEvent) size() int {
- return len(event.id) + len(event.data) + len(event.event) + len(event.retry)
-}
-
-func (event *SseEvent) String() string {
- return fmt.Sprintf("SseEvent{id: %q, event: %q, data: %q, retry: %q}", event.id, event.event, event.data, event.retry)
-}
-
-type SseEvent struct {
- id []byte
- data []byte
- event []byte
- retry []byte
-}
-
-var (
- sseIdPrefix = []byte("id: ")
- sseDataPrefix = []byte("data: ")
- sseEventPrefix = []byte("event: ")
- sseRetryPrefix = []byte("retry: ")
-
- // Lenient prefixes without space for APIs that don't strictly follow SSE specification
- sseIdPrefixNoSpace = []byte("id:")
- sseDataPrefixNoSpace = []byte("data:")
- sseEventPrefixNoSpace = []byte("event:")
- sseRetryPrefixNoSpace = []byte("retry:")
-)
diff --git a/seed/go-sdk/streaming/dummy/client.go b/seed/go-sdk/streaming/dummy/client.go
deleted file mode 100644
index 47c2a23f6cce..000000000000
--- a/seed/go-sdk/streaming/dummy/client.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package dummy
-
-import (
- context "context"
- stream "github.com/fern-api/stream-go/v2"
- core "github.com/fern-api/stream-go/v2/core"
- internal "github.com/fern-api/stream-go/v2/internal"
- option "github.com/fern-api/stream-go/v2/option"
- http "net/http"
-)
-
-type Client struct {
- WithRawResponse *RawClient
-
- options *core.RequestOptions
- baseURL string
- caller *internal.Caller
-}
-
-func NewClient(options *core.RequestOptions) *Client {
- return &Client{
- WithRawResponse: NewRawClient(options),
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
-
-func (c *Client) GenerateStream(
- ctx context.Context,
- request *stream.GenerateStreamRequest,
- opts ...option.RequestOption,
-) (*core.Stream[stream.StreamResponse], error) {
- options := core.NewRequestOptions(opts...)
- baseURL := internal.ResolveBaseURL(
- options.BaseURL,
- c.baseURL,
- "",
- )
- endpointURL := baseURL + "/generate-stream"
- headers := internal.MergeHeaders(
- c.options.ToHeader(),
- options.ToHeader(),
- )
- streamer := internal.NewStreamer[stream.StreamResponse](c.caller)
- return streamer.Stream(
- ctx,
- &internal.StreamParams{
- URL: endpointURL,
- Method: http.MethodPost,
- Headers: headers,
- MaxAttempts: options.MaxAttempts,
- BodyProperties: options.BodyProperties,
- QueryParameters: options.QueryParameters,
- Client: options.HTTPClient,
- Request: request,
- ErrorDecoder: internal.NewErrorDecoder(stream.ErrorCodes),
- },
- )
-}
-
-func (c *Client) Generate(
- ctx context.Context,
- request *stream.Generateequest,
- opts ...option.RequestOption,
-) (*stream.StreamResponse, error) {
- response, err := c.WithRawResponse.Generate(
- ctx,
- request,
- opts...,
- )
- if err != nil {
- return nil, err
- }
- return response.Body, nil
-}
diff --git a/seed/go-sdk/streaming/dummy/raw_client.go b/seed/go-sdk/streaming/dummy/raw_client.go
deleted file mode 100644
index 361c67f5907f..000000000000
--- a/seed/go-sdk/streaming/dummy/raw_client.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package dummy
-
-import (
- context "context"
- stream "github.com/fern-api/stream-go/v2"
- core "github.com/fern-api/stream-go/v2/core"
- internal "github.com/fern-api/stream-go/v2/internal"
- option "github.com/fern-api/stream-go/v2/option"
- http "net/http"
-)
-
-type RawClient struct {
- baseURL string
- caller *internal.Caller
- options *core.RequestOptions
-}
-
-func NewRawClient(options *core.RequestOptions) *RawClient {
- return &RawClient{
- options: options,
- baseURL: options.BaseURL,
- caller: internal.NewCaller(
- &internal.CallerParams{
- Client: options.HTTPClient,
- MaxAttempts: options.MaxAttempts,
- },
- ),
- }
-}
-
-func (r *RawClient) Generate(
- ctx context.Context,
- request *stream.Generateequest,
- opts ...option.RequestOption,
-) (*core.Response[*stream.StreamResponse], error) {
- options := core.NewRequestOptions(opts...)
- baseURL := internal.ResolveBaseURL(
- options.BaseURL,
- r.baseURL,
- "",
- )
- endpointURL := baseURL + "/generate"
- headers := internal.MergeHeaders(
- r.options.ToHeader(),
- options.ToHeader(),
- )
- var response *stream.StreamResponse
- raw, err := r.caller.Call(
- ctx,
- &internal.CallParams{
- URL: endpointURL,
- Method: http.MethodPost,
- Headers: headers,
- MaxAttempts: options.MaxAttempts,
- BodyProperties: options.BodyProperties,
- QueryParameters: options.QueryParameters,
- Client: options.HTTPClient,
- Request: request,
- Response: &response,
- },
- )
- if err != nil {
- return nil, err
- }
- return &core.Response[*stream.StreamResponse]{
- StatusCode: raw.StatusCode,
- Header: raw.Header,
- Body: response,
- }, nil
-}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
deleted file mode 100644
index cdc45a765996..000000000000
--- a/seed/go-sdk/streaming/dynamic-snippets/example0/snippet.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package example
-
-import (
- client "github.com/fern-api/stream-go/v2/client"
- option "github.com/fern-api/stream-go/v2/option"
- stream "github.com/fern-api/stream-go/v2"
- context "context"
-)
-
-func do() {
- client := client.NewClient(
- option.WithBaseURL(
- "https://api.fern.com",
- ),
- )
- request := &stream.GenerateStreamRequest{
- NumEvents: 1,
- }
- client.Dummy.GenerateStream(
- context.TODO(),
- request,
- )
-}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
deleted file mode 100644
index 5a3f2af9383d..000000000000
--- a/seed/go-sdk/streaming/dynamic-snippets/example1/snippet.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package example
-
-import (
- client "github.com/fern-api/stream-go/v2/client"
- option "github.com/fern-api/stream-go/v2/option"
- stream "github.com/fern-api/stream-go/v2"
- context "context"
-)
-
-func do() {
- client := client.NewClient(
- option.WithBaseURL(
- "https://api.fern.com",
- ),
- )
- request := &stream.Generateequest{
- NumEvents: 5,
- }
- client.Dummy.Generate(
- context.TODO(),
- request,
- )
-}
diff --git a/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go b/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
deleted file mode 100644
index 4e83013bdd35..000000000000
--- a/seed/go-sdk/streaming/dynamic-snippets/example2/snippet.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package example
-
-import (
- client "github.com/fern-api/stream-go/v2/client"
- option "github.com/fern-api/stream-go/v2/option"
- stream "github.com/fern-api/stream-go/v2"
- context "context"
-)
-
-func do() {
- client := client.NewClient(
- option.WithBaseURL(
- "https://api.fern.com",
- ),
- )
- request := &stream.Generateequest{
- NumEvents: 1,
- }
- client.Dummy.Generate(
- context.TODO(),
- request,
- )
-}
diff --git a/seed/go-sdk/streaming/internal/caller.go b/seed/go-sdk/streaming/internal/caller.go
deleted file mode 100644
index 53d1c2d54b22..000000000000
--- a/seed/go-sdk/streaming/internal/caller.go
+++ /dev/null
@@ -1,250 +0,0 @@
-package internal
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "reflect"
- "strings"
-
- "github.com/fern-api/stream-go/v2/core"
-)
-
-const (
- // contentType specifies the JSON Content-Type header value.
- contentType = "application/json"
- contentTypeHeader = "Content-Type"
-)
-
-// Caller calls APIs and deserializes their response, if any.
-type Caller struct {
- client core.HTTPClient
- retrier *Retrier
-}
-
-// CallerParams represents the parameters used to constrcut a new *Caller.
-type CallerParams struct {
- Client core.HTTPClient
- MaxAttempts uint
-}
-
-// NewCaller returns a new *Caller backed by the given parameters.
-func NewCaller(params *CallerParams) *Caller {
- var httpClient core.HTTPClient = http.DefaultClient
- if params.Client != nil {
- httpClient = params.Client
- }
- var retryOptions []RetryOption
- if params.MaxAttempts > 0 {
- retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
- }
- return &Caller{
- client: httpClient,
- retrier: NewRetrier(retryOptions...),
- }
-}
-
-// CallParams represents the parameters used to issue an API call.
-type CallParams struct {
- URL string
- Method string
- MaxAttempts uint
- Headers http.Header
- BodyProperties map[string]interface{}
- QueryParameters url.Values
- Client core.HTTPClient
- Request interface{}
- Response interface{}
- ResponseIsOptional bool
- ErrorDecoder ErrorDecoder
-}
-
-// CallResponse is a parsed HTTP response from an API call.
-type CallResponse struct {
- StatusCode int
- Header http.Header
-}
-
-// Call issues an API call according to the given call parameters.
-func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) {
- url := buildURL(params.URL, params.QueryParameters)
- req, err := newRequest(
- ctx,
- url,
- params.Method,
- params.Headers,
- params.Request,
- params.BodyProperties,
- )
- if err != nil {
- return nil, err
- }
-
- // If the call has been cancelled, don't issue the request.
- if err := ctx.Err(); err != nil {
- return nil, err
- }
-
- client := c.client
- if params.Client != nil {
- // Use the HTTP client scoped to the request.
- client = params.Client
- }
-
- var retryOptions []RetryOption
- if params.MaxAttempts > 0 {
- retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
- }
-
- resp, err := c.retrier.Run(
- client.Do,
- req,
- params.ErrorDecoder,
- retryOptions...,
- )
- if err != nil {
- return nil, err
- }
-
- // Close the response body after we're done.
- defer resp.Body.Close()
-
- // Check if the call was cancelled before we return the error
- // associated with the call and/or unmarshal the response data.
- if err := ctx.Err(); err != nil {
- return nil, err
- }
-
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- return nil, decodeError(resp, params.ErrorDecoder)
- }
-
- // Mutate the response parameter in-place.
- if params.Response != nil {
- if writer, ok := params.Response.(io.Writer); ok {
- _, err = io.Copy(writer, resp.Body)
- } else {
- err = json.NewDecoder(resp.Body).Decode(params.Response)
- }
- if err != nil {
- if err == io.EOF {
- if params.ResponseIsOptional {
- // The response is optional, so we should ignore the
- // io.EOF error
- return &CallResponse{
- StatusCode: resp.StatusCode,
- Header: resp.Header,
- }, nil
- }
- return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response)
- }
- return nil, err
- }
- }
-
- return &CallResponse{
- StatusCode: resp.StatusCode,
- Header: resp.Header,
- }, nil
-}
-
-// buildURL constructs the final URL by appending the given query parameters (if any).
-func buildURL(
- url string,
- queryParameters url.Values,
-) string {
- if len(queryParameters) == 0 {
- return url
- }
- if strings.ContainsRune(url, '?') {
- url += "&"
- } else {
- url += "?"
- }
- url += queryParameters.Encode()
- return url
-}
-
-// newRequest returns a new *http.Request with all of the fields
-// required to issue the call.
-func newRequest(
- ctx context.Context,
- url string,
- method string,
- endpointHeaders http.Header,
- request interface{},
- bodyProperties map[string]interface{},
-) (*http.Request, error) {
- requestBody, err := newRequestBody(request, bodyProperties)
- if err != nil {
- return nil, err
- }
- req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- req.Header.Set(contentTypeHeader, contentType)
- for name, values := range endpointHeaders {
- req.Header[name] = values
- }
- return req, nil
-}
-
-// newRequestBody returns a new io.Reader that represents the HTTP request body.
-func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) {
- if isNil(request) {
- if len(bodyProperties) == 0 {
- return nil, nil
- }
- requestBytes, err := json.Marshal(bodyProperties)
- if err != nil {
- return nil, err
- }
- return bytes.NewReader(requestBytes), nil
- }
- if body, ok := request.(io.Reader); ok {
- return body, nil
- }
- requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties)
- if err != nil {
- return nil, err
- }
- return bytes.NewReader(requestBytes), nil
-}
-
-// decodeError decodes the error from the given HTTP response. Note that
-// it's the caller's responsibility to close the response body.
-func decodeError(response *http.Response, errorDecoder ErrorDecoder) error {
- if errorDecoder != nil {
- // This endpoint has custom errors, so we'll
- // attempt to unmarshal the error into a structured
- // type based on the status code.
- return errorDecoder(response.StatusCode, response.Header, response.Body)
- }
- // This endpoint doesn't have any custom error
- // types, so we just read the body as-is, and
- // put it into a normal error.
- bytes, err := io.ReadAll(response.Body)
- if err != nil && err != io.EOF {
- return err
- }
- if err == io.EOF {
- // The error didn't have a response body,
- // so all we can do is return an error
- // with the status code.
- return core.NewAPIError(response.StatusCode, response.Header, nil)
- }
- return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes)))
-}
-
-// isNil is used to determine if the request value is equal to nil (i.e. an interface
-// value that holds a nil concrete value is itself non-nil).
-func isNil(value interface{}) bool {
- return value == nil || reflect.ValueOf(value).IsNil()
-}
diff --git a/seed/go-sdk/streaming/internal/caller_test.go b/seed/go-sdk/streaming/internal/caller_test.go
deleted file mode 100644
index bab98eb18836..000000000000
--- a/seed/go-sdk/streaming/internal/caller_test.go
+++ /dev/null
@@ -1,395 +0,0 @@
-package internal
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "net/url"
- "strconv"
- "testing"
-
- "github.com/fern-api/stream-go/v2/core"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// InternalTestCase represents a single test case.
-type InternalTestCase struct {
- description string
-
- // Server-side assertions.
- givePathSuffix string
- giveMethod string
- giveResponseIsOptional bool
- giveHeader http.Header
- giveErrorDecoder ErrorDecoder
- giveRequest *InternalTestRequest
- giveQueryParams url.Values
- giveBodyProperties map[string]interface{}
-
- // Client-side assertions.
- wantResponse *InternalTestResponse
- wantHeaders http.Header
- wantError error
-}
-
-// InternalTestRequest a simple request body.
-type InternalTestRequest struct {
- Id string `json:"id"`
-}
-
-// InternalTestResponse a simple response body.
-type InternalTestResponse struct {
- Id string `json:"id"`
- ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"`
- QueryParameters url.Values `json:"queryParameters,omitempty"`
-}
-
-// InternalTestNotFoundError represents a 404.
-type InternalTestNotFoundError struct {
- *core.APIError
-
- Message string `json:"message"`
-}
-
-func TestCall(t *testing.T) {
- tests := []*InternalTestCase{
- {
- description: "GET success",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- },
- },
- {
- description: "GET success with query",
- givePathSuffix: "?limit=1",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "limit": []string{"1"},
- },
- },
- },
- {
- description: "GET not found",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: &InternalTestRequest{
- Id: strconv.Itoa(http.StatusNotFound),
- },
- giveErrorDecoder: newTestErrorDecoder(t),
- wantError: &InternalTestNotFoundError{
- APIError: core.NewAPIError(
- http.StatusNotFound,
- http.Header{},
- errors.New(`{"message":"ID \"404\" not found"}`),
- ),
- },
- },
- {
- description: "POST empty body",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: nil,
- wantError: core.NewAPIError(
- http.StatusBadRequest,
- http.Header{},
- errors.New("invalid request"),
- ),
- },
- {
- description: "POST optional response",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- giveResponseIsOptional: true,
- },
- {
- description: "POST API error",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"fail"},
- },
- giveRequest: &InternalTestRequest{
- Id: strconv.Itoa(http.StatusInternalServerError),
- },
- wantError: core.NewAPIError(
- http.StatusInternalServerError,
- http.Header{},
- errors.New("failed to process request"),
- ),
- },
- {
- description: "POST extra properties",
- giveMethod: http.MethodPost,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: new(InternalTestRequest),
- giveBodyProperties: map[string]interface{}{
- "key": "value",
- },
- wantResponse: &InternalTestResponse{
- ExtraBodyProperties: map[string]interface{}{
- "key": "value",
- },
- },
- },
- {
- description: "GET extra query parameters",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveQueryParams: url.Values{
- "extra": []string{"true"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "extra": []string{"true"},
- },
- },
- },
- {
- description: "GET merge extra query parameters",
- givePathSuffix: "?limit=1",
- giveMethod: http.MethodGet,
- giveHeader: http.Header{
- "X-API-Status": []string{"success"},
- },
- giveRequest: &InternalTestRequest{
- Id: "123",
- },
- giveQueryParams: url.Values{
- "extra": []string{"true"},
- },
- wantResponse: &InternalTestResponse{
- Id: "123",
- QueryParameters: url.Values{
- "limit": []string{"1"},
- "extra": []string{"true"},
- },
- },
- },
- }
- for _, test := range tests {
- t.Run(test.description, func(t *testing.T) {
- var (
- server = newTestServer(t, test)
- client = server.Client()
- )
- caller := NewCaller(
- &CallerParams{
- Client: client,
- },
- )
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL + test.givePathSuffix,
- Method: test.giveMethod,
- Headers: test.giveHeader,
- BodyProperties: test.giveBodyProperties,
- QueryParameters: test.giveQueryParams,
- Request: test.giveRequest,
- Response: &response,
- ResponseIsOptional: test.giveResponseIsOptional,
- ErrorDecoder: test.giveErrorDecoder,
- },
- )
- if test.wantError != nil {
- assert.EqualError(t, err, test.wantError.Error())
- return
- }
- require.NoError(t, err)
- assert.Equal(t, test.wantResponse, response)
- })
- }
-}
-
-func TestMergeHeaders(t *testing.T) {
- t.Run("both empty", func(t *testing.T) {
- merged := MergeHeaders(make(http.Header), make(http.Header))
- assert.Empty(t, merged)
- })
-
- t.Run("empty left", func(t *testing.T) {
- left := make(http.Header)
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
- })
-
- t.Run("empty right", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Version", "0.0.1")
-
- right := make(http.Header)
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, "0.0.1", merged.Get("X-API-Version"))
- })
-
- t.Run("single value override", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Version", "0.0.0")
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
- })
-
- t.Run("multiple value override", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Versions", "0.0.0")
-
- right := make(http.Header)
- right.Add("X-API-Versions", "0.0.1")
- right.Add("X-API-Versions", "0.0.2")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions"))
- })
-
- t.Run("disjoint merge", func(t *testing.T) {
- left := make(http.Header)
- left.Set("X-API-Tenancy", "test")
-
- right := make(http.Header)
- right.Set("X-API-Version", "0.0.1")
-
- merged := MergeHeaders(left, right)
- assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy"))
- assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version"))
- })
-}
-
-// newTestServer returns a new *httptest.Server configured with the
-// given test parameters.
-func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server {
- return httptest.NewServer(
- http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, tc.giveMethod, r.Method)
- assert.Equal(t, contentType, r.Header.Get(contentTypeHeader))
- for header, value := range tc.giveHeader {
- assert.Equal(t, value, r.Header.Values(header))
- }
-
- request := new(InternalTestRequest)
-
- bytes, err := io.ReadAll(r.Body)
- if tc.giveRequest == nil {
- require.Empty(t, bytes)
- w.WriteHeader(http.StatusBadRequest)
- _, err = w.Write([]byte("invalid request"))
- require.NoError(t, err)
- return
- }
- require.NoError(t, err)
- require.NoError(t, json.Unmarshal(bytes, request))
-
- switch request.Id {
- case strconv.Itoa(http.StatusNotFound):
- notFoundError := &InternalTestNotFoundError{
- APIError: &core.APIError{
- StatusCode: http.StatusNotFound,
- },
- Message: fmt.Sprintf("ID %q not found", request.Id),
- }
- bytes, err = json.Marshal(notFoundError)
- require.NoError(t, err)
-
- w.WriteHeader(http.StatusNotFound)
- _, err = w.Write(bytes)
- require.NoError(t, err)
- return
-
- case strconv.Itoa(http.StatusInternalServerError):
- w.WriteHeader(http.StatusInternalServerError)
- _, err = w.Write([]byte("failed to process request"))
- require.NoError(t, err)
- return
- }
-
- if tc.giveResponseIsOptional {
- w.WriteHeader(http.StatusOK)
- return
- }
-
- extraBodyProperties := make(map[string]interface{})
- require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties))
- delete(extraBodyProperties, "id")
-
- response := &InternalTestResponse{
- Id: request.Id,
- ExtraBodyProperties: extraBodyProperties,
- QueryParameters: r.URL.Query(),
- }
- bytes, err = json.Marshal(response)
- require.NoError(t, err)
-
- _, err = w.Write(bytes)
- require.NoError(t, err)
- },
- ),
- )
-}
-
-// newTestErrorDecoder returns an error decoder suitable for tests.
-func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error {
- return func(statusCode int, header http.Header, body io.Reader) error {
- raw, err := io.ReadAll(body)
- require.NoError(t, err)
-
- var (
- apiError = core.NewAPIError(statusCode, header, errors.New(string(raw)))
- decoder = json.NewDecoder(bytes.NewReader(raw))
- )
- if statusCode == http.StatusNotFound {
- value := new(InternalTestNotFoundError)
- value.APIError = apiError
- require.NoError(t, decoder.Decode(value))
-
- return value
- }
- return apiError
- }
-}
diff --git a/seed/go-sdk/streaming/internal/error_decoder.go b/seed/go-sdk/streaming/internal/error_decoder.go
deleted file mode 100644
index e04ba8093068..000000000000
--- a/seed/go-sdk/streaming/internal/error_decoder.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
-
- "github.com/fern-api/stream-go/v2/core"
-)
-
-// ErrorCodes maps HTTP status codes to error constructors.
-type ErrorCodes map[int]func(*core.APIError) error
-
-// ErrorDecoder decodes *http.Response errors and returns a
-// typed API error (e.g. *core.APIError).
-type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error
-
-// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes.
-// errorCodesOverrides is optional and will be merged with the default error codes,
-// with overrides taking precedence.
-func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder {
- // Merge default error codes with overrides
- mergedErrorCodes := make(ErrorCodes)
-
- // Start with default error codes
- for statusCode, errorFunc := range errorCodes {
- mergedErrorCodes[statusCode] = errorFunc
- }
-
- // Apply overrides if provided
- if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil {
- for statusCode, errorFunc := range errorCodesOverrides[0] {
- mergedErrorCodes[statusCode] = errorFunc
- }
- }
-
- return func(statusCode int, header http.Header, body io.Reader) error {
- raw, err := io.ReadAll(body)
- if err != nil {
- return fmt.Errorf("failed to read error from response body: %w", err)
- }
- apiError := core.NewAPIError(
- statusCode,
- header,
- errors.New(string(raw)),
- )
- newErrorFunc, ok := mergedErrorCodes[statusCode]
- if !ok {
- // This status code isn't recognized, so we return
- // the API error as-is.
- return apiError
- }
- customError := newErrorFunc(apiError)
- if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil {
- // If we fail to decode the error, we return the
- // API error as-is.
- return apiError
- }
- return customError
- }
-}
diff --git a/seed/go-sdk/streaming/internal/error_decoder_test.go b/seed/go-sdk/streaming/internal/error_decoder_test.go
deleted file mode 100644
index 604c09120ac5..000000000000
--- a/seed/go-sdk/streaming/internal/error_decoder_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package internal
-
-import (
- "bytes"
- "errors"
- "net/http"
- "testing"
-
- "github.com/fern-api/stream-go/v2/core"
- "github.com/stretchr/testify/assert"
-)
-
-func TestErrorDecoder(t *testing.T) {
- decoder := NewErrorDecoder(
- ErrorCodes{
- http.StatusNotFound: func(apiError *core.APIError) error {
- return &InternalTestNotFoundError{APIError: apiError}
- },
- })
-
- tests := []struct {
- description string
- giveStatusCode int
- giveHeader http.Header
- giveBody string
- wantError error
- }{
- {
- description: "unrecognized status code",
- giveStatusCode: http.StatusInternalServerError,
- giveHeader: http.Header{},
- giveBody: "Internal Server Error",
- wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")),
- },
- {
- description: "not found with valid JSON",
- giveStatusCode: http.StatusNotFound,
- giveHeader: http.Header{},
- giveBody: `{"message": "Resource not found"}`,
- wantError: &InternalTestNotFoundError{
- APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)),
- Message: "Resource not found",
- },
- },
- {
- description: "not found with invalid JSON",
- giveStatusCode: http.StatusNotFound,
- giveHeader: http.Header{},
- giveBody: `Resource not found`,
- wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.description, func(t *testing.T) {
- assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody))))
- })
- }
-}
diff --git a/seed/go-sdk/streaming/internal/explicit_fields.go b/seed/go-sdk/streaming/internal/explicit_fields.go
deleted file mode 100644
index 4bdf34fc2b7c..000000000000
--- a/seed/go-sdk/streaming/internal/explicit_fields.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package internal
-
-import (
- "math/big"
- "reflect"
- "strings"
-)
-
-// HandleExplicitFields processes a struct to remove `omitempty` from
-// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields).
-// Note that `marshaler` should be an embedded struct to avoid infinite recursion.
-// Returns an interface{} that can be passed to json.Marshal.
-func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} {
- val := reflect.ValueOf(marshaler)
- typ := reflect.TypeOf(marshaler)
-
- // Handle pointer types
- if val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil
- }
- val = val.Elem()
- typ = typ.Elem()
- }
-
- // Only handle struct types
- if val.Kind() != reflect.Struct {
- return marshaler
- }
-
- // Handle embedded struct pattern
- var sourceVal reflect.Value
- var sourceType reflect.Type
-
- // Check if this is an embedded struct pattern
- if typ.NumField() == 1 && typ.Field(0).Anonymous {
- // This is likely an embedded struct, get the embedded value
- embeddedField := val.Field(0)
- sourceVal = embeddedField
- sourceType = embeddedField.Type()
- } else {
- // Regular struct
- sourceVal = val
- sourceType = typ
- }
-
- // If no explicit fields set, use standard marshaling
- if explicitFields == nil || explicitFields.Sign() == 0 {
- return marshaler
- }
-
- // Create a new struct type with modified tags
- fields := make([]reflect.StructField, 0, sourceType.NumField())
-
- for i := 0; i < sourceType.NumField(); i++ {
- field := sourceType.Field(i)
-
- // Skip unexported fields and the explicitFields field itself
- if !field.IsExported() || field.Name == "explicitFields" {
- continue
- }
-
- // Check if this field has been explicitly set
- fieldBit := big.NewInt(1)
- fieldBit.Lsh(fieldBit, uint(i))
- if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 {
- // Remove omitempty from the json tag
- tag := field.Tag.Get("json")
- if tag != "" && tag != "-" {
- // Parse the json tag, remove omitempty from options
- parts := strings.Split(tag, ",")
- if len(parts) > 1 {
- var newParts []string
- newParts = append(newParts, parts[0]) // Keep the field name
- for _, part := range parts[1:] {
- if strings.TrimSpace(part) != "omitempty" {
- newParts = append(newParts, part)
- }
- }
- tag = strings.Join(newParts, ",")
- }
-
- // Reconstruct the struct tag
- newTag := `json:"` + tag + `"`
- if urlTag := field.Tag.Get("url"); urlTag != "" {
- newTag += ` url:"` + urlTag + `"`
- }
-
- field.Tag = reflect.StructTag(newTag)
- }
- }
-
- fields = append(fields, field)
- }
-
- // Create new struct type with modified tags
- newType := reflect.StructOf(fields)
- newVal := reflect.New(newType).Elem()
-
- // Copy field values from original struct to new struct
- fieldIndex := 0
- for i := 0; i < sourceType.NumField(); i++ {
- originalField := sourceType.Field(i)
-
- // Skip unexported fields and the explicitFields field itself
- if !originalField.IsExported() || originalField.Name == "explicitFields" {
- continue
- }
-
- originalValue := sourceVal.Field(i)
- newVal.Field(fieldIndex).Set(originalValue)
- fieldIndex++
- }
-
- return newVal.Interface()
-}
diff --git a/seed/go-sdk/streaming/internal/explicit_fields_test.go b/seed/go-sdk/streaming/internal/explicit_fields_test.go
deleted file mode 100644
index 3d05e88a2ce9..000000000000
--- a/seed/go-sdk/streaming/internal/explicit_fields_test.go
+++ /dev/null
@@ -1,497 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "math/big"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testExplicitFieldsStruct struct {
- Name *string `json:"name,omitempty"`
- Code *string `json:"code,omitempty"`
- Count *int `json:"count,omitempty"`
- Enabled *bool `json:"enabled,omitempty"`
- Tags []string `json:"tags,omitempty"`
- //lint:ignore unused this field is intentionally unused for testing
- unexported string `json:"-"`
- explicitFields *big.Int `json:"-"`
-}
-
-var (
- testFieldName = big.NewInt(1 << 0)
- testFieldCode = big.NewInt(1 << 1)
- testFieldCount = big.NewInt(1 << 2)
- testFieldEnabled = big.NewInt(1 << 3)
- testFieldTags = big.NewInt(1 << 4)
-)
-
-func (t *testExplicitFieldsStruct) require(field *big.Int) {
- if t.explicitFields == nil {
- t.explicitFields = big.NewInt(0)
- }
- t.explicitFields.Or(t.explicitFields, field)
-}
-
-func (t *testExplicitFieldsStruct) SetName(name *string) {
- t.Name = name
- t.require(testFieldName)
-}
-
-func (t *testExplicitFieldsStruct) SetCode(code *string) {
- t.Code = code
- t.require(testFieldCode)
-}
-
-func (t *testExplicitFieldsStruct) SetCount(count *int) {
- t.Count = count
- t.require(testFieldCount)
-}
-
-func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) {
- t.Enabled = enabled
- t.require(testFieldEnabled)
-}
-
-func (t *testExplicitFieldsStruct) SetTags(tags []string) {
- t.Tags = tags
- t.require(testFieldTags)
-}
-
-func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) {
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*t),
- }
- return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields))
-}
-
-type testStructWithoutExplicitFields struct {
- Name *string `json:"name,omitempty"`
- Code *string `json:"code,omitempty"`
-}
-
-func TestHandleExplicitFields(t *testing.T) {
- tests := []struct {
- desc string
- giveInput interface{}
- wantBytes []byte
- wantError string
- }{
- {
- desc: "nil input",
- giveInput: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "non-struct input",
- giveInput: "string",
- wantBytes: []byte(`"string"`),
- },
- {
- desc: "slice input",
- giveInput: []string{"a", "b"},
- wantBytes: []byte(`["a","b"]`),
- },
- {
- desc: "map input",
- giveInput: map[string]interface{}{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "struct without explicitFields field",
- giveInput: &testStructWithoutExplicitFields{
- Name: stringPtr("test"),
- Code: nil,
- },
- wantBytes: []byte(`{"name":"test"}`),
- },
- {
- desc: "struct with no explicit fields set",
- giveInput: &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- Code: nil,
- },
- wantBytes: []byte(`{"name":"test"}`),
- },
- {
- desc: "struct with explicit nil field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- }
- s.SetCode(nil)
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null}`),
- },
- {
- desc: "struct with explicit non-nil field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetName(stringPtr("explicit"))
- s.SetCode(stringPtr("also-explicit"))
- return s
- }(),
- wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`),
- },
- {
- desc: "struct with mixed explicit and implicit fields",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Count: intPtr(42),
- }
- s.SetCode(nil) // explicit nil
- return s
- }(),
- wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`),
- },
- {
- desc: "struct with multiple explicit nil fields",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("test"),
- }
- s.SetCode(nil)
- s.SetCount(nil)
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null,"count":null}`),
- },
- {
- desc: "struct with slice field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{
- Tags: []string{"tag1", "tag2"},
- }
- s.SetTags(nil) // explicit nil slice
- return s
- }(),
- wantBytes: []byte(`{"tags":null}`),
- },
- {
- desc: "struct with boolean field",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetEnabled(boolPtr(false)) // explicit false
- return s
- }(),
- wantBytes: []byte(`{"enabled":false}`),
- },
- {
- desc: "struct with all fields explicit",
- giveInput: func() *testExplicitFieldsStruct {
- s := &testExplicitFieldsStruct{}
- s.SetName(stringPtr("test"))
- s.SetCode(nil)
- s.SetCount(intPtr(0))
- s.SetEnabled(boolPtr(false))
- s.SetTags([]string{})
- return s
- }(),
- wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- var explicitFields *big.Int
- if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok {
- explicitFields = s.explicitFields
- }
- bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields))
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.JSONEq(t, string(tt.wantBytes), string(bytes))
-
- // Verify it's valid JSON
- var value interface{}
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) {
- t.Run("custom marshaler with explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
- s.SetCode(stringPtr("test-code"))
-
- bytes, err := s.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
- })
-
- t.Run("custom marshaler with no explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Code: stringPtr("also-implicit"),
- }
-
- bytes, err := s.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsPointerHandling(t *testing.T) {
- t.Run("nil pointer", func(t *testing.T) {
- var s *testExplicitFieldsStruct
- bytes, err := json.Marshal(HandleExplicitFields(s, nil))
- require.NoError(t, err)
- assert.Equal(t, []byte(`null`), bytes)
- })
-
- t.Run("pointer to struct", func(t *testing.T) {
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
-
- bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
- require.NoError(t, err)
- assert.JSONEq(t, `{"name":null}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) {
- t.Run("embedded struct with explicit fields", func(t *testing.T) {
- // Create a struct similar to what MarshalJSON creates
- s := &testExplicitFieldsStruct{}
- s.SetName(nil)
- s.SetCode(stringPtr("test-code"))
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should include both explicit fields (name as null, code as "test-code")
- assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes))
- })
-
- t.Run("embedded struct with no explicit fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Name: stringPtr("implicit"),
- Code: stringPtr("also-implicit"),
- }
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should only include non-nil fields (omitempty behavior)
- assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes))
- })
-
- t.Run("embedded struct with mixed fields", func(t *testing.T) {
- s := &testExplicitFieldsStruct{
- Count: intPtr(42), // implicit field
- }
- s.SetName(nil) // explicit nil
- s.SetCode(stringPtr("explicit")) // explicit value
-
- type embed testExplicitFieldsStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*s),
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields))
- require.NoError(t, err)
- // Should include explicit null, explicit value, and implicit value
- assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes))
- })
-}
-
-func TestHandleExplicitFieldsTagHandling(t *testing.T) {
- type testStructWithComplexTags struct {
- Field1 *string `json:"field1,omitempty" url:"field1,omitempty"`
- Field2 *string `json:"field2,omitempty,string" url:"field2"`
- Field3 *string `json:"-"`
- Field4 *string `json:"field4"`
- explicitFields *big.Int `json:"-"`
- }
-
- s := &testStructWithComplexTags{
- Field1: stringPtr("test1"),
- Field4: stringPtr("test4"),
- explicitFields: big.NewInt(1), // Only first field is explicit
- }
-
- bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields))
- require.NoError(t, err)
-
- // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included
- assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes))
-}
-
-// Test types for nested struct explicit fields testing
-type testNestedStruct struct {
- NestedName *string `json:"nested_name,omitempty"`
- NestedCode *string `json:"nested_code,omitempty"`
- explicitFields *big.Int `json:"-"`
-}
-
-type testParentStruct struct {
- ParentName *string `json:"parent_name,omitempty"`
- Nested *testNestedStruct `json:"nested,omitempty"`
- explicitFields *big.Int `json:"-"`
-}
-
-var (
- nestedFieldName = big.NewInt(1 << 0)
- nestedFieldCode = big.NewInt(1 << 1)
-)
-
-var (
- parentFieldName = big.NewInt(1 << 0)
- parentFieldNested = big.NewInt(1 << 1)
-)
-
-func (n *testNestedStruct) require(field *big.Int) {
- if n.explicitFields == nil {
- n.explicitFields = big.NewInt(0)
- }
- n.explicitFields.Or(n.explicitFields, field)
-}
-
-func (n *testNestedStruct) SetNestedName(name *string) {
- n.NestedName = name
- n.require(nestedFieldName)
-}
-
-func (n *testNestedStruct) SetNestedCode(code *string) {
- n.NestedCode = code
- n.require(nestedFieldCode)
-}
-
-func (n *testNestedStruct) MarshalJSON() ([]byte, error) {
- type embed testNestedStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*n),
- }
- return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields))
-}
-
-func (p *testParentStruct) require(field *big.Int) {
- if p.explicitFields == nil {
- p.explicitFields = big.NewInt(0)
- }
- p.explicitFields.Or(p.explicitFields, field)
-}
-
-func (p *testParentStruct) SetParentName(name *string) {
- p.ParentName = name
- p.require(parentFieldName)
-}
-
-func (p *testParentStruct) SetNested(nested *testNestedStruct) {
- p.Nested = nested
- p.require(parentFieldNested)
-}
-
-func (p *testParentStruct) MarshalJSON() ([]byte, error) {
- type embed testParentStruct
- var marshaler = struct {
- embed
- }{
- embed: embed(*p),
- }
- return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields))
-}
-
-func TestHandleExplicitFieldsNestedStruct(t *testing.T) {
- tests := []struct {
- desc string
- setupFunc func() *testParentStruct
- wantBytes []byte
- }{
- {
- desc: "nested struct with explicit nil in nested object",
- setupFunc: func() *testParentStruct {
- nested := &testNestedStruct{
- NestedName: stringPtr("implicit-nested"),
- }
- nested.SetNestedCode(nil) // explicit nil
-
- return &testParentStruct{
- ParentName: stringPtr("implicit-parent"),
- Nested: nested,
- }
- },
- wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`),
- },
- {
- desc: "parent with explicit nil nested struct",
- setupFunc: func() *testParentStruct {
- parent := &testParentStruct{
- ParentName: stringPtr("implicit-parent"),
- }
- parent.SetNested(nil) // explicit nil nested struct
- return parent
- },
- wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`),
- },
- {
- desc: "all explicit fields in nested structure",
- setupFunc: func() *testParentStruct {
- nested := &testNestedStruct{}
- nested.SetNestedName(stringPtr("explicit-nested"))
- nested.SetNestedCode(nil) // explicit nil
-
- parent := &testParentStruct{}
- parent.SetParentName(nil) // explicit nil
- parent.SetNested(nested) // explicit nested struct
-
- return parent
- },
- wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`),
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- parent := tt.setupFunc()
- bytes, err := parent.MarshalJSON()
- require.NoError(t, err)
- assert.JSONEq(t, string(tt.wantBytes), string(bytes))
-
- // Verify it's valid JSON
- var value interface{}
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-// Helper functions
-func stringPtr(s string) *string {
- return &s
-}
-
-func intPtr(i int) *int {
- return &i
-}
-
-func boolPtr(b bool) *bool {
- return &b
-}
diff --git a/seed/go-sdk/streaming/internal/extra_properties.go b/seed/go-sdk/streaming/internal/extra_properties.go
deleted file mode 100644
index 540c3fd89eeb..000000000000
--- a/seed/go-sdk/streaming/internal/extra_properties.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package internal
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "reflect"
- "strings"
-)
-
-// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property.
-func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) {
- return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value})
-}
-
-// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties.
-func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) {
- bytes, err := json.Marshal(marshaler)
- if err != nil {
- return nil, err
- }
- if len(extraProperties) == 0 {
- return bytes, nil
- }
- keys, err := getKeys(marshaler)
- if err != nil {
- return nil, err
- }
- for _, key := range keys {
- if _, ok := extraProperties[key]; ok {
- return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key)
- }
- }
- extraBytes, err := json.Marshal(extraProperties)
- if err != nil {
- return nil, err
- }
- if isEmptyJSON(bytes) {
- if isEmptyJSON(extraBytes) {
- return bytes, nil
- }
- return extraBytes, nil
- }
- result := bytes[:len(bytes)-1]
- result = append(result, ',')
- result = append(result, extraBytes[1:len(extraBytes)-1]...)
- result = append(result, '}')
- return result, nil
-}
-
-// ExtractExtraProperties extracts any extra properties from the given value.
-func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) {
- val := reflect.ValueOf(value)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return nil, fmt.Errorf("value must be non-nil to extract extra properties")
- }
- val = val.Elem()
- }
- if err := json.Unmarshal(bytes, &value); err != nil {
- return nil, err
- }
- var extraProperties map[string]interface{}
- if err := json.Unmarshal(bytes, &extraProperties); err != nil {
- return nil, err
- }
- for i := 0; i < val.Type().NumField(); i++ {
- key := jsonKey(val.Type().Field(i))
- if key == "" || key == "-" {
- continue
- }
- delete(extraProperties, key)
- }
- for _, key := range exclude {
- delete(extraProperties, key)
- }
- if len(extraProperties) == 0 {
- return nil, nil
- }
- return extraProperties, nil
-}
-
-// getKeys returns the keys associated with the given value. The value must be a
-// a struct or a map with string keys.
-func getKeys(value interface{}) ([]string, error) {
- val := reflect.ValueOf(value)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- if !val.IsValid() {
- return nil, nil
- }
- switch val.Kind() {
- case reflect.Struct:
- return getKeysForStructType(val.Type()), nil
- case reflect.Map:
- var keys []string
- if val.Type().Key().Kind() != reflect.String {
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
- for _, key := range val.MapKeys() {
- keys = append(keys, key.String())
- }
- return keys, nil
- default:
- return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value)
- }
-}
-
-// getKeysForStructType returns all the keys associated with the given struct type,
-// visiting embedded fields recursively.
-func getKeysForStructType(structType reflect.Type) []string {
- if structType.Kind() == reflect.Pointer {
- structType = structType.Elem()
- }
- if structType.Kind() != reflect.Struct {
- return nil
- }
- var keys []string
- for i := 0; i < structType.NumField(); i++ {
- field := structType.Field(i)
- if field.Anonymous {
- keys = append(keys, getKeysForStructType(field.Type)...)
- continue
- }
- keys = append(keys, jsonKey(field))
- }
- return keys
-}
-
-// jsonKey returns the JSON key from the struct tag of the given field,
-// excluding the omitempty flag (if any).
-func jsonKey(field reflect.StructField) string {
- return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")
-}
-
-// isEmptyJSON returns true if the given data is empty, the empty JSON object, or
-// an explicit null.
-func isEmptyJSON(data []byte) bool {
- return len(data) <= 2 || bytes.Equal(data, []byte("null"))
-}
diff --git a/seed/go-sdk/streaming/internal/extra_properties_test.go b/seed/go-sdk/streaming/internal/extra_properties_test.go
deleted file mode 100644
index aa2510ee5121..000000000000
--- a/seed/go-sdk/streaming/internal/extra_properties_test.go
+++ /dev/null
@@ -1,228 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type testMarshaler struct {
- Name string `json:"name"`
- BirthDate time.Time `json:"birthDate"`
- CreatedAt time.Time `json:"created_at"`
-}
-
-func (t *testMarshaler) MarshalJSON() ([]byte, error) {
- type embed testMarshaler
- var marshaler = struct {
- embed
- BirthDate string `json:"birthDate"`
- CreatedAt string `json:"created_at"`
- }{
- embed: embed(*t),
- BirthDate: t.BirthDate.Format("2006-01-02"),
- CreatedAt: t.CreatedAt.Format(time.RFC3339),
- }
- return MarshalJSONWithExtraProperty(marshaler, "type", "test")
-}
-
-func TestMarshalJSONWithExtraProperties(t *testing.T) {
- tests := []struct {
- desc string
- giveMarshaler interface{}
- giveExtraProperties map[string]interface{}
- wantBytes []byte
- wantError string
- }{
- {
- desc: "invalid type",
- giveMarshaler: []string{"invalid"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid key type",
- giveMarshaler: map[int]interface{}{42: "value"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`,
- },
- {
- desc: "invalid map overwrite",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"key": "overwrite"},
- wantError: `cannot add extra property "key" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"},
- wantError: `cannot add extra property "birthDate" because it is already defined on the type`,
- },
- {
- desc: "invalid struct overwrite embedded type",
- giveMarshaler: new(testMarshaler),
- giveExtraProperties: map[string]interface{}{"name": "bob"},
- wantError: `cannot add extra property "name" because it is already defined on the type`,
- },
- {
- desc: "nil",
- giveMarshaler: nil,
- giveExtraProperties: nil,
- wantBytes: []byte(`null`),
- },
- {
- desc: "empty",
- giveMarshaler: map[string]interface{}{},
- giveExtraProperties: map[string]interface{}{},
- wantBytes: []byte(`{}`),
- },
- {
- desc: "no extra properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "only extra properties",
- giveMarshaler: map[string]interface{}{},
- giveExtraProperties: map[string]interface{}{"key": "value"},
- wantBytes: []byte(`{"key":"value"}`),
- },
- {
- desc: "single extra property",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"extra": "property"},
- wantBytes: []byte(`{"key":"value","extra":"property"}`),
- },
- {
- desc: "multiple extra properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{"one": 1, "two": 2},
- wantBytes: []byte(`{"key":"value","one":1,"two":2}`),
- },
- {
- desc: "nested properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{
- "user": map[string]interface{}{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "multiple nested properties",
- giveMarshaler: map[string]interface{}{"key": "value"},
- giveExtraProperties: map[string]interface{}{
- "metadata": map[string]interface{}{
- "ip": "127.0.0.1",
- },
- "user": map[string]interface{}{
- "age": 42,
- "name": "alice",
- },
- },
- wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`),
- },
- {
- desc: "custom marshaler",
- giveMarshaler: &testMarshaler{
- Name: "alice",
- BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
- CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
- },
- giveExtraProperties: map[string]interface{}{
- "extra": "property",
- },
- wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`),
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties)
- if tt.wantError != "" {
- require.EqualError(t, err, tt.wantError)
- assert.Nil(t, tt.wantBytes)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, tt.wantBytes, bytes)
-
- value := make(map[string]interface{})
- require.NoError(t, json.Unmarshal(bytes, &value))
- })
- }
-}
-
-func TestExtractExtraProperties(t *testing.T) {
- t.Run("none", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value)
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-
- t.Run("non-nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("nil pointer", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value *user
- _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- assert.EqualError(t, err, "value must be non-nil to extract extra properties")
- })
-
- t.Run("non-zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("zero value", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- var value user
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value)
- require.NoError(t, err)
- assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties)
- })
-
- t.Run("exclude", func(t *testing.T) {
- type user struct {
- Name string `json:"name"`
- }
- value := &user{
- Name: "alice",
- }
- extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age")
- require.NoError(t, err)
- assert.Nil(t, extraProperties)
- })
-}
diff --git a/seed/go-sdk/streaming/internal/http.go b/seed/go-sdk/streaming/internal/http.go
deleted file mode 100644
index 77863752bb58..000000000000
--- a/seed/go-sdk/streaming/internal/http.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package internal
-
-import (
- "fmt"
- "net/http"
- "net/url"
- "reflect"
-)
-
-// HTTPClient is an interface for a subset of the *http.Client.
-type HTTPClient interface {
- Do(*http.Request) (*http.Response, error)
-}
-
-// ResolveBaseURL resolves the base URL from the given arguments,
-// preferring the first non-empty value.
-func ResolveBaseURL(values ...string) string {
- for _, value := range values {
- if value != "" {
- return value
- }
- }
- return ""
-}
-
-// EncodeURL encodes the given arguments into the URL, escaping
-// values as needed. Pointer arguments are dereferenced before processing.
-func EncodeURL(urlFormat string, args ...interface{}) string {
- escapedArgs := make([]interface{}, 0, len(args))
- for _, arg := range args {
- // Dereference the argument if it's a pointer
- value := dereferenceArg(arg)
- escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value)))
- }
- return fmt.Sprintf(urlFormat, escapedArgs...)
-}
-
-// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value.
-// If the argument is not a pointer or is nil, it returns the argument as-is.
-func dereferenceArg(arg interface{}) interface{} {
- if arg == nil {
- return arg
- }
-
- v := reflect.ValueOf(arg)
-
- // Keep dereferencing until we get to a non-pointer value or hit nil
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- return nil
- }
- v = v.Elem()
- }
-
- return v.Interface()
-}
-
-// MergeHeaders merges the given headers together, where the right
-// takes precedence over the left.
-func MergeHeaders(left, right http.Header) http.Header {
- for key, values := range right {
- if len(values) > 1 {
- left[key] = values
- continue
- }
- if value := right.Get(key); value != "" {
- left.Set(key, value)
- }
- }
- return left
-}
diff --git a/seed/go-sdk/streaming/internal/query.go b/seed/go-sdk/streaming/internal/query.go
deleted file mode 100644
index 1cbaf7fe1c02..000000000000
--- a/seed/go-sdk/streaming/internal/query.go
+++ /dev/null
@@ -1,353 +0,0 @@
-package internal
-
-import (
- "encoding/base64"
- "fmt"
- "net/url"
- "reflect"
- "strings"
- "time"
-
- "github.com/google/uuid"
-)
-
-var (
- bytesType = reflect.TypeOf([]byte{})
- queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem()
- timeType = reflect.TypeOf(time.Time{})
- uuidType = reflect.TypeOf(uuid.UUID{})
-)
-
-// QueryEncoder is an interface implemented by any type that wishes to encode
-// itself into URL values in a non-standard way.
-type QueryEncoder interface {
- EncodeQueryValues(key string, v *url.Values) error
-}
-
-// prepareValue handles common validation and unwrapping logic for both functions
-func prepareValue(v interface{}) (reflect.Value, url.Values, error) {
- values := make(url.Values)
- val := reflect.ValueOf(v)
- for val.Kind() == reflect.Ptr {
- if val.IsNil() {
- return reflect.Value{}, values, nil
- }
- val = val.Elem()
- }
-
- if v == nil {
- return reflect.Value{}, values, nil
- }
-
- if val.Kind() != reflect.Struct {
- return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
- }
-
- err := reflectValue(values, val, "")
- if err != nil {
- return reflect.Value{}, nil, err
- }
-
- return val, values, nil
-}
-
-// QueryValues encodes url.Values from request objects.
-//
-// Note: This type is inspired by Google's query encoding library, but
-// supports far less customization and is tailored to fit this SDK's use case.
-//
-// Ref: https://github.com/google/go-querystring
-func QueryValues(v interface{}) (url.Values, error) {
- _, values, err := prepareValue(v)
- return values, err
-}
-
-// QueryValuesWithDefaults encodes url.Values from request objects
-// and default values, merging the defaults into the request.
-// It's expected that the values of defaults are wire names.
-func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) {
- val, values, err := prepareValue(v)
- if err != nil {
- return values, err
- }
- if !val.IsValid() {
- return values, nil
- }
-
- // apply defaults to zero-value fields directly on the original struct
- valType := val.Type()
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- fieldType := valType.Field(i)
- fieldName := fieldType.Name
-
- if fieldType.PkgPath != "" && !fieldType.Anonymous {
- // Skip unexported fields.
- continue
- }
-
- // check if field is zero value and we have a default for it
- if field.CanSet() && field.IsZero() {
- tag := fieldType.Tag.Get("url")
- if tag == "" || tag == "-" {
- continue
- }
- wireName, _ := parseTag(tag)
- if wireName == "" {
- wireName = fieldName
- }
- if defaultVal, exists := defaults[wireName]; exists {
- values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{}))
- }
- }
- }
-
- return values, err
-}
-
-// reflectValue populates the values parameter from the struct fields in val.
-// Embedded structs are followed recursively (using the rules defined in the
-// Values function documentation) breadth-first.
-func reflectValue(values url.Values, val reflect.Value, scope string) error {
- typ := val.Type()
- for i := 0; i < typ.NumField(); i++ {
- sf := typ.Field(i)
- if sf.PkgPath != "" && !sf.Anonymous {
- // Skip unexported fields.
- continue
- }
-
- sv := val.Field(i)
- tag := sf.Tag.Get("url")
- if tag == "" || tag == "-" {
- continue
- }
-
- name, opts := parseTag(tag)
- if name == "" {
- name = sf.Name
- }
-
- if scope != "" {
- name = scope + "[" + name + "]"
- }
-
- if opts.Contains("omitempty") && isEmptyValue(sv) {
- continue
- }
-
- if sv.Type().Implements(queryEncoderType) {
- // If sv is a nil pointer and the custom encoder is defined on a non-pointer
- // method receiver, set sv to the zero value of the underlying type
- if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) {
- sv = reflect.New(sv.Type().Elem())
- }
-
- m := sv.Interface().(QueryEncoder)
- if err := m.EncodeQueryValues(name, &values); err != nil {
- return err
- }
- continue
- }
-
- // Recursively dereference pointers, but stop at nil pointers.
- for sv.Kind() == reflect.Ptr {
- if sv.IsNil() {
- break
- }
- sv = sv.Elem()
- }
-
- if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType {
- values.Add(name, valueString(sv, opts, sf))
- continue
- }
-
- if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
- if sv.Len() == 0 {
- // Skip if slice or array is empty.
- continue
- }
- for i := 0; i < sv.Len(); i++ {
- value := sv.Index(i)
- if isStructPointer(value) && !value.IsNil() {
- if err := reflectValue(values, value.Elem(), name); err != nil {
- return err
- }
- } else {
- values.Add(name, valueString(value, opts, sf))
- }
- }
- continue
- }
-
- if sv.Kind() == reflect.Map {
- if err := reflectMap(values, sv, name); err != nil {
- return err
- }
- continue
- }
-
- if sv.Kind() == reflect.Struct {
- if err := reflectValue(values, sv, name); err != nil {
- return err
- }
- continue
- }
-
- values.Add(name, valueString(sv, opts, sf))
- }
-
- return nil
-}
-
-// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value
-func reflectMap(values url.Values, val reflect.Value, scope string) error {
- if val.IsNil() {
- return nil
- }
-
- iter := val.MapRange()
- for iter.Next() {
- k := iter.Key()
- v := iter.Value()
-
- key := fmt.Sprint(k.Interface())
- paramName := scope + "[" + key + "]"
-
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- break
- }
- v = v.Elem()
- }
-
- for v.Kind() == reflect.Interface {
- v = v.Elem()
- }
-
- if v.Kind() == reflect.Map {
- if err := reflectMap(values, v, paramName); err != nil {
- return err
- }
- continue
- }
-
- if v.Kind() == reflect.Struct {
- if err := reflectValue(values, v, paramName); err != nil {
- return err
- }
- continue
- }
-
- if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
- if v.Len() == 0 {
- continue
- }
- for i := 0; i < v.Len(); i++ {
- value := v.Index(i)
- if isStructPointer(value) && !value.IsNil() {
- if err := reflectValue(values, value.Elem(), paramName); err != nil {
- return err
- }
- } else {
- values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{}))
- }
- }
- continue
- }
-
- values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{}))
- }
-
- return nil
-}
-
-// valueString returns the string representation of a value.
-func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string {
- for v.Kind() == reflect.Ptr {
- if v.IsNil() {
- return ""
- }
- v = v.Elem()
- }
-
- if v.Type() == timeType {
- t := v.Interface().(time.Time)
- if format := sf.Tag.Get("format"); format == "date" {
- return t.Format("2006-01-02")
- }
- return t.Format(time.RFC3339)
- }
-
- if v.Type() == uuidType {
- u := v.Interface().(uuid.UUID)
- return u.String()
- }
-
- if v.Type() == bytesType {
- b := v.Interface().([]byte)
- return base64.StdEncoding.EncodeToString(b)
- }
-
- return fmt.Sprint(v.Interface())
-}
-
-// isEmptyValue checks if a value should be considered empty for the purposes
-// of omitting fields with the "omitempty" option.
-func isEmptyValue(v reflect.Value) bool {
- type zeroable interface {
- IsZero() bool
- }
-
- if !v.IsZero() {
- if z, ok := v.Interface().(zeroable); ok {
- return z.IsZero()
- }
- }
-
- switch v.Kind() {
- case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
- return v.Len() == 0
- case reflect.Bool:
- return !v.Bool()
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return v.Int() == 0
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- return v.Uint() == 0
- case reflect.Float32, reflect.Float64:
- return v.Float() == 0
- case reflect.Interface, reflect.Ptr:
- return v.IsNil()
- case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer:
- return false
- }
-
- return false
-}
-
-// isStructPointer returns true if the given reflect.Value is a pointer to a struct.
-func isStructPointer(v reflect.Value) bool {
- return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct
-}
-
-// tagOptions is the string following a comma in a struct field's "url" tag, or
-// the empty string. It does not include the leading comma.
-type tagOptions []string
-
-// parseTag splits a struct field's url tag into its name and comma-separated
-// options.
-func parseTag(tag string) (string, tagOptions) {
- s := strings.Split(tag, ",")
- return s[0], s[1:]
-}
-
-// Contains checks whether the tagOptions contains the specified option.
-func (o tagOptions) Contains(option string) bool {
- for _, s := range o {
- if s == option {
- return true
- }
- }
- return false
-}
diff --git a/seed/go-sdk/streaming/internal/query_test.go b/seed/go-sdk/streaming/internal/query_test.go
deleted file mode 100644
index 2c28cb8acf68..000000000000
--- a/seed/go-sdk/streaming/internal/query_test.go
+++ /dev/null
@@ -1,395 +0,0 @@
-package internal
-
-import (
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestQueryValues(t *testing.T) {
- t.Run("empty optional", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- values, err := QueryValues(&example{})
- require.NoError(t, err)
- assert.Empty(t, values)
- })
-
- t.Run("empty required", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Required string `json:"required" url:"required"`
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- values, err := QueryValues(&example{})
- require.NoError(t, err)
- assert.Equal(t, "required=", values.Encode())
- })
-
- t.Run("allow multiple", func(t *testing.T) {
- type example struct {
- Values []string `json:"values" url:"values"`
- }
-
- values, err := QueryValues(
- &example{
- Values: []string{"foo", "bar", "baz"},
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode())
- })
-
- t.Run("nested object", func(t *testing.T) {
- type nested struct {
- Value *string `json:"value,omitempty" url:"value,omitempty"`
- }
- type example struct {
- Required string `json:"required" url:"required"`
- Nested *nested `json:"nested,omitempty" url:"nested,omitempty"`
- }
-
- nestedValue := "nestedValue"
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- Nested: &nested{
- Value: &nestedValue,
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode())
- })
-
- t.Run("url unspecified", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- NotFound string `json:"notFound"`
- }
-
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- NotFound: "notFound",
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "required=requiredValue", values.Encode())
- })
-
- t.Run("url ignored", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- NotFound string `json:"notFound" url:"-"`
- }
-
- values, err := QueryValues(
- &example{
- Required: "requiredValue",
- NotFound: "notFound",
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "required=requiredValue", values.Encode())
- })
-
- t.Run("datetime", func(t *testing.T) {
- type example struct {
- DateTime time.Time `json:"dateTime" url:"dateTime"`
- }
-
- values, err := QueryValues(
- &example{
- DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode())
- })
-
- t.Run("date", func(t *testing.T) {
- type example struct {
- Date time.Time `json:"date" url:"date" format:"date"`
- }
-
- values, err := QueryValues(
- &example{
- Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC),
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "date=1994-03-16", values.Encode())
- })
-
- t.Run("optional time", func(t *testing.T) {
- type example struct {
- Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"`
- }
-
- values, err := QueryValues(
- &example{},
- )
- require.NoError(t, err)
- assert.Empty(t, values.Encode())
- })
-
- t.Run("omitempty with non-pointer zero value", func(t *testing.T) {
- type enum string
-
- type example struct {
- Enum enum `json:"enum,omitempty" url:"enum,omitempty"`
- }
-
- values, err := QueryValues(
- &example{},
- )
- require.NoError(t, err)
- assert.Empty(t, values.Encode())
- })
-
- t.Run("object array", func(t *testing.T) {
- type object struct {
- Key string `json:"key" url:"key"`
- Value string `json:"value" url:"value"`
- }
- type example struct {
- Objects []*object `json:"objects,omitempty" url:"objects,omitempty"`
- }
-
- values, err := QueryValues(
- &example{
- Objects: []*object{
- {
- Key: "hello",
- Value: "world",
- },
- {
- Key: "foo",
- Value: "bar",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode())
- })
-
- t.Run("map", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "foo": "bar",
- "baz": "qux",
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode())
- })
-
- t.Run("nested map", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "inner": map[string]interface{}{
- "foo": "bar",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode())
- })
-
- t.Run("nested map array", func(t *testing.T) {
- type request struct {
- Metadata map[string]interface{} `json:"metadata" url:"metadata"`
- }
- values, err := QueryValues(
- &request{
- Metadata: map[string]interface{}{
- "inner": []string{
- "one",
- "two",
- "three",
- },
- },
- },
- )
- require.NoError(t, err)
- assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode())
- })
-}
-
-func TestQueryValuesWithDefaults(t *testing.T) {
- t.Run("apply defaults to zero values", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- Enabled bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
- })
-
- t.Run("preserve non-zero values over defaults", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- Enabled bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- values, err := QueryValuesWithDefaults(&example{
- Name: "actual-name",
- Age: 30,
- // Enabled remains false (zero value), should get default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode())
- })
-
- t.Run("ignore defaults for fields not in struct", func(t *testing.T) {
- type example struct {
- Name string `json:"name" url:"name"`
- Age int `json:"age" url:"age"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "nonexistent": "should-be-ignored",
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&name=default-name", values.Encode())
- })
-
- t.Run("type conversion for compatible defaults", func(t *testing.T) {
- type example struct {
- Count int64 `json:"count" url:"count"`
- Rate float64 `json:"rate" url:"rate"`
- Message string `json:"message" url:"message"`
- }
-
- defaults := map[string]interface{}{
- "count": int(100), // int -> int64 conversion
- "rate": float32(2.5), // float32 -> float64 conversion
- "message": "hello", // string -> string (no conversion needed)
- }
-
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode())
- })
-
- t.Run("mixed with pointer fields and omitempty", func(t *testing.T) {
- type example struct {
- Required string `json:"required" url:"required"`
- Optional *string `json:"optional,omitempty" url:"optional,omitempty"`
- Count int `json:"count,omitempty" url:"count,omitempty"`
- }
-
- defaultOptional := "default-optional"
- defaults := map[string]interface{}{
- "required": "default-required",
- "optional": &defaultOptional, // pointer type
- "count": 42,
- }
-
- values, err := QueryValuesWithDefaults(&example{
- Required: "custom-required", // should override default
- // Optional is nil, should get default
- // Count is 0, should get default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode())
- })
-
- t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) {
- type example struct {
- Name *string `json:"name" url:"name"`
- Age *int `json:"age" url:"age"`
- Enabled *bool `json:"enabled" url:"enabled"`
- }
-
- defaults := map[string]interface{}{
- "name": "default-name",
- "age": 25,
- "enabled": true,
- }
-
- // first, test that a properly empty request is overridden:
- {
- values, err := QueryValuesWithDefaults(&example{}, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode())
- }
-
- // second, test that a request that contains zeros is not overridden:
- var (
- name = ""
- age = 0
- enabled = false
- )
- values, err := QueryValuesWithDefaults(&example{
- Name: &name, // explicit empty string should override default
- Age: &age, // explicit zero should override default
- Enabled: &enabled, // explicit false should override default
- }, defaults)
- require.NoError(t, err)
- assert.Equal(t, "age=0&enabled=false&name=", values.Encode())
- })
-
- t.Run("nil input returns empty values", func(t *testing.T) {
- defaults := map[string]any{
- "name": "default-name",
- "age": 25,
- }
-
- // Test with nil
- values, err := QueryValuesWithDefaults(nil, defaults)
- require.NoError(t, err)
- assert.Empty(t, values)
-
- // Test with nil pointer
- type example struct {
- Name string `json:"name" url:"name"`
- }
- var nilPtr *example
- values, err = QueryValuesWithDefaults(nilPtr, defaults)
- require.NoError(t, err)
- assert.Empty(t, values)
- })
-}
diff --git a/seed/go-sdk/streaming/internal/retrier.go b/seed/go-sdk/streaming/internal/retrier.go
deleted file mode 100644
index 4efae1b4c286..000000000000
--- a/seed/go-sdk/streaming/internal/retrier.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package internal
-
-import (
- "crypto/rand"
- "math/big"
- "net/http"
- "strconv"
- "time"
-)
-
-const (
- defaultRetryAttempts = 2
- minRetryDelay = 1000 * time.Millisecond
- maxRetryDelay = 60000 * time.Millisecond
-)
-
-// RetryOption adapts the behavior the *Retrier.
-type RetryOption func(*retryOptions)
-
-// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do).
-type RetryFunc func(*http.Request) (*http.Response, error)
-
-// WithMaxAttempts configures the maximum number of attempts
-// of the *Retrier.
-func WithMaxAttempts(attempts uint) RetryOption {
- return func(opts *retryOptions) {
- opts.attempts = attempts
- }
-}
-
-// Retrier retries failed requests a configurable number of times with an
-// exponential back-off between each retry.
-type Retrier struct {
- attempts uint
-}
-
-// NewRetrier constructs a new *Retrier with the given options, if any.
-func NewRetrier(opts ...RetryOption) *Retrier {
- options := new(retryOptions)
- for _, opt := range opts {
- opt(options)
- }
- attempts := uint(defaultRetryAttempts)
- if options.attempts > 0 {
- attempts = options.attempts
- }
- return &Retrier{
- attempts: attempts,
- }
-}
-
-// Run issues the request and, upon failure, retries the request if possible.
-//
-// The request will be retried as long as the request is deemed retryable and the
-// number of retry attempts has not grown larger than the configured retry limit.
-func (r *Retrier) Run(
- fn RetryFunc,
- request *http.Request,
- errorDecoder ErrorDecoder,
- opts ...RetryOption,
-) (*http.Response, error) {
- options := new(retryOptions)
- for _, opt := range opts {
- opt(options)
- }
- maxRetryAttempts := r.attempts
- if options.attempts > 0 {
- maxRetryAttempts = options.attempts
- }
- var (
- retryAttempt uint
- previousError error
- )
- return r.run(
- fn,
- request,
- errorDecoder,
- maxRetryAttempts,
- retryAttempt,
- previousError,
- )
-}
-
-func (r *Retrier) run(
- fn RetryFunc,
- request *http.Request,
- errorDecoder ErrorDecoder,
- maxRetryAttempts uint,
- retryAttempt uint,
- previousError error,
-) (*http.Response, error) {
- if retryAttempt >= maxRetryAttempts {
- return nil, previousError
- }
-
- // If the call has been cancelled, don't issue the request.
- if err := request.Context().Err(); err != nil {
- return nil, err
- }
-
- response, err := fn(request)
- if err != nil {
- return nil, err
- }
-
- if r.shouldRetry(response) {
- defer response.Body.Close()
-
- delay, err := r.retryDelay(response, retryAttempt)
- if err != nil {
- return nil, err
- }
-
- time.Sleep(delay)
-
- return r.run(
- fn,
- request,
- errorDecoder,
- maxRetryAttempts,
- retryAttempt+1,
- decodeError(response, errorDecoder),
- )
- }
-
- return response, nil
-}
-
-// shouldRetry returns true if the request should be retried based on the given
-// response status code.
-func (r *Retrier) shouldRetry(response *http.Response) bool {
- return response.StatusCode == http.StatusTooManyRequests ||
- response.StatusCode == http.StatusRequestTimeout ||
- response.StatusCode >= http.StatusInternalServerError
-}
-
-// retryDelay calculates the delay time based on response headers,
-// falling back to exponential backoff if no headers are present.
-func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) {
- // Check for Retry-After header first (RFC 7231), applying no jitter
- if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
- // Parse as number of seconds...
- if seconds, err := strconv.Atoi(retryAfter); err == nil {
- delay := time.Duration(seconds) * time.Second
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return delay, nil
- }
- }
-
- // ...or as an HTTP date; both are valid
- if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil {
- delay := time.Until(retryTime)
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return delay, nil
- }
- }
- }
-
- // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter
- if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" {
- if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil {
- // Assume Unix timestamp in seconds
- resetTime := time.Unix(resetTimestamp, 0)
- delay := time.Until(resetTime)
- if delay > 0 {
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
- return r.addPositiveJitter(delay)
- }
- }
- }
-
- // Fall back to exponential backoff
- return r.exponentialBackoff(retryAttempt)
-}
-
-// exponentialBackoff calculates the delay time based on the retry attempt
-// and applies symmetric jitter (±10% around the delay).
-func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) {
- if retryAttempt > 63 { // 2^63+ would overflow uint64
- retryAttempt = 63
- }
-
- delay := minRetryDelay << retryAttempt
- if delay > maxRetryDelay {
- delay = maxRetryDelay
- }
-
- return r.addSymmetricJitter(delay)
-}
-
-// addJitterWithRange applies jitter to the given delay.
-// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%).
-func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) {
- jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100))
- jitter, err := rand.Int(rand.Reader, jitterRange)
- if err != nil {
- return 0, err
- }
-
- jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100
- if jitteredDelay < minRetryDelay {
- jitteredDelay = minRetryDelay
- }
- if jitteredDelay > maxRetryDelay {
- jitteredDelay = maxRetryDelay
- }
- return jitteredDelay, nil
-}
-
-// addPositiveJitter applies positive jitter to the given delay (100%-120% range).
-func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) {
- return r.addJitterWithRange(delay, 100, 120)
-}
-
-// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range).
-func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) {
- return r.addJitterWithRange(delay, 90, 110)
-}
-
-type retryOptions struct {
- attempts uint
-}
diff --git a/seed/go-sdk/streaming/internal/retrier_test.go b/seed/go-sdk/streaming/internal/retrier_test.go
deleted file mode 100644
index 15242fb5062d..000000000000
--- a/seed/go-sdk/streaming/internal/retrier_test.go
+++ /dev/null
@@ -1,300 +0,0 @@
-package internal
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/fern-api/stream-go/v2/core"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type RetryTestCase struct {
- description string
-
- giveAttempts uint
- giveStatusCodes []int
- giveResponse *InternalTestResponse
-
- wantResponse *InternalTestResponse
- wantError *core.APIError
-}
-
-func TestRetrier(t *testing.T) {
- tests := []*RetryTestCase{
- {
- description: "retry request succeeds after multiple failures",
- giveAttempts: 3,
- giveStatusCodes: []int{
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusOK,
- },
- giveResponse: &InternalTestResponse{
- Id: "1",
- },
- wantResponse: &InternalTestResponse{
- Id: "1",
- },
- },
- {
- description: "retry request fails if MaxAttempts is exceeded",
- giveAttempts: 3,
- giveStatusCodes: []int{
- http.StatusRequestTimeout,
- http.StatusRequestTimeout,
- http.StatusRequestTimeout,
- http.StatusOK,
- },
- wantError: &core.APIError{
- StatusCode: http.StatusRequestTimeout,
- },
- },
- {
- description: "retry durations increase exponentially and stay within the min and max delay values",
- giveAttempts: 4,
- giveStatusCodes: []int{
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusServiceUnavailable,
- http.StatusOK,
- },
- },
- {
- description: "retry does not occur on status code 404",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusNotFound, http.StatusOK},
- wantError: &core.APIError{
- StatusCode: http.StatusNotFound,
- },
- },
- {
- description: "retries occur on status code 429",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK},
- },
- {
- description: "retries occur on status code 408",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
- },
- {
- description: "retries occur on status code 500",
- giveAttempts: 2,
- giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
- },
- }
-
- for _, tc := range tests {
- t.Run(tc.description, func(t *testing.T) {
- var (
- test = tc
- server = newTestRetryServer(t, test)
- client = server.Client()
- )
-
- t.Parallel()
-
- caller := NewCaller(
- &CallerParams{
- Client: client,
- },
- )
-
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL,
- Method: http.MethodGet,
- Request: &InternalTestRequest{},
- Response: &response,
- MaxAttempts: test.giveAttempts,
- ResponseIsOptional: true,
- },
- )
-
- if test.wantError != nil {
- require.IsType(t, err, &core.APIError{})
- expectedErrorCode := test.wantError.StatusCode
- actualErrorCode := err.(*core.APIError).StatusCode
- assert.Equal(t, expectedErrorCode, actualErrorCode)
- return
- }
-
- require.NoError(t, err)
- assert.Equal(t, test.wantResponse, response)
- })
- }
-}
-
-// newTestRetryServer returns a new *httptest.Server configured with the
-// given test parameters, suitable for testing retries.
-func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server {
- var index int
- timestamps := make([]time.Time, 0, len(tc.giveStatusCodes))
-
- return httptest.NewServer(
- http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- timestamps = append(timestamps, time.Now())
- if index > 0 && index < len(expectedRetryDurations) {
- // Ensure that the duration between retries increases exponentially,
- // and that it is within the minimum and maximum retry delay values.
- actualDuration := timestamps[index].Sub(timestamps[index-1])
- expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100
- expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100
- assert.True(
- t,
- actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax,
- "expected duration to be in range [%v, %v], got %v",
- expectedDurationMin,
- expectedDurationMax,
- actualDuration,
- )
- assert.LessOrEqual(
- t,
- actualDuration,
- maxRetryDelay,
- "expected duration to be less than the maxRetryDelay (%v), got %v",
- maxRetryDelay,
- actualDuration,
- )
- assert.GreaterOrEqual(
- t,
- actualDuration,
- minRetryDelay,
- "expected duration to be greater than the minRetryDelay (%v), got %v",
- minRetryDelay,
- actualDuration,
- )
- }
-
- request := new(InternalTestRequest)
- bytes, err := io.ReadAll(r.Body)
- require.NoError(t, err)
- require.NoError(t, json.Unmarshal(bytes, request))
- require.LessOrEqual(t, index, len(tc.giveStatusCodes))
-
- statusCode := tc.giveStatusCodes[index]
-
- w.WriteHeader(statusCode)
-
- if tc.giveResponse != nil && statusCode == http.StatusOK {
- bytes, err = json.Marshal(tc.giveResponse)
- require.NoError(t, err)
- _, err = w.Write(bytes)
- require.NoError(t, err)
- }
-
- index++
- },
- ),
- )
-}
-
-// expectedRetryDurations holds an array of calculated retry durations,
-// where the index of the array should correspond to the retry attempt.
-//
-// Values are calculated based off of `minRetryDelay * 2^i`.
-var expectedRetryDurations = []time.Duration{
- 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms
- 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms
- 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms
- 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms
-}
-
-func TestRetryDelayTiming(t *testing.T) {
- tests := []struct {
- name string
- headerName string
- headerValueFunc func() string
- expectedMinMs int64
- expectedMaxMs int64
- }{
- {
- name: "retry-after with seconds value",
- headerName: "retry-after",
- headerValueFunc: func() string {
- return "1"
- },
- expectedMinMs: 500,
- expectedMaxMs: 1500,
- },
- {
- name: "retry-after with HTTP date",
- headerName: "retry-after",
- headerValueFunc: func() string {
- return time.Now().Add(3 * time.Second).Format(time.RFC1123)
- },
- expectedMinMs: 1500,
- expectedMaxMs: 4500,
- },
- {
- name: "x-ratelimit-reset with future timestamp",
- headerName: "x-ratelimit-reset",
- headerValueFunc: func() string {
- return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix())
- },
- expectedMinMs: 1500,
- expectedMaxMs: 4500,
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- var timestamps []time.Time
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- timestamps = append(timestamps, time.Now())
- if len(timestamps) == 1 {
- // First request - return retryable error with header
- w.Header().Set(tt.headerName, tt.headerValueFunc())
- w.WriteHeader(http.StatusTooManyRequests)
- } else {
- // Second request - return success
- w.WriteHeader(http.StatusOK)
- response := &InternalTestResponse{Id: "success"}
- bytes, _ := json.Marshal(response)
- w.Write(bytes)
- }
- }))
- defer server.Close()
-
- caller := NewCaller(&CallerParams{
- Client: server.Client(),
- })
-
- var response *InternalTestResponse
- _, err := caller.Call(
- context.Background(),
- &CallParams{
- URL: server.URL,
- Method: http.MethodGet,
- Request: &InternalTestRequest{},
- Response: &response,
- MaxAttempts: 2,
- ResponseIsOptional: true,
- },
- )
-
- require.NoError(t, err)
- require.Len(t, timestamps, 2, "Expected exactly 2 requests")
-
- actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds()
-
- assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs,
- "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs)
- assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs,
- "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs)
- })
- }
-}
diff --git a/seed/go-sdk/streaming/internal/streamer.go b/seed/go-sdk/streaming/internal/streamer.go
deleted file mode 100644
index 205dff896cab..000000000000
--- a/seed/go-sdk/streaming/internal/streamer.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package internal
-
-import (
- "context"
- "net/http"
- "net/url"
-
- "github.com/fern-api/stream-go/v2/core"
-)
-
-const (
- // DefaultDataPrefix is the default prefix used for SSE streaming.
- DefaultSSEDataPrefix = "data: "
-
- // DefaultTerminator is the default terminator used for SSE streaming.
- DefaultSSETerminator = "[DONE]"
-)
-
-// Streamer calls APIs and streams responses using a *Stream.
-type Streamer[T any] struct {
- client HTTPClient
- retrier *Retrier
-}
-
-// NewStreamer returns a new *Streamer backed by the given caller's HTTP client.
-func NewStreamer[T any](caller *Caller) *Streamer[T] {
- return &Streamer[T]{
- client: caller.client,
- retrier: caller.retrier,
- }
-}
-
-// StreamParams represents the parameters used to issue an API streaming call.
-type StreamParams struct {
- URL string
- Method string
- Prefix string
- Delimiter string
- Terminator string
- MaxAttempts uint
- Headers http.Header
- BodyProperties map[string]interface{}
- QueryParameters url.Values
- Client HTTPClient
- Request interface{}
- ErrorDecoder ErrorDecoder
- Format core.StreamFormat
-}
-
-// Stream issues an API streaming call according to the given stream parameters.
-func (s *Streamer[T]) Stream(ctx context.Context, params *StreamParams) (*core.Stream[T], error) {
- url := buildURL(params.URL, params.QueryParameters)
- req, err := newRequest(
- ctx,
- url,
- params.Method,
- params.Headers,
- params.Request,
- params.BodyProperties,
- )
- if err != nil {
- return nil, err
- }
-
- // If the call has been cancelled, don't issue the request.
- if err := ctx.Err(); err != nil {
- return nil, err
- }
-
- client := s.client
- if params.Client != nil {
- // Use the HTTP client scoped to the request.
- client = params.Client
- }
-
- var retryOptions []RetryOption
- if params.MaxAttempts > 0 {
- retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts))
- }
-
- resp, err := s.retrier.Run(
- client.Do,
- req,
- params.ErrorDecoder,
- retryOptions...,
- )
- if err != nil {
- return nil, err
- }
-
- // Check if the call was cancelled before we return the error
- // associated with the call and/or unmarshal the response data.
- if err := ctx.Err(); err != nil {
- defer resp.Body.Close()
- return nil, err
- }
-
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- defer resp.Body.Close()
- return nil, decodeError(resp, params.ErrorDecoder)
- }
-
- var opts []core.StreamOption
- if params.Delimiter != "" {
- opts = append(opts, core.WithDelimiter(params.Delimiter))
- }
- if params.Prefix != "" {
- opts = append(opts, core.WithPrefix(params.Prefix))
- }
- if params.Terminator != "" {
- opts = append(opts, core.WithTerminator(params.Terminator))
- }
- if params.Format != core.StreamFormatEmpty {
- opts = append(opts, core.WithFormat(params.Format))
- }
-
- return core.NewStream[T](resp, opts...), nil
-}
diff --git a/seed/go-sdk/streaming/internal/stringer.go b/seed/go-sdk/streaming/internal/stringer.go
deleted file mode 100644
index 312801851e0e..000000000000
--- a/seed/go-sdk/streaming/internal/stringer.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package internal
-
-import "encoding/json"
-
-// StringifyJSON returns a pretty JSON string representation of
-// the given value.
-func StringifyJSON(value interface{}) (string, error) {
- bytes, err := json.MarshalIndent(value, "", " ")
- if err != nil {
- return "", err
- }
- return string(bytes), nil
-}
diff --git a/seed/go-sdk/streaming/internal/time.go b/seed/go-sdk/streaming/internal/time.go
deleted file mode 100644
index ab0e269fade3..000000000000
--- a/seed/go-sdk/streaming/internal/time.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "time"
-)
-
-const dateFormat = "2006-01-02"
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date (e.g. 2006-01-02).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type Date struct {
- t *time.Time
-}
-
-// NewDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewDate(t time.Time) *Date {
- return &Date{t: &t}
-}
-
-// NewOptionalDate returns a new *Date. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDate(t *time.Time) *Date {
- if t == nil {
- return nil
- }
- return &Date{t: t}
-}
-
-// Time returns the Date's underlying time, if any. If the
-// date is nil, the zero value is returned.
-func (d *Date) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the Date's underlying time.Time, if any.
-func (d *Date) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *Date) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(dateFormat))
-}
-
-func (d *Date) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(dateFormat, raw)
- if err != nil {
- return err
- }
-
- *d = Date{t: &parsedTime}
- return nil
-}
-
-// DateTime wraps time.Time and adapts its JSON representation
-// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z).
-//
-// Ref: https://ijmacd.github.io/rfc3339-iso8601
-type DateTime struct {
- t *time.Time
-}
-
-// NewDateTime returns a new *DateTime.
-func NewDateTime(t time.Time) *DateTime {
- return &DateTime{t: &t}
-}
-
-// NewOptionalDateTime returns a new *DateTime. If the given time.Time
-// is nil, nil will be returned.
-func NewOptionalDateTime(t *time.Time) *DateTime {
- if t == nil {
- return nil
- }
- return &DateTime{t: t}
-}
-
-// Time returns the DateTime's underlying time, if any. If the
-// date-time is nil, the zero value is returned.
-func (d *DateTime) Time() time.Time {
- if d == nil || d.t == nil {
- return time.Time{}
- }
- return *d.t
-}
-
-// TimePtr returns a pointer to the DateTime's underlying time.Time, if any.
-func (d *DateTime) TimePtr() *time.Time {
- if d == nil || d.t == nil {
- return nil
- }
- if d.t.IsZero() {
- return nil
- }
- return d.t
-}
-
-func (d *DateTime) MarshalJSON() ([]byte, error) {
- if d == nil || d.t == nil {
- return nil, nil
- }
- return json.Marshal(d.t.Format(time.RFC3339))
-}
-
-func (d *DateTime) UnmarshalJSON(data []byte) error {
- var raw string
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
-
- parsedTime, err := time.Parse(time.RFC3339, raw)
- if err != nil {
- return err
- }
-
- *d = DateTime{t: &parsedTime}
- return nil
-}
diff --git a/seed/go-sdk/streaming/option/request_option.go b/seed/go-sdk/streaming/option/request_option.go
deleted file mode 100644
index 52c6b5d8bdf9..000000000000
--- a/seed/go-sdk/streaming/option/request_option.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Code generated by Fern. DO NOT EDIT.
-
-package option
-
-import (
- core "github.com/fern-api/stream-go/v2/core"
- http "net/http"
- url "net/url"
-)
-
-// RequestOption adapts the behavior of an individual request.
-type RequestOption = core.RequestOption
-
-// WithBaseURL sets the base URL, overriding the default
-// environment, if any.
-func WithBaseURL(baseURL string) *core.BaseURLOption {
- return &core.BaseURLOption{
- BaseURL: baseURL,
- }
-}
-
-// WithHTTPClient uses the given HTTPClient to issue the request.
-func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption {
- return &core.HTTPClientOption{
- HTTPClient: httpClient,
- }
-}
-
-// WithHTTPHeader adds the given http.Header to the request.
-func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption {
- return &core.HTTPHeaderOption{
- // Clone the headers so they can't be modified after the option call.
- HTTPHeader: httpHeader.Clone(),
- }
-}
-
-// WithBodyProperties adds the given body properties to the request.
-func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption {
- copiedBodyProperties := make(map[string]interface{}, len(bodyProperties))
- for key, value := range bodyProperties {
- copiedBodyProperties[key] = value
- }
- return &core.BodyPropertiesOption{
- BodyProperties: copiedBodyProperties,
- }
-}
-
-// WithQueryParameters adds the given query parameters to the request.
-func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption {
- copiedQueryParameters := make(url.Values, len(queryParameters))
- for key, values := range queryParameters {
- copiedQueryParameters[key] = values
- }
- return &core.QueryParametersOption{
- QueryParameters: copiedQueryParameters,
- }
-}
-
-// WithMaxAttempts configures the maximum number of retry attempts.
-func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption {
- return &core.MaxAttemptsOption{
- MaxAttempts: attempts,
- }
-}