Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generators/ruby-v2/base/src/project/RubyProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ class CustomTestFile {

# This test is run via command line: rake customtest
describe "Custom Test" do
it "Defalt" do
it "Default" do
refute false
end
end
Expand Down
20 changes: 20 additions & 0 deletions generators/ruby-v2/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { Endpoint } from "@fern-fern/generator-exec-sdk/api";
import { HttpService, IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
import { SingleUrlEnvironmentGenerator } from "./environment/SingleUrlEnvironmentGenerator";
import { buildReference } from "./reference/buildReference";
import { RootClientGenerator } from "./root-client/RootClientGenerator";
import { SdkCustomConfigSchema } from "./SdkCustomConfig";
import { SdkGeneratorContext } from "./SdkGeneratorContext";
Expand Down Expand Up @@ -112,6 +113,16 @@ export class SdkGeneratorCLI extends AbstractRubyGeneratorCli<SdkCustomConfigSch
}
}

try {
await this.generateReference({ context });
} catch (error) {
context.logger.warn("Failed to generate reference.md, this is OK.");
if (error instanceof Error) {
context.logger.warn((error as Error)?.message);
context.logger.warn((error as Error)?.stack ?? "");
}
}

await context.project.persist();

try {
Expand Down Expand Up @@ -209,4 +220,13 @@ export class SdkGeneratorCLI extends AbstractRubyGeneratorCli<SdkCustomConfigSch

return endpointSnippets;
}

private async generateReference({ context }: { context: SdkGeneratorContext }): Promise<void> {
const builder = buildReference({ context });
const content = await context.generatorAgent.generateReference(builder);

context.project.addRawFiles(
new File(context.generatorAgent.REFERENCE_FILENAME, RelativeFilePath.of("."), content)
);
}
}
80 changes: 80 additions & 0 deletions generators/ruby-v2/sdk/src/SdkGeneratorContext.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { GeneratorNotificationService } from "@fern-api/base-generator";
import { assertNever } from "@fern-api/core-utils";
import { join, RelativeFilePath } from "@fern-api/path-utils";
import { ruby } from "@fern-api/ruby-ast";
import { ClassReference } from "@fern-api/ruby-ast/src/ast/ClassReference";
import { AbstractRubyGeneratorContext, AsIsFiles, RubyProject } from "@fern-api/ruby-base";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import {
ExampleEndpointCall,
HttpEndpoint,
HttpService,
IntermediateRepresentation,
Name,
ServiceId,
StreamingResponse,
Subpackage,
SubpackageId,
TypeDeclaration,
Expand Down Expand Up @@ -98,6 +102,82 @@ export class SdkGeneratorContext extends AbstractRubyGeneratorContext<SdkCustomC
);
}

public maybeGetExampleEndpointCall(endpoint: HttpEndpoint): ExampleEndpointCall | null {
// Try user-specified examples first
if (endpoint.userSpecifiedExamples.length > 0) {
const exampleEndpointCall = endpoint.userSpecifiedExamples[0]?.example;
if (exampleEndpointCall != null) {
return exampleEndpointCall;
}
}
// Fall back to auto-generated examples
const exampleEndpointCall = endpoint.autogeneratedExamples[0]?.example;

return exampleEndpointCall ?? null;
}

public getReturnTypeForEndpoint(httpEndpoint: HttpEndpoint): ruby.Type {
const responseBody = httpEndpoint.response?.body;

if (responseBody == null) {
return ruby.Type.void();
}

switch (responseBody.type) {
case "json":
return this.typeMapper.convert({ reference: responseBody.value.responseBodyType });
case "fileDownload":
case "text":
return ruby.Type.string();
case "bytes":
throw new Error("Returning bytes is not supported");
case "streaming":
case "streamParameter": {
const streamingResponse = this.getStreamingResponse(httpEndpoint);
if (!streamingResponse) {
throw new Error(
`Unable to parse streaming response for endpoint ${httpEndpoint.name.camelCase.safeName}`
);
}
return this.getStreamPayload(streamingResponse);
}
default:
assertNever(responseBody);
}
}

