Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9edf3cd
chore(rust): enable git and git config for rust sdk generator docker
iamnamananand996 Aug 13, 2025
3119086
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 15, 2025
6bc8f32
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 15, 2025
43ddd8f
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 16, 2025
1d08d9d
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 18, 2025
fb67226
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 20, 2025
c052f98
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 20, 2025
f432710
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 25, 2025
10a142c
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 27, 2025
e2310d3
Merge branch 'fern-api:main' into main
iamnamananand996 Aug 27, 2025
f9ef3e7
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 1, 2025
a762e9a
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 4, 2025
dc6d55c
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 10, 2025
d33fef0
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 11, 2025
7ccae94
feat(rust): support `dynamic-snippets`
iamnamananand996 Sep 11, 2025
03555e1
fix: biome lint
iamnamananand996 Sep 11, 2025
209e0eb
chore(rust): update seed
iamnamananand996 Sep 11, 2025
8b27bc8
fix: biome
iamnamananand996 Sep 11, 2025
0c604ed
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 11, 2025
68678ff
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 16, 2025
275f573
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 16, 2025
2744bf8
Merge branch 'main' of https://github.com/iamnamananand996/fern into …
iamnamananand996 Sep 16, 2025
b25c8a7
refactor(rust): dynamic-snippets folder structure and formatting
iamnamananand996 Sep 16, 2025
1d36021
Merge branch 'fern-api:main' into main
iamnamananand996 Sep 16, 2025
4776efd
Merge branch 'main' of https://github.com/iamnamananand996/fern into …
iamnamananand996 Sep 16, 2025
cfdf5b8
feat(rust): add formatting for readme snips
iamnamananand996 Sep 17, 2025
3a1a529
chore(rust): update seed
iamnamananand996 Sep 17, 2025
937bc63
fix: biome check
iamnamananand996 Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
65 changes: 64 additions & 1 deletion generators/rust/base/src/formatter/RustFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Logger } from "@fern-api/logger";
import { spawn } from "child_process";
import { spawn, spawnSync } from "child_process";

export interface RustFormatterOptions {
outputDir: string;
Expand Down Expand Up @@ -52,3 +52,66 @@ export async function formatRustCode({ outputDir, logger }: RustFormatterOptions
});
});
}

/**
* Format a single Rust code snippet using rustfmt
* @param code The Rust code to format
* @param logger Optional logger for debug output
* @returns The formatted code, or original code if formatting fails
*/
export function formatRustSnippet(code: string, logger?: Logger): string {
try {
const result = spawnSync("rustfmt", ["--edition=2021"], {
input: code,
encoding: "utf8",
timeout: 5000 // 5 second timeout
});

if (result.status === 0 && result.stdout) {
logger?.debug("rustfmt snippet formatting successful");
return result.stdout;
} else {
logger?.debug(`rustfmt snippet formatting failed with status ${result.status}`);
}
} catch (error) {
logger?.debug(`rustfmt snippet formatting error: ${error}`);
}
return code; // Fallback to original if formatting fails
}

/**
* Format a single Rust code snippet asynchronously using rustfmt
* @param code The Rust code to format
* @param logger Optional logger for debug output
* @returns Promise that resolves to formatted code, or original code if formatting fails
*/
export async function formatRustSnippetAsync(code: string, logger?: Logger): Promise<string> {
return new Promise((resolve) => {
const rustfmt = spawn("rustfmt", ["--edition=2021"], {
stdio: "pipe"
});

rustfmt.stdin.write(code);
rustfmt.stdin.end();

let formatted = "";
rustfmt.stdout.on("data", (data) => {
formatted += data.toString();
});

rustfmt.on("close", (exitCode) => {
if (exitCode === 0 && formatted) {
logger?.debug("rustfmt async snippet formatting successful");
resolve(formatted);
} else {
logger?.debug(`rustfmt async snippet formatting failed with exit code ${exitCode}`);
resolve(code); // Fallback to original
}
});

rustfmt.on("error", (error) => {
logger?.debug(`rustfmt async snippet formatting error: ${error.message}`);
resolve(code); // Fallback to original
});
});
}
2 changes: 1 addition & 1 deletion generators/rust/base/src/formatter/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { RustFormatterOptions } from "./RustFormatter";
export { formatRustCode } from "./RustFormatter";
export { formatRustCode, formatRustSnippet, formatRustSnippetAsync } from "./RustFormatter";
1 change: 1 addition & 0 deletions generators/rust/dynamic-snippets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@fern-api/core-utils": "workspace:*",
"@fern-api/dynamic-ir-sdk": "^59.6.1",
"@fern-api/path-utils": "workspace:*",
"@fern-api/rust-base": "workspace:*",
"@fern-api/rust-codegen": "workspace:*",
"@types/lodash-es": "^4.17.12",
"@types/node": "18.15.3",
Expand Down
29 changes: 22 additions & 7 deletions generators/rust/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AbstractFormatter, Scope, Severity } from "@fern-api/browser-compatible-base-generator";
import { assertNever } from "@fern-api/core-utils";
import { FernIr } from "@fern-api/dynamic-ir-sdk";
import { formatRustSnippet, formatRustSnippetAsync } from "@fern-api/rust-base";
import { rust } from "@fern-api/rust-codegen";

