import { expect, it } from "@effect/vitest";import { Effect, Option, Schema } from "effect";import { buildToolTypeScriptPreview } from "@executor-js/sdk/core"; import { convertGoogleDiscoveryBundleToOpenApi, convertGoogleDiscoveryToOpenApi, isGoogleDiscoveryUrl, normalizeGoogleDiscoveryUrl,} from "./discovery";import { googleOAuthConsentScopesForPreset } from "./service-policy";import { extract, parse } from "@executor-js/plugin-openapi"; const ConvertedOperation = Schema.Struct({ operationId: Schema.String, "x-executor-toolPath": Schema.String, parameters: Schema.Array( Schema.Struct({ name: Schema.String, in: Schema.String, required: Schema.Boolean, description: Schema.optional(Schema.String), schema: Schema.Unknown, style: Schema.optional(Schema.String), explode: Schema.optional(Schema.Boolean), allowReserved: Schema.optional(Schema.Boolean), }), ), servers: Schema.optional(Schema.Array(Schema.Struct({ url: Schema.String }))), security: Schema.optional( Schema.Array(Schema.Record(Schema.String, Schema.Array(Schema.String))), ), "x-executor-pathTemplate": Schema.optional(Schema.String), requestBody: Schema.optional(Schema.Unknown), responses: Schema.Unknown, "x-google-scopes": Schema.Array(Schema.String),}); const ConvertedSpec = Schema.Struct({ openapi: Schema.String, servers: Schema.Array(Schema.Struct({ url: Schema.String })), paths: Schema.Record(Schema.String, Schema.Record(Schema.String, ConvertedOperation)), components: Schema.Struct({ schemas: Schema.Record(Schema.String, Schema.Unknown), }),}); const decodeConvertedSpec = Schema.decodeUnknownSync(Schema.fromJsonString(ConvertedSpec)); const OAUTH2_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest";const OAUTH2_USERINFO_SCOPES = [ "openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile",] as const; const oauth2DiscoveryDoc = { name: "oauth2", version: "v2", title: "Google OAuth2 API", rootUrl: "https://www.googleapis.com/", servicePath: "", auth: { oauth2: { scopes: { openid: { description: "Associate you with your personal info on Google", }, "https://www.googleapis.com/auth/userinfo.email": { description: "See your primary Google Account email address", }, "https://www.googleapis.com/auth/userinfo.profile": { description: "See your personal info", }, }, }, }, methods: { tokeninfo: { id: "oauth2.tokeninfo", httpMethod: "POST", path: "oauth2/v2/tokeninfo", parameters: { access_token: { location: "query", type: "string", }, }, response: { $ref: "Tokeninfo" }, }, }, resources: { userinfo: { methods: { get: { id: "oauth2.userinfo.get", httpMethod: "GET", path: "oauth2/v2/userinfo", scopes: OAUTH2_USERINFO_SCOPES, response: { $ref: "Userinfo" }, }, }, resources: { v2: { resources: { me: { methods: { get: { id: "oauth2.userinfo.v2.me.get", httpMethod: "GET", path: "userinfo/v2/me", scopes: OAUTH2_USERINFO_SCOPES, response: { $ref: "Userinfo" }, }, }, }, }, }, }, }, }, schemas: { Tokeninfo: { id: "Tokeninfo", type: "object", properties: { audience: { type: "string" }, scope: { type: "string" }, }, }, Userinfo: { id: "Userinfo", type: "object", properties: { email: { type: "string" }, family_name: { type: "string" }, gender: { type: "string" }, given_name: { type: "string" }, hd: { type: "string" }, id: { type: "string" }, link: { type: "string" }, locale: { type: "string" }, name: { type: "string" }, picture: { type: "string" }, verified_email: { type: "boolean" }, }, }, },}; it("accepts only supported HTTPS Google Discovery endpoints", () => { expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest/"), ).toBe("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"); expect( normalizeGoogleDiscoveryUrl( "https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest", ), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/forms/v1/rest"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/keep/v1/rest"), ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://chat.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://www.googleapis.com/discovery/v1/apis/chat/v1/rest"); expect( normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://forms.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://keep.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1"); expect(isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( true, ); expect(isGoogleDiscoveryUrl("https://evilgoogleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( false, ); expect(isGoogleDiscoveryUrl("http://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( false, ); expect( isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest?next=x"), ).toBe(false); expect( isGoogleDiscoveryUrl("https://token@www.googleapis.com/discovery/v1/apis/gmail/v1/rest"), ).toBe(false);}); const normalizeOpenApiRefsForPreview = (node: unknown): unknown => { if (node == null || typeof node !== "object") return node; if (Array.isArray(node)) return node.map(normalizeOpenApiRefsForPreview); const obj = node as Record<string, unknown>; if (typeof obj.$ref === "string") { const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/); return match ? { ...obj, $ref: `#/$defs/${match[1]}` } : obj; } return Object.fromEntries( Object.entries(obj).map(([key, value]) => [key, normalizeOpenApiRefsForPreview(value)]), );}; it.effect("converts Google Discovery documents into Executor-preserving OpenAPI 3 specs", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify({ name: "gmail", version: "v1", title: "Gmail API", rootUrl: "https://gmail.googleapis.com/", servicePath: "", auth: { oauth2: { scopes: { "https://www.googleapis.com/auth/gmail.metadata": { description: "Read metadata", }, }, }, }, resources: { users: { resources: { messages: { methods: { list: { id: "gmail.users.messages.list", httpMethod: "GET", path: "gmail/v1/users/{userId}/messages", scopes: ["https://www.googleapis.com/auth/gmail.metadata"], parameters: { userId: { location: "path", required: true, type: "string", description: "The user's email address. The special value me can be used.", }, metadataHeaders: { location: "query", repeated: true, type: "string", }, }, }, }, }, drafts: { methods: { create: { id: "gmail.users.drafts.create", httpMethod: "POST", path: "gmail/v1/users/{userId}/drafts", request: { $ref: "Draft" }, response: { $ref: "Draft" }, scopes: ["https://www.googleapis.com/auth/gmail.metadata"], parameters: { userId: { location: "path", required: true, type: "string", }, }, }, }, }, }, }, }, schemas: { Draft: { id: "Draft", type: "object", description: "A draft email.", properties: { id: { type: "string", description: "The immutable ID of the draft.", }, message: { $ref: "Message", }, }, }, Message: { id: "Message", type: "object", properties: { id: { type: "string", }, labelIds: { type: "array", items: { type: "string" }, }, }, }, }, }), }); const spec = decodeConvertedSpec(result.specText); const operation = spec.paths["/gmail/v1/users/{userId}/messages"]?.get; const createDraft = spec.paths["/gmail/v1/users/{userId}/drafts"]?.post; expect(spec.openapi).toBe("3.1.0"); expect(spec.servers).toEqual([{ url: "https://gmail.googleapis.com/" }]); expect(result.specText).not.toContain("_tag"); expect(operation).toMatchObject({ operationId: "users.messages.list", "x-executor-toolPath": "users.messages.list", "x-google-scopes": ["https://www.googleapis.com/auth/gmail.metadata"], }); expect(operation?.security).toEqual([ { googleOAuth2: ["https://www.googleapis.com/auth/gmail.metadata"] }, ]); expect(operation?.parameters).toContainEqual( expect.objectContaining({ name: "metadataHeaders", in: "query", style: "form", explode: true, }), ); expect(operation?.parameters).toContainEqual( expect.objectContaining({ name: "userId", description: "The user's email address. The special value me can be used.", schema: expect.objectContaining({ type: "string" }), }), ); expect(createDraft).toMatchObject({ operationId: "users.drafts.create", "x-executor-toolPath": "users.drafts.create", }); expect(createDraft).toMatchObject({ requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Draft" }, }, }, }, }); expect(createDraft?.parameters).toContainEqual( expect.objectContaining({ name: "userId", schema: expect.objectContaining({ type: "string" }), }), ); const parsed = yield* parse(result.specText); const extracted = yield* extract(parsed); const extractedDraftCreate = extracted.operations.find( (candidate) => candidate.operationId === "users.drafts.create", ); expect(extractedDraftCreate?.operationId).toBe("users.drafts.create"); const preview = yield* Effect.promise(() => buildToolTypeScriptPreview({ inputSchema: normalizeOpenApiRefsForPreview( extractedDraftCreate ? Option.getOrUndefined(extractedDraftCreate.inputSchema) : undefined, ), outputSchema: normalizeOpenApiRefsForPreview( extractedDraftCreate ? Option.getOrUndefined(extractedDraftCreate.outputSchema) : undefined, ), defs: new Map( Object.entries(spec.components.schemas).map(([name, schema]) => [ name, normalizeOpenApiRefsForPreview(schema), ]), ), }), ); expect(preview.inputTypeScript).toBe("{ userId: string; body?: Draft; }"); expect(preview.outputTypeScript).toBe("Draft"); expect(preview.typeScriptDefinitions).toMatchObject({ Draft: "{ id?: string; message?: Message; }", Message: "{ id?: string; labelIds?: string[]; }", }); // v2: the conversion now exposes a v2 oauth `authenticationTemplate` rather // than v1's `oauth2` source config. // removed: identityScopes assertion - that field belonged to the v1 // OAuth2SourceConfig slot model, which no longer exists in v2. const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2"); expect(oauthTemplate).toBeDefined(); }),); it.effect("converts Google OAuth2 v2 top-level and aliased userinfo methods", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: OAUTH2_URL, // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify(oauth2DiscoveryDoc), }); const spec = decodeConvertedSpec(result.specText); const userinfo = spec.paths["/oauth2/v2/userinfo"]?.get; const userinfoMe = spec.paths["/userinfo/v2/me"]?.get; const tokeninfo = spec.paths["/oauth2/v2/tokeninfo"]?.post; expect(userinfo).toMatchObject({ operationId: "userinfo.get", "x-executor-toolPath": "userinfo.get", "x-google-scopes": [...OAUTH2_USERINFO_SCOPES], }); expect(userinfo?.security).toEqual( OAUTH2_USERINFO_SCOPES.map((scope) => ({ googleOAuth2: [scope] })), ); expect(userinfo?.responses).toMatchObject({ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Userinfo" }, }, }, }, }); expect(userinfoMe).toMatchObject({ operationId: "userinfo.v2.me.get", "x-executor-toolPath": "userinfo.v2.me.get", "x-google-scopes": [...OAUTH2_USERINFO_SCOPES], }); expect(tokeninfo).toMatchObject({ operationId: "tokeninfo", "x-executor-toolPath": "tokeninfo", }); }),); it.effect("marks Google Discovery media-download methods as binary responses", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify({ name: "drive", version: "v3", title: "Drive API", rootUrl: "https://www.googleapis.com/", servicePath: "drive/v3/", resources: { files: { methods: { export: { id: "drive.files.export", httpMethod: "GET", path: "files/{fileId}/export", supportsMediaDownload: true, useMediaDownloadService: true, parameters: { fileId: { location: "path", required: true, type: "string", }, mimeType: { location: "query", required: true, type: "string", }, }, }, }, }, }, schemas: {}, }), }); const spec = decodeConvertedSpec(result.specText); const operation = spec.paths["/files/{fileId}/export"]?.get; expect(operation?.responses).toMatchObject({ "200": { content: { "application/octet-stream": { schema: { type: "string", format: "binary" }, }, }, }, }); const parsed = yield* parse(result.specText); const extracted = yield* extract(parsed); const exportOperation = extracted.operations.find( (candidate) => candidate.operationId === "files.export", ); expect(exportOperation?.operationId).toBe("files.export"); const responseFileHint = Option.flatMap( exportOperation?.responseBody ?? Option.none(),Lines 1–500 of 1671. Load more
import { expect, it } from "@effect/vitest"; import { Effect, Option, Schema } from "effect"; import { buildToolTypeScriptPreview } from "@executor-js/sdk/core"; import { convertGoogleDiscoveryBundleToOpenApi, convertGoogleDiscoveryToOpenApi, isGoogleDiscoveryUrl, normalizeGoogleDiscoveryUrl, } from "./discovery"; import { googleOAuthConsentScopesForPreset } from "./service-policy"; import { extract, parse } from "@executor-js/plugin-openapi"; const ConvertedOperation = Schema.Struct({ operationId: Schema.String, "x-executor-toolPath": Schema.String, parameters: Schema.Array( Schema.Struct({ name: Schema.String, in: Schema.String, required: Schema.Boolean, description: Schema.optional(Schema.String), schema: Schema.Unknown, style: Schema.optional(Schema.String), explode: Schema.optional(Schema.Boolean), allowReserved: Schema.optional(Schema.Boolean), }), ), servers: Schema.optional(Schema.Array(Schema.Struct({ url: Schema.String }))), security: Schema.optional( Schema.Array(Schema.Record(Schema.String, Schema.Array(Schema.String))), ), "x-executor-pathTemplate": Schema.optional(Schema.String), requestBody: Schema.optional(Schema.Unknown), responses: Schema.Unknown, "x-google-scopes": Schema.Array(Schema.String), }); const ConvertedSpec = Schema.Struct({ openapi: Schema.String, servers: Schema.Array(Schema.Struct({ url: Schema.String })), paths: Schema.Record(Schema.String, Schema.Record(Schema.String, ConvertedOperation)), components: Schema.Struct({ schemas: Schema.Record(Schema.String, Schema.Unknown), }), }); const decodeConvertedSpec = Schema.decodeUnknownSync(Schema.fromJsonString(ConvertedSpec)); const OAUTH2_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest"; const OAUTH2_USERINFO_SCOPES = [ "openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", ] as const; const oauth2DiscoveryDoc = { name: "oauth2", version: "v2", title: "Google OAuth2 API", rootUrl: "https://www.googleapis.com/", servicePath: "", auth: { oauth2: { scopes: { openid: { description: "Associate you with your personal info on Google", }, "https://www.googleapis.com/auth/userinfo.email": { description: "See your primary Google Account email address", }, "https://www.googleapis.com/auth/userinfo.profile": { description: "See your personal info", }, }, }, }, methods: { tokeninfo: { id: "oauth2.tokeninfo", httpMethod: "POST", path: "oauth2/v2/tokeninfo", parameters: { access_token: { location: "query", type: "string", }, }, response: { $ref: "Tokeninfo" }, }, }, resources: { userinfo: { methods: { get: { id: "oauth2.userinfo.get", httpMethod: "GET", path: "oauth2/v2/userinfo", scopes: OAUTH2_USERINFO_SCOPES, response: { $ref: "Userinfo" }, }, }, resources: { v2: { resources: { me: { methods: { get: { id: "oauth2.userinfo.v2.me.get", httpMethod: "GET", path: "userinfo/v2/me", scopes: OAUTH2_USERINFO_SCOPES, response: { $ref: "Userinfo" }, }, }, }, }, }, }, }, }, schemas: { Tokeninfo: { id: "Tokeninfo", type: "object", properties: { audience: { type: "string" }, scope: { type: "string" }, }, }, Userinfo: { id: "Userinfo", type: "object", properties: { email: { type: "string" }, family_name: { type: "string" }, gender: { type: "string" }, given_name: { type: "string" }, hd: { type: "string" }, id: { type: "string" }, link: { type: "string" }, locale: { type: "string" }, name: { type: "string" }, picture: { type: "string" }, verified_email: { type: "boolean" }, }, }, }, }; it("accepts only supported HTTPS Google Discovery endpoints", () => { expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest/"), ).toBe("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"); expect( normalizeGoogleDiscoveryUrl( "https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest", ), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/forms/v1/rest"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/keep/v1/rest"), ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://chat.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://www.googleapis.com/discovery/v1/apis/chat/v1/rest"); expect( normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://forms.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); expect( normalizeGoogleDiscoveryUrl("https://keep.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1"); expect(isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( true, ); expect(isGoogleDiscoveryUrl("https://evilgoogleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( false, ); expect(isGoogleDiscoveryUrl("http://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe( false, ); expect( isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest?next=x"), ).toBe(false); expect( isGoogleDiscoveryUrl("https://token@www.googleapis.com/discovery/v1/apis/gmail/v1/rest"), ).toBe(false); }); const normalizeOpenApiRefsForPreview = (node: unknown): unknown => { if (node == null || typeof node !== "object") return node; if (Array.isArray(node)) return node.map(normalizeOpenApiRefsForPreview); const obj = node as Record<string, unknown>; if (typeof obj.$ref === "string") { const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/); return match ? { ...obj, $ref: `#/$defs/${match[1]}` } : obj; } return Object.fromEntries( Object.entries(obj).map(([key, value]) => [key, normalizeOpenApiRefsForPreview(value)]), ); }; it.effect("converts Google Discovery documents into Executor-preserving OpenAPI 3 specs", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify({ name: "gmail", version: "v1", title: "Gmail API", rootUrl: "https://gmail.googleapis.com/", servicePath: "", auth: { oauth2: { scopes: { "https://www.googleapis.com/auth/gmail.metadata": { description: "Read metadata", }, }, }, }, resources: { users: { resources: { messages: { methods: { list: { id: "gmail.users.messages.list", httpMethod: "GET", path: "gmail/v1/users/{userId}/messages", scopes: ["https://www.googleapis.com/auth/gmail.metadata"], parameters: { userId: { location: "path", required: true, type: "string", description: "The user's email address. The special value me can be used.", }, metadataHeaders: { location: "query", repeated: true, type: "string", }, }, }, }, }, drafts: { methods: { create: { id: "gmail.users.drafts.create", httpMethod: "POST", path: "gmail/v1/users/{userId}/drafts", request: { $ref: "Draft" }, response: { $ref: "Draft" }, scopes: ["https://www.googleapis.com/auth/gmail.metadata"], parameters: { userId: { location: "path", required: true, type: "string", }, }, }, }, }, }, }, }, schemas: { Draft: { id: "Draft", type: "object", description: "A draft email.", properties: { id: { type: "string", description: "The immutable ID of the draft.", }, message: { $ref: "Message", }, }, }, Message: { id: "Message", type: "object", properties: { id: { type: "string", }, labelIds: { type: "array", items: { type: "string" }, }, }, }, }, }), }); const spec = decodeConvertedSpec(result.specText); const operation = spec.paths["/gmail/v1/users/{userId}/messages"]?.get; const createDraft = spec.paths["/gmail/v1/users/{userId}/drafts"]?.post; expect(spec.openapi).toBe("3.1.0"); expect(spec.servers).toEqual([{ url: "https://gmail.googleapis.com/" }]); expect(result.specText).not.toContain("_tag"); expect(operation).toMatchObject({ operationId: "users.messages.list", "x-executor-toolPath": "users.messages.list", "x-google-scopes": ["https://www.googleapis.com/auth/gmail.metadata"], }); expect(operation?.security).toEqual([ { googleOAuth2: ["https://www.googleapis.com/auth/gmail.metadata"] }, ]); expect(operation?.parameters).toContainEqual( expect.objectContaining({ name: "metadataHeaders", in: "query", style: "form", explode: true, }), ); expect(operation?.parameters).toContainEqual( expect.objectContaining({ name: "userId", description: "The user's email address. The special value me can be used.", schema: expect.objectContaining({ type: "string" }), }), ); expect(createDraft).toMatchObject({ operationId: "users.drafts.create", "x-executor-toolPath": "users.drafts.create", }); expect(createDraft).toMatchObject({ requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Draft" }, }, }, }, }); expect(createDraft?.parameters).toContainEqual( expect.objectContaining({ name: "userId", schema: expect.objectContaining({ type: "string" }), }), ); const parsed = yield* parse(result.specText); const extracted = yield* extract(parsed); const extractedDraftCreate = extracted.operations.find( (candidate) => candidate.operationId === "users.drafts.create", ); expect(extractedDraftCreate?.operationId).toBe("users.drafts.create"); const preview = yield* Effect.promise(() => buildToolTypeScriptPreview({ inputSchema: normalizeOpenApiRefsForPreview( extractedDraftCreate ? Option.getOrUndefined(extractedDraftCreate.inputSchema) : undefined, ), outputSchema: normalizeOpenApiRefsForPreview( extractedDraftCreate ? Option.getOrUndefined(extractedDraftCreate.outputSchema) : undefined, ), defs: new Map( Object.entries(spec.components.schemas).map(([name, schema]) => [ name, normalizeOpenApiRefsForPreview(schema), ]), ), }), ); expect(preview.inputTypeScript).toBe("{ userId: string; body?: Draft; }"); expect(preview.outputTypeScript).toBe("Draft"); expect(preview.typeScriptDefinitions).toMatchObject({ Draft: "{ id?: string; message?: Message; }", Message: "{ id?: string; labelIds?: string[]; }", }); // v2: the conversion now exposes a v2 oauth `authenticationTemplate` rather // than v1's `oauth2` source config. // removed: identityScopes assertion - that field belonged to the v1 // OAuth2SourceConfig slot model, which no longer exists in v2. const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2"); expect(oauthTemplate).toBeDefined(); }), ); it.effect("converts Google OAuth2 v2 top-level and aliased userinfo methods", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: OAUTH2_URL, // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify(oauth2DiscoveryDoc), }); const spec = decodeConvertedSpec(result.specText); const userinfo = spec.paths["/oauth2/v2/userinfo"]?.get; const userinfoMe = spec.paths["/userinfo/v2/me"]?.get; const tokeninfo = spec.paths["/oauth2/v2/tokeninfo"]?.post; expect(userinfo).toMatchObject({ operationId: "userinfo.get", "x-executor-toolPath": "userinfo.get", "x-google-scopes": [...OAUTH2_USERINFO_SCOPES], }); expect(userinfo?.security).toEqual( OAUTH2_USERINFO_SCOPES.map((scope) => ({ googleOAuth2: [scope] })), ); expect(userinfo?.responses).toMatchObject({ "200": { content: { "application/json": { schema: { $ref: "#/components/schemas/Userinfo" }, }, }, }, }); expect(userinfoMe).toMatchObject({ operationId: "userinfo.v2.me.get", "x-executor-toolPath": "userinfo.v2.me.get", "x-google-scopes": [...OAUTH2_USERINFO_SCOPES], }); expect(tokeninfo).toMatchObject({ operationId: "tokeninfo", "x-executor-toolPath": "tokeninfo", }); }), ); it.effect("marks Google Discovery media-download methods as binary responses", () => Effect.gen(function* () { const result = yield* convertGoogleDiscoveryToOpenApi({ discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", // @effect-diagnostics-next-line preferSchemaOverJson:off documentText: JSON.stringify({ name: "drive", version: "v3", title: "Drive API", rootUrl: "https://www.googleapis.com/", servicePath: "drive/v3/", resources: { files: { methods: { export: { id: "drive.files.export", httpMethod: "GET", path: "files/{fileId}/export", supportsMediaDownload: true, useMediaDownloadService: true, parameters: { fileId: { location: "path", required: true, type: "string", }, mimeType: { location: "query", required: true, type: "string", }, }, }, }, }, }, schemas: {}, }), }); const spec = decodeConvertedSpec(result.specText); const operation = spec.paths["/files/{fileId}/export"]?.get; expect(operation?.responses).toMatchObject({ "200": { content: { "application/octet-stream": { schema: { type: "string", format: "binary" }, }, }, }, }); const parsed = yield* parse(result.specText); const extracted = yield* extract(parsed); const exportOperation = extracted.operations.find( (candidate) => candidate.operationId === "files.export", ); expect(exportOperation?.operationId).toBe("files.export"); const responseFileHint = Option.flatMap( exportOperation?.responseBody ?? Option.none(),