public getStreamingResponse(endpoint: HttpEndpoint): StreamingResponse | undefined {
const responseBody = endpoint.response?.body;
if (responseBody == null) {
return undefined;
}
switch (responseBody.type) {
case "streaming":
return responseBody.value;
case "streamParameter":
return responseBody.streamResponse;
case "fileDownload":
case "json":
case "text":
case "bytes":
return undefined;
default:
assertNever(responseBody);
}
}

public getStreamPayload(streamingResponse: StreamingResponse): ruby.Type {
switch (streamingResponse.type) {
case "json":
case "sse":
return this.typeMapper.convert({ reference: streamingResponse.payload });
case "text":
return ruby.Type.string();
default:
assertNever(streamingResponse);
}
}

public getSubpackageOrThrow(subpackageId: SubpackageId): Subpackage {
const subpackage = this.ir.subpackages[subpackageId];
if (subpackage == null) {
Expand Down
238 changes: 238 additions & 0 deletions generators/ruby-v2/sdk/src/reference/buildReference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { ReferenceConfigBuilder } from "@fern-api/base-generator";
import { ruby } from "@fern-api/ruby-ast";

import { FernGeneratorCli } from "@fern-fern/generator-cli-sdk";
import { HttpEndpoint, HttpService, ServiceId, TypeReference } from "@fern-fern/ir-sdk/api";

import { SdkGeneratorContext } from "../SdkGeneratorContext";
import { SingleEndpointSnippet } from "./EndpointSnippetsGenerator";

export function buildReference({ context }: { context: SdkGeneratorContext }): ReferenceConfigBuilder {
const builder = new ReferenceConfigBuilder();
const serviceEntries = Object.entries(context.ir.services);

serviceEntries.forEach(([serviceId, service]) => {
const section = isRootServiceId({ context, serviceId })
? builder.addRootSection()
: builder.addSection({ title: getSectionTitle({ service }) });
const endpoints = getEndpointReferencesForService({ context, serviceId, service });
for (const endpoint of endpoints) {
section.addEndpoint(endpoint);
}
});

return builder;
}

function getEndpointReferencesForService({
context,
serviceId,
service
}: {
context: SdkGeneratorContext;
serviceId: ServiceId;
service: HttpService;
}): FernGeneratorCli.EndpointReference[] {
return service.endpoints
.map((endpoint) => {
let singleEndpointSnippet;
const exampleCall = context.maybeGetExampleEndpointCall(endpoint);
if (exampleCall) {
singleEndpointSnippet = context.snippetGenerator.getSingleEndpointSnippet({
endpoint,
example: exampleCall
});
}
if (!singleEndpointSnippet) {
return undefined;
}
return getEndpointReference({
context,
serviceId,
service,
endpoint,
singleEndpointSnippet
});
})
.filter((endpoint): endpoint is FernGeneratorCli.EndpointReference => !!endpoint);
}

function getEndpointReference({
context,
serviceId,
service,
endpoint,
singleEndpointSnippet
}: {
context: SdkGeneratorContext;
serviceId: ServiceId;
service: HttpService;
endpoint: HttpEndpoint;
singleEndpointSnippet: SingleEndpointSnippet;
}): FernGeneratorCli.EndpointReference {
const returnValue = getReturnValue({ context, endpoint });
return {
title: {
snippetParts: [
{
text: getAccessFromRootClient({ context, service }) + "."
},
{
text: getEndpointMethodName({ endpoint })
},
{
text: getReferenceEndpointInvocationParameters({ context, endpoint })
}
],
returnValue
},
description: endpoint.docs,
snippet: singleEndpointSnippet.endpointCall.trim(),
parameters: getEndpointParameters({ context, endpoint })
};
}

function getAccessFromRootClient({ context, service }: { context: SdkGeneratorContext; service: HttpService }): string {
const clientVariableName = "client";
const servicePath = service.name.fernFilepath.allParts.map((part) => part.pascalCase.safeName);
return servicePath.length > 0 ? `${clientVariableName}.${servicePath.join(".")}` : clientVariableName;
}

function getEndpointMethodName({ endpoint }: { endpoint: HttpEndpoint }): string {
return endpoint.name.pascalCase.safeName;
}

function getReferenceEndpointInvocationParameters({
context,
endpoint
}: {
context: SdkGeneratorContext;
endpoint: HttpEndpoint;
}): string {
const parameters: string[] = [];

endpoint.allPathParameters.forEach((pathParam) => {
parameters.push(pathParam.name.pascalCase.safeName);
});

if (endpoint.requestBody != null) {
switch (endpoint.requestBody.type) {
case "inlinedRequestBody":
parameters.push("request");
break;
case "reference":
parameters.push("request");
break;
case "fileUpload":
parameters.push("request");
break;
case "bytes":
parameters.push("request");
break;
}
}

return `(${parameters.join(", ")})`;
}

function getReturnValue({
context,
endpoint
}: {
context: SdkGeneratorContext;
endpoint: HttpEndpoint;
}): { text: string } | undefined {
const returnType = context.getReturnTypeForEndpoint(endpoint);
const returnTypeString = getSimpleTypeName(returnType, context);
return { text: returnTypeString };
}

function getRubyTypeString({
context,
typeReference
}: {
context: SdkGeneratorContext;
typeReference: TypeReference;
}): string {
const rubyType = context.typeMapper.convert({ reference: typeReference });
return getSimpleTypeName(rubyType, context);
}

function getSimpleTypeName(rubyType: ruby.Type, context: SdkGeneratorContext): string {
const simpleWriter = new ruby.Writer({
customConfig: context.customConfig
});

rubyType.write(simpleWriter);

const typeName = simpleWriter.buffer.trim();

// TODO: anything else??

return typeName;
}

function getEndpointParameters({
context,
endpoint
}: {
context: SdkGeneratorContext;
endpoint: HttpEndpoint;
}): FernGeneratorCli.ParameterReference[] {
const parameters: FernGeneratorCli.ParameterReference[] = [];

endpoint.allPathParameters.forEach((pathParam) => {
parameters.push({
name: pathParam.name.camelCase.safeName,
type: getRubyTypeString({ context, typeReference: pathParam.valueType }),
description: pathParam.docs,
required: true
});
});

endpoint.queryParameters.forEach((queryParam) => {
parameters.push({
name: queryParam.name.name.camelCase.safeName,
type: getRubyTypeString({ context, typeReference: queryParam.valueType }),
description: queryParam.docs,
required: !queryParam.allowMultiple
});
});

endpoint.headers.forEach((header) => {
parameters.push({
name: header.name.name.camelCase.safeName,
type: getRubyTypeString({ context, typeReference: header.valueType }),
description: header.docs,
required: true
});
});

if (endpoint.requestBody != null && endpoint.requestBody.type === "inlinedRequestBody") {
endpoint.requestBody.properties.forEach((property) => {
parameters.push({
name: property.name.name.camelCase.safeName,
type: getRubyTypeString({ context, typeReference: property.valueType }),
description: property.docs,
required: true
});
});
} else if (endpoint.requestBody != null && endpoint.requestBody.type === "reference") {
parameters.push({
name: "request",
type: getRubyTypeString({ context, typeReference: endpoint.requestBody.requestBodyType }),
description: endpoint.requestBody.docs,
required: true
});
}

return parameters;
}

function isRootServiceId({ context, serviceId }: { context: SdkGeneratorContext; serviceId: ServiceId }): boolean {
return context.ir.rootPackage.service === serviceId;
}

function getSectionTitle({ service }: { service: HttpService }): string {
return service.displayName ?? service.name.fernFilepath.allParts.map((part) => part.pascalCase.safeName).join(" ");
}
Loading
Loading