Skip to content

Commit 9feb892

Browse files
feat(ruby): Adding reference.md generation to ruby-v2 generator (#9659)
* Adding reference.md generation to ruby-v2 * Adding example reference.md file * Linting * Typo fix in custom test file * Updating seed * Linting * Automated update of seed files --------- Co-authored-by: Chris Mallinson <[email protected]> Co-authored-by: chanks <[email protected]>
1 parent 5e482b6 commit 9feb892

File tree

185 files changed

+18975
-91
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

185 files changed

+18975
-91
lines changed

generators/ruby-v2/base/src/project/RubyProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ class CustomTestFile {
398398
399399
# This test is run via command line: rake customtest
400400
describe "Custom Test" do
401-
it "Defalt" do
401+
it "Default" do
402402
refute false
403403
end
404404
end

generators/ruby-v2/sdk/src/SdkGeneratorCli.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
88
import { Endpoint } from "@fern-fern/generator-exec-sdk/api";
99
import { HttpService, IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
1010
import { SingleUrlEnvironmentGenerator } from "./environment/SingleUrlEnvironmentGenerator";
11+
import { buildReference } from "./reference/buildReference";
1112
import { RootClientGenerator } from "./root-client/RootClientGenerator";
1213
import { SdkCustomConfigSchema } from "./SdkCustomConfig";
1314
import { SdkGeneratorContext } from "./SdkGeneratorContext";
@@ -112,6 +113,16 @@ export class SdkGeneratorCLI extends AbstractRubyGeneratorCli<SdkCustomConfigSch
112113
}
113114
}
114115

116+
try {
117+
await this.generateReference({ context });
118+
} catch (error) {
119+
context.logger.warn("Failed to generate reference.md, this is OK.");
120+
if (error instanceof Error) {
121+
context.logger.warn((error as Error)?.message);
122+
context.logger.warn((error as Error)?.stack ?? "");
123+
}
124+
}
125+
115126
await context.project.persist();
116127

117128
try {
@@ -209,4 +220,13 @@ export class SdkGeneratorCLI extends AbstractRubyGeneratorCli<SdkCustomConfigSch
209220

210221
return endpointSnippets;
211222
}
223+
224+
private async generateReference({ context }: { context: SdkGeneratorContext }): Promise<void> {
225+
const builder = buildReference({ context });
226+
const content = await context.generatorAgent.generateReference(builder);
227+
228+
context.project.addRawFiles(
229+
new File(context.generatorAgent.REFERENCE_FILENAME, RelativeFilePath.of("."), content)
230+
);
231+
}
212232
}

generators/ruby-v2/sdk/src/SdkGeneratorContext.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { GeneratorNotificationService } from "@fern-api/base-generator";
2+
import { assertNever } from "@fern-api/core-utils";
23
import { join, RelativeFilePath } from "@fern-api/path-utils";
34
import { ruby } from "@fern-api/ruby-ast";
45
import { ClassReference } from "@fern-api/ruby-ast/src/ast/ClassReference";
56
import { AbstractRubyGeneratorContext, AsIsFiles, RubyProject } from "@fern-api/ruby-base";
67
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
78
import {
9+
ExampleEndpointCall,
10+
HttpEndpoint,
811
HttpService,
912
IntermediateRepresentation,
1013
Name,
1114
ServiceId,
15+
StreamingResponse,
1216
Subpackage,
1317
SubpackageId,
1418
TypeDeclaration,
@@ -98,6 +102,82 @@ export class SdkGeneratorContext extends AbstractRubyGeneratorContext<SdkCustomC
98102
);
99103
}
100104