import { DynamicSnippetsGeneratorContext } from "./context/DynamicSnippetsGeneratorContext";
Expand All @@ -24,7 +25,10 @@ export class EndpointSnippetGenerator {
request: FernIr.dynamic.EndpointSnippetRequest;
}): Promise<string> {
const components = this.buildCodeComponents({ endpoint, snippet: request });
return components.join("\n") + "\n";
const rawCode = components.join("\n") + "\n";
// Try to format with rustfmt
const formattedCode = await formatRustSnippetAsync(rawCode);
return formattedCode;
}

public generateSnippetSync({
Expand All @@ -35,7 +39,10 @@ export class EndpointSnippetGenerator {
request: FernIr.dynamic.EndpointSnippetRequest;
}): string {
const components = this.buildCodeComponents({ endpoint, snippet: request });
return components.join("\n") + "\n";
const rawCode = components.join("\n") + "\n";
// Try sync formatting with rustfmt, but fallback to raw code if it fails
const formattedCode = formatRustSnippet(rawCode);
return formattedCode;
}

private buildCodeBlock({
Expand Down Expand Up @@ -110,7 +117,9 @@ export class EndpointSnippetGenerator {
method: "expect",
args: [rust.Expression.stringLiteral("Failed to build client")]
})
})
}),
// Add the actual API method call
this.callMethod({ endpoint, snippet })
]);

