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 - --[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](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) -+[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Fern%2FTypeScript) - [![npm shield](https://img.shields.io/npm/v/)](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 + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](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 + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](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 + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](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, - } -}