105+
public maybeGetExampleEndpointCall(endpoint: HttpEndpoint): ExampleEndpointCall | null {
106+
// Try user-specified examples first
107+
if (endpoint.userSpecifiedExamples.length > 0) {
108+
const exampleEndpointCall = endpoint.userSpecifiedExamples[0]?.example;
109+
if (exampleEndpointCall != null) {
110+
return exampleEndpointCall;
111+
}
112+
}
113+
// Fall back to auto-generated examples
114+
const exampleEndpointCall = endpoint.autogeneratedExamples[0]?.example;
115+
116+
return exampleEndpointCall ?? null;
117+
}
118+
119+
public getReturnTypeForEndpoint(httpEndpoint: HttpEndpoint): ruby.Type {
120+
const responseBody = httpEndpoint.response?.body;
121+
122+
if (responseBody == null) {
123+
return ruby.Type.void();
124+
}
125+
126+
switch (responseBody.type) {
127+
case "json":
128+
return this.typeMapper.convert({ reference: responseBody.value.responseBodyType });
129+
case "fileDownload":
130+
case "text":
131+
return ruby.Type.string();
132+
case "bytes":
133+
throw new Error("Returning bytes is not supported");
134+
case "streaming":
135+
case "streamParameter": {
136+
const streamingResponse = this.getStreamingResponse(httpEndpoint);
137+
if (!streamingResponse) {
138+
throw new Error(
139+
`Unable to parse streaming response for endpoint ${httpEndpoint.name.camelCase.safeName}`
140+
);
141+
}
142+
return this.getStreamPayload(streamingResponse);
143+
}
144+
default:
145+
assertNever(responseBody);
146+
}
147+
}
148+
149+
public getStreamingResponse(endpoint: HttpEndpoint): StreamingResponse | undefined {
150+
const responseBody = endpoint.response?.body;
151+
if (responseBody == null) {
152+
return undefined;
153+
}
154+
switch (responseBody.type) {
155+
case "streaming":
156+
return responseBody.value;
157+
case "streamParameter":
158+
return responseBody.streamResponse;
159+
case "fileDownload":
160+
case "json":
161+
case "text":
162+
case "bytes":
163+
return undefined;
164+
default:
165+
assertNever(responseBody);
166+
}
167+
}
168+
169+
public getStreamPayload(streamingResponse: StreamingResponse): ruby.Type {
170+
switch (streamingResponse.type) {
171+
case "json":
172+
case "sse":
173+
return this.typeMapper.convert({ reference: streamingResponse.payload });
174+
case "text":
175+
return ruby.Type.string();
176+
default:
177+
assertNever(streamingResponse);
178+
}
179+
}
180+
101181
public getSubpackageOrThrow(subpackageId: SubpackageId): Subpackage {
102182
const subpackage = this.ir.subpackages[subpackageId];
103183
if (subpackage == null) {
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { ReferenceConfigBuilder } from "@fern-api/base-generator";
2+
import { ruby } from "@fern-api/ruby-ast";
3+
4+
import { FernGeneratorCli } from "@fern-fern/generator-cli-sdk";
5+
import { HttpEndpoint, HttpService, ServiceId, TypeReference } from "@fern-fern/ir-sdk/api";
6+
7+
import { SdkGeneratorContext } from "../SdkGeneratorContext";
8+
import { SingleEndpointSnippet } from "./EndpointSnippetsGenerator";
9+
10+
export function buildReference({ context }: { context: SdkGeneratorContext }): ReferenceConfigBuilder {
11+
const builder = new ReferenceConfigBuilder();
12+
const serviceEntries = Object.entries(context.ir.services);
13+
14+
serviceEntries.forEach(([serviceId, service]) => {
15+
const section = isRootServiceId({ context, serviceId })
16+
? builder.addRootSection()
17+
: builder.addSection({ title: getSectionTitle({ service }) });
18+
const endpoints = getEndpointReferencesForService({ context, serviceId, service });
19+
for (const endpoint of endpoints) {
20+
section.addEndpoint(endpoint);
21+
}
22+
});
23+
24+
return builder;
25+
}
26+
27+
function getEndpointReferencesForService({
28+
context,
29+
serviceId,
30+
service
31+
}: {
32+
context: SdkGeneratorContext;
33+
serviceId: ServiceId;
34+
service: HttpService;
35+
}): FernGeneratorCli.EndpointReference[] {
36+
return service.endpoints
37+
.map((endpoint) => {
38+
let singleEndpointSnippet;
39+
const exampleCall = context.maybeGetExampleEndpointCall(endpoint);
40+
if (exampleCall) {
41+
singleEndpointSnippet = context.snippetGenerator.getSingleEndpointSnippet({
42+
endpoint,
43+
example: exampleCall
44+
});
45+
}
46+
if (!singleEndpointSnippet) {
47+
return undefined;
48+
}
49+
return getEndpointReference({
50+
context,
51+
serviceId,
52+
service,
53+
endpoint,
54+
singleEndpointSnippet
55+
});
56+
})
57+
.filter((endpoint): endpoint is FernGeneratorCli.EndpointReference => !!endpoint);
58+
}
59+
60+
function getEndpointReference({
61+
context,
62+
serviceId,
63+
service,
64+
endpoint,
65+
singleEndpointSnippet
66+
}: {
67+
context: SdkGeneratorContext;
68+
serviceId: ServiceId;
69+
service: HttpService;
70+
endpoint: HttpEndpoint;
71+
singleEndpointSnippet: SingleEndpointSnippet;
72+
}): FernGeneratorCli.EndpointReference {
73+
const returnValue = getReturnValue({ context, endpoint });
74+
return {
75+
title: {
76+
snippetParts: [
77+
{
78+
text: getAccessFromRootClient({ context, service }) + "."
79+
},
80+
{
81+
text: getEndpointMethodName({ endpoint })
82+
},
83+
{
84+
text: getReferenceEndpointInvocationParameters({ context, endpoint })
85+
}
86+
],
87+
returnValue
88+
},
89+
description: endpoint.docs,
90+
snippet: singleEndpointSnippet.endpointCall.trim(),
91+
parameters: getEndpointParameters({ context, endpoint })
92+
};
93+
}
94+
95+
function getAccessFromRootClient({ context, service }: { context: SdkGeneratorContext; service: HttpService }): string {
96+
const clientVariableName = "client";
97+
const servicePath = service.name.fernFilepath.allParts.map((part) => part.pascalCase.safeName);
98+
return servicePath.length > 0 ? `${clientVariableName}.${servicePath.join(".")}` : clientVariableName;
99+
}
100+
101+
function getEndpointMethodName({ endpoint }: { endpoint: HttpEndpoint }): string {
102+
return endpoint.name.pascalCase.safeName;
103+
}
104+
105+
function getReferenceEndpointInvocationParameters({
106+
context,
107+
endpoint
108+
}: {
109+
context: SdkGeneratorContext;
110+
endpoint: HttpEndpoint;
111+
}): string {
112+
const parameters: string[] = [];
113+
114+
endpoint.allPathParameters.forEach((pathParam) => {
115+
parameters.push(pathParam.name.pascalCase.safeName);
116+
});
117+
118+
if (endpoint.requestBody != null) {
119+
switch (endpoint.requestBody.type) {
120+
case "inlinedRequestBody":
121+
parameters.push("request");
122+
break;
123+
case "reference":
124+
parameters.push("request");
125+
break;
126+
case "fileUpload":
127+
parameters.push("request");
128+
break;
129+
case "bytes":
130+
parameters.push("request");
131+
break;
132+
}
133+
}
134+
135+
return `(${parameters.join(", ")})`;
136+
}
137+
138+
function getReturnValue({
139+
context,
140+
endpoint
141+
}: {
142+
context: SdkGeneratorContext;
143+
endpoint: HttpEndpoint;
144+
}): { text: string } | undefined {
145+
const returnType = context.getReturnTypeForEndpoint(endpoint);
146+
const returnTypeString = getSimpleTypeName(returnType, context);
147+
return { text: returnTypeString };
148+
}
149+
150+
function getRubyTypeString({
151+
context,
152+
typeReference
153+
}: {
154+
context: SdkGeneratorContext;
155+
typeReference: TypeReference;
156+
}): string {
157+
const rubyType = context.typeMapper.convert({ reference: typeReference });
158+
return getSimpleTypeName(rubyType, context);
159+
}
160+
161+
function getSimpleTypeName(rubyType: ruby.Type, context: SdkGeneratorContext): string {
162+
const simpleWriter = new ruby.Writer({
163+
customConfig: context.customConfig
164+
});
165+
166+
rubyType.write(simpleWriter);
167+
168+
const typeName = simpleWriter.buffer.trim();
169+
170+
// TODO: anything else??
171+
172+
return typeName;
173+
}
174+
175+
function getEndpointParameters({
176+
context,
177+
endpoint
178+
}: {
179+
context: SdkGeneratorContext;
180+
endpoint: HttpEndpoint;
181+
}): FernGeneratorCli.ParameterReference[] {
182+
const parameters: FernGeneratorCli.ParameterReference[] = [];
183+
184+
endpoint.allPathParameters.forEach((pathParam) => {
185+
parameters.push({
186+
name: pathParam.name.camelCase.safeName,
187+
type: getRubyTypeString({ context, typeReference: pathParam.valueType }),
188+
description: pathParam.docs,
189+
required: true
190+
});
191+
});
192+
193+
endpoint.queryParameters.forEach((queryParam) => {
194+
parameters.push({
195+
name: queryParam.name.name.camelCase.safeName,
196+
type: getRubyTypeString({ context, typeReference: queryParam.valueType }),
197+
description: queryParam.docs,
198+
required: !queryParam.allowMultiple
199+
});
200+
});
201+
202+
endpoint.headers.forEach((header) => {
203+
parameters.push({
204+
name: header.name.name.camelCase.safeName,
205+
type: getRubyTypeString({ context, typeReference: header.valueType }),
206+
description: header.docs,
207+
required: true
208+
});
209+
});
210+
211+
if (endpoint.requestBody != null && endpoint.requestBody.type === "inlinedRequestBody") {
212+
endpoint.requestBody.properties.forEach((property) => {
213+
parameters.push({
214+
name: property.name.name.camelCase.safeName,
215+
type: getRubyTypeString({ context, typeReference: property.valueType }),
216+
description: property.docs,
217+
required: true
218+
});
219+
});
220+
} else if (endpoint.requestBody != null && endpoint.requestBody.type === "reference") {
221+
parameters.push({
222+
name: "request",
223+
type: getRubyTypeString({ context, typeReference: endpoint.requestBody.requestBodyType }),
224+
description: endpoint.requestBody.docs,
225+
required: true
226+
});
227+
}
228+
229+
return parameters;
230+
}
231+
232+
function isRootServiceId({ context, serviceId }: { context: SdkGeneratorContext; serviceId: ServiceId }): boolean {
233+
return context.ir.rootPackage.service === serviceId;
234+
}
235+
236+
function getSectionTitle({ service }: { service: HttpService }): string {
237+
return service.displayName ?? service.name.fernFilepath.allParts.map((part) => part.pascalCase.safeName).join(" ");
238+
}

0 commit comments

Comments
 (0)