-
Notifications
You must be signed in to change notification settings - Fork 336
feat(csharp): generate invocation-only dynamic snippets #17405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devin/1786644600-invocation-only-dynamic-snippets
Are you sure you want to change the base?
Changes from all commits
cad1246
1355d36
15ea033
437292d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { FernIr } from "@fern-api/dynamic-ir-sdk"; | ||
|
|
||
| /** | ||
| * The structured result of an invocation-only snippet. | ||
| * | ||
| * Unlike {@link FernIr.dynamic.EndpointSnippetResponse}, which returns a single fully-formed | ||
| * snippet string, this exposes the individual pieces a docs template needs to render (and | ||
| * keep in sync) an invocation on its own: the bare call, the imports the call requires, and | ||
| * the generated client class/type name. | ||
| */ | ||
| export interface InvocationSnippetResponse { | ||
| /** | ||
| * The bare invocation/call (e.g. `client.plants.update(...)`) with no imports, no client | ||
| * instantiation, and no trailing statement terminator. Honors `options.clientVariableName`. | ||
| */ | ||
| snippet: string; | ||
| /** | ||
| * The import block the call requires (e.g. an SDK namespace import for a branded string | ||
| * alias). Empty string when the call references no imports. | ||
| */ | ||
| imports: string; | ||
| /** | ||
| * The generated client class/type name (e.g. `AcmeClient`), so docs can render | ||
| * `new {{clientName}}(...)` and track renames of the SDK client. | ||
| */ | ||
| clientName: string; | ||
| errors: FernIr.dynamic.Error_[] | undefined; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,11 @@ | ||
| import { NamedArgument, Options, Scope, Severity, Style } from "@fern-api/browser-compatible-base-generator"; | ||
| import { | ||
| InvocationSnippetResponse, | ||
| NamedArgument, | ||
| Options, | ||
| Scope, | ||
| Severity, | ||
| Style | ||
| } from "@fern-api/browser-compatible-base-generator"; | ||
| import { assertNever } from "@fern-api/core-utils"; | ||
| import { ast, is, WithGeneration } from "@fern-api/csharp-codegen"; | ||
| import { FernIr } from "@fern-api/dynamic-ir-sdk"; | ||
|
|
@@ -12,6 +19,9 @@ import { FilePropertyInfo } from "./context/FilePropertyMapper.js"; | |
| // DEFAULT_REQUEST_PARAMETER_NAME in convertHttpSdkRequest.ts). | ||
| const REQUEST_PARAMETER_NAME = "request"; | ||
|
|
||
| // The generated full snippet constructs and invokes the client through a local named `client`. | ||
| const CLIENT_VAR_NAME = "client"; | ||
|
|
||
| export class EndpointSnippetGenerator extends WithGeneration { | ||
| private context: DynamicSnippetsGeneratorContext; | ||
|
|
||
|
|
@@ -75,6 +85,52 @@ export class EndpointSnippetGenerator extends WithGeneration { | |
| throw new Error("Unsupported"); | ||
| } | ||
|
|
||
| /** | ||
| * Generates the structured pieces of an endpoint invocation for callers that render the | ||
| * invocation within code of their own (e.g. a documentation code template): the bare call | ||
| * (e.g. `client.Plants.Update(...)`), the `using ...;` block the call requires, and the | ||
| * generated client class name. | ||
| */ | ||
| public generateInvocationSnippetSync({ | ||
| endpoint, | ||
| request, | ||
| options | ||
| }: { | ||
| endpoint: FernIr.dynamic.Endpoint; | ||
| request: FernIr.dynamic.EndpointSnippetRequest; | ||
| options?: Options; | ||
| }): InvocationSnippetResponse { | ||
| const invocation = this.callMethod({ | ||
| endpoint, | ||
| snippet: request, | ||
| clientVariableName: options?.clientVariableName | ||
| }); | ||
| // The caller supplies the client and terminates the statement themselves, so the | ||
| // invocation is emitted as a bare expression: no `var client = new ...Client()` | ||
| // construction, no `Examples` class/method scaffold, and no trailing `;`. When the call | ||
| // references SDK or stdlib types (e.g. a body value constructed with `DateTime` or a | ||
| // `Guid`) the `using ...;` block it needs is surfaced separately so the caller can render | ||
| // it rather than falling back to the complete snippet. | ||
| const { code, imports } = invocation.toStringWithoutImports({ | ||
|
Comment on lines
+110
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Invocation-only snippets skip the check that the requested example belongs to the endpoint The invocation-only snippet path builds the call directly ( Missing example-id guard compared with the full-snippet builder
Because Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| namespace: "Examples", | ||
| generation: this.generation, | ||
| allNamespaceSegments: new Set(), | ||
| allTypeClassReferences: new Map(), | ||
|
Comment on lines
+115
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 warning The full-snippet path renders with the real context ( |
||
| // See generateSnippet for rationale: user-facing snippets skip the global:: qualifier. | ||
| skipGlobalQualifier: true | ||
| }); | ||
| return { | ||
| snippet: this.stripTrailingSemicolon(code.trim()), | ||
| imports, | ||
| clientName: this.Types.RootClientForSnippets.name, | ||
| errors: this.context.errors.empty() ? undefined : this.context.errors.toDynamicSnippetErrors() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 suggestion
|
||
| }; | ||
| } | ||
|
|
||
| private stripTrailingSemicolon(code: string): string { | ||
| return code.endsWith(";") ? code.slice(0, -1).trimEnd() : code; | ||
| } | ||
|
|
||
| private buildCodeBlock({ | ||
| endpoint, | ||
| snippet, | ||
|
|
@@ -152,17 +208,19 @@ export class EndpointSnippetGenerator extends WithGeneration { | |
|
|
||
| private callMethod({ | ||
| endpoint, | ||
| snippet | ||
| snippet, | ||
| clientVariableName | ||
| }: { | ||
| endpoint: FernIr.dynamic.Endpoint; | ||
| snippet: FernIr.dynamic.EndpointSnippetRequest; | ||
| clientVariableName?: string; | ||
| }): ast.CodeBlock | ast.MethodInvocation { | ||
| // if the example has *any* sample with stream set to true, then the method is an async enumerable | ||
| const isAsyncEnumerable = | ||
| endpoint.response?.type === "streaming" || endpoint.response?.type === "streamParameter"; | ||
|
|
||
| const invocation = this.csharp.invokeMethod({ | ||
| on: this.csharp.codeblock("client"), | ||
| on: this.csharp.codeblock(clientVariableName ?? CLIENT_VAR_NAME), | ||
| method: this.getMethod({ endpoint }), | ||
| arguments_: this.getMethodArgs({ endpoint, snippet }), | ||
| async: true, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 suggestion
formatSyncis being handed a bare expression fragment, not a compilation unit. Most C# formatters (csharpier) will throw or mangle on unparseable input. No caller passes a formatter today, so this is latent — either drop theformatterparam from this helper or document that callers must pass a formatter tolerant of fragments.