// Create the standalone function
Expand Down Expand Up @@ -146,14 +155,20 @@ export class EndpointSnippetGenerator {
endpoint: FernIr.dynamic.Endpoint;
snippet: FernIr.dynamic.EndpointSnippetRequest;
}): rust.UseStatement[] {
const imports = [
const imports = ["ClientConfig", this.getClientName({ endpoint })];

// Add request struct import if this endpoint uses an inlined request
if (endpoint.request.type === "inlined") {
const requestStructName = this.context.getStructName(endpoint.request.declaration.name);
imports.push(requestStructName);
}

return [
new rust.UseStatement({
path: this.context.getPackageName(),
items: ["ClientConfig", this.getClientName({ endpoint })]
items: imports
})
];

return imports;
}

private getClientConfigStruct({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export class DynamicTypeInstantiationMapper {
return this.convertLiteral({ literal: args.typeReference.value, value: args.value });
case "unknown":
return this.convertUnknown({ value: args.value });
case "named":
return this.convertNamed({ typeReference: args.typeReference, value: args.value });
case "optional":
case "nullable":
return this.convertOptional({ typeReference: args.typeReference, value: args.value });
case "list":
return this.convertList({ typeReference: args.typeReference, value: args.value });
default:
return rust.Expression.raw('todo!("Unhandled type reference")');
}
Expand Down Expand Up @@ -102,9 +109,61 @@ export class DynamicTypeInstantiationMapper {

if (typeof value === "object") {
// Use serde_json for complex objects
return rust.Expression.raw(`serde_json::json!(${JSON.stringify(JSON.stringify(value))})`);
return rust.Expression.raw(`serde_json::json!(${JSON.stringify(value)})`);
}

return rust.Expression.stringLiteral("");
}

private convertNamed({
typeReference,
value
}: {
typeReference: FernIr.dynamic.TypeReference;
value: unknown;
}): rust.Expression {
// For now, use JSON for named types - this removes the todo!() error
// TODO: Implement proper struct instantiation in the future
if (typeof value === "object" && value != null) {
return rust.Expression.raw(`serde_json::json!(${JSON.stringify(value)})`);
}

return rust.Expression.stringLiteral(String(value));
}

private convertOptional({
typeReference,
value
}: {
typeReference: FernIr.dynamic.TypeReference;
value: unknown;
}): rust.Expression {
if (value == null) {
return rust.Expression.none();
}
// For optional/nullable, use the inner type's value structure
const innerTypeRef =
(typeReference as FernIr.dynamic.TypeReference.Optional | FernIr.dynamic.TypeReference.Nullable).value ||
({ type: "unknown" } as FernIr.dynamic.TypeReference);
const innerValue = this.convert({ typeReference: innerTypeRef, value });
return rust.Expression.functionCall("Some", [innerValue]);
}

private convertList({
typeReference,
value
}: {
typeReference: FernIr.dynamic.TypeReference;
value: unknown;
}): rust.Expression {
if (!Array.isArray(value)) {
return rust.Expression.vec([]);
}
// For lists, use the inner type's value structure
const innerTypeRef =
(typeReference as FernIr.dynamic.TypeReference.List).value ||
({ type: "unknown" } as FernIr.dynamic.TypeReference);
const elements = value.map((item) => this.convert({ typeReference: innerTypeRef, value: item }));
return rust.Expression.vec(elements);
}
}
68 changes: 68 additions & 0 deletions generators/rust/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { generateModels } from "@fern-api/rust-model";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { Endpoint } from "@fern-fern/generator-exec-sdk/api";
import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";
import { EnvironmentGenerator } from "./environment/EnvironmentGenerator";
import { ErrorGenerator } from "./error/ErrorGenerator";
import { ClientConfigGenerator } from "./generators/ClientConfigGenerator";
Expand Down Expand Up @@ -70,6 +72,10 @@ export class SdkGeneratorCli extends AbstractRustGeneratorCli<SdkCustomConfigSch
await context.project.persist();
context.logger.info("=== PERSIST COMPLETE ===");

// Generate dynamic-snippets directory with individual .rs files
await this.generateDynamicSnippetFiles(context);
context.logger.info("=== dynamic-snippets COMPLETE ===");

context.logger.info("=== RUNNING rustfmt ===");
await formatRustCode({
outputDir: context.project.absolutePathToOutputDirectory,
Expand Down Expand Up @@ -397,6 +403,68 @@ export class SdkGeneratorCli extends AbstractRustGeneratorCli<SdkCustomConfigSch
}
}

// ===========================
// DYNAMIC SNIPPETS GENERATION
// ===========================

private async generateDynamicSnippetFiles(context: SdkGeneratorContext): Promise<void> {
context.logger.info("=== GENERATING DYNAMIC SNIPPETS ===");

// Check if we have output directory configured
if (!context.config.output.path) {
context.logger.info("No output path configured, skipping dynamic snippets");
return;
}

// Check if we have dynamic IR
const dynamicIr = context.ir.dynamic;
if (dynamicIr == null) {
context.logger.info("No dynamic IR available, skipping dynamic snippets");
return;
}

// Create dynamic snippets generator
const dynamicSnippetsGenerator = new DynamicSnippetsGenerator({
ir: convertIr(dynamicIr),
config: context.config
});

let exampleIndex = 0;

// Process each endpoint from dynamic IR
for (const [endpointId, endpoint] of Object.entries(dynamicIr.endpoints)) {
// Get examples for this endpoint
const examples = endpoint.examples ?? [];

for (const example of examples) {
try {
// Convert example to dynamic snippet request
const snippetRequest = convertDynamicEndpointSnippetRequest(example);

// Generate the snippet
const snippetResponse = await dynamicSnippetsGenerator.generate(snippetRequest);

if (snippetResponse.snippet) {
// Create dynamic-snippets directory if it doesn't exist
const dynamicSnippetsDir = join(context.config.output.path, "dynamic-snippets");
await mkdir(dynamicSnippetsDir, { recursive: true });

// Write the Rust snippet file directly
const snippetPath = join(dynamicSnippetsDir, `example${exampleIndex}.rs`);
await writeFile(snippetPath, snippetResponse.snippet);

context.logger.info(`Generated dynamic snippet: ${snippetPath}`);
exampleIndex++;
}
} catch (error) {
context.logger.warn(`Failed to generate dynamic snippet for ${endpointId}: ${error}`);
}
}
}

context.logger.info(`=== DYNAMIC SNIPPETS COMPLETE (${exampleIndex} files) ===`);
}

// ===========================
// UTILITY METHODS
// ===========================
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions seed/rust-sdk/accept-header/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions seed/rust-sdk/accept-header/dynamic-snippets/example0.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions seed/rust-sdk/accept-header/dynamic-snippets/example1.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion seed/rust-sdk/alias-extends/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading