adk-typescript
Version:
TypeScript port of Google's Agent Development Kit (ADK)
548 lines (547 loc) • 20.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleApiToOpenApiConverterImpl = void 0;
exports.main = main;
const fs = __importStar(require("fs"));
/**
* Converts Google API Discovery documents to OpenAPI v3 format.
* Implements the GoogleApiToOpenApiConverter interface.
*/
class GoogleApiToOpenApiConverterImpl {
/**
* Initialize the converter with the API name and version.
*
* @param apiName The name of the Google API (e.g., "calendar")
* @param apiVersion The version of the API (e.g., "v3")
*/
constructor(apiName, apiVersion) {
/**
* The Google API resource
*/
this.googleApiResource = null;
/**
* The Google API specification
*/
this.googleApiSpec = null;
/**
* The OpenAPI specification
*/
this.openApiSpec = {
openapi: "3.0.0",
info: {},
servers: [],
paths: {},
components: { schemas: {}, securitySchemes: {} }
};
this.apiName = apiName;
this.apiVersion = apiVersion;
}
/**
* Fetches the Google API specification using discovery service.
*
* Note: In a real implementation, this would use the googleapis library.
* For this port, we'll create a placeholder that simulates fetching the API spec.
*/
async fetchGoogleApiSpec() {
try {
console.log(`Fetching Google API spec for ${this.apiName} ${this.apiVersion}`);
// In a real implementation, this would use:
// const { google } = require('googleapis');
// this.googleApiResource = await google.discoverAPI(
// `https://discovery.googleapis.com/discovery/v1/apis/${this.apiName}/${this.apiVersion}/rest`
// );
// this.googleApiSpec = this.googleApiResource._rootDesc;
// For now, let's create a placeholder implementation
// This would be replaced with actual API discovery in a real implementation
console.log("Note: This is a placeholder implementation. In a real implementation, this would fetch the actual API spec.");
// Simulated API spec (would be replaced with actual API spec in real implementation)
this.googleApiSpec = {
name: this.apiName,
version: this.apiVersion,
title: `${this.apiName} API`,
description: `The ${this.apiName} API`,
documentationLink: `https://developers.google.com/apis-explorer/#p/${this.apiName}/${this.apiVersion}/`,
rootUrl: `https://${this.apiName}.googleapis.com/`,
servicePath: "",
schemas: {},
resources: {},
auth: {
oauth2: {
scopes: {}
}
}
};
if (!this.googleApiSpec) {
throw new Error("Failed to retrieve API specification");
}
console.log(`Successfully fetched ${this.apiName} API specification`);
}
catch (error) {
console.error("Error fetching API spec:", error);
throw error;
}
}
/**
* Convert the Google API spec to OpenAPI v3 format.
*
* @returns Object containing the converted OpenAPI v3 specification
*/
async convert() {
if (!this.googleApiSpec) {
await this.fetchGoogleApiSpec();
}
// Convert basic API information
this._convertInfo();
// Convert server information
this._convertServers();
// Convert authentication/authorization schemes
this._convertSecuritySchemes();
// Convert schemas (models)
this._convertSchemas();
// Convert endpoints/paths
this._convertResources(this.googleApiSpec.resources || {});
// Convert top-level methods, if any
this._convertMethods(this.googleApiSpec.methods || {}, "/");
return this.openApiSpec;
}
/**
* Convert basic API information.
*/
_convertInfo() {
this.openApiSpec.info = {
title: this.googleApiSpec.title || `${this.apiName} API`,
description: this.googleApiSpec.description || "",
version: this.googleApiSpec.version || this.apiVersion,
contact: {},
termsOfService: this.googleApiSpec.documentationLink || ""
};
// Add documentation links if available
const docsLink = this.googleApiSpec.documentationLink;
if (docsLink) {
this.openApiSpec.externalDocs = {
description: "API Documentation",
url: docsLink
};
}
}
/**
* Convert server information.
*/
_convertServers() {
let baseUrl = (this.googleApiSpec.rootUrl || "") + (this.googleApiSpec.servicePath || "");
// Remove trailing slash if present
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
this.openApiSpec.servers = [{
url: baseUrl,
description: `${this.apiName} ${this.apiVersion} API`
}];
}
/**
* Convert authentication and authorization schemes.
*/
_convertSecuritySchemes() {
const auth = this.googleApiSpec.auth || {};
const oauth2 = auth.oauth2 || {};
let formattedScopes = {};
if (oauth2) {
// Handle OAuth2
const scopes = oauth2.scopes || {};
for (const [scope, scopeInfo] of Object.entries(scopes)) {
// Handle potentially undefined scopeInfo or missing description
const scopeInfoObj = scopeInfo;
formattedScopes[scope] = scopeInfoObj && typeof scopeInfoObj === 'object' && 'description' in scopeInfoObj
? scopeInfoObj.description || ""
: "";
}
this.openApiSpec.components.securitySchemes.oauth2 = {
type: "oauth2",
description: "OAuth 2.0 authentication",
flows: {
authorizationCode: {
authorizationUrl: "https://accounts.google.com/o/oauth2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
scopes: formattedScopes
}
}
};
}
// Add API key authentication (most Google APIs support this)
this.openApiSpec.components.securitySchemes.apiKey = {
type: "apiKey",
in: "query",
name: "key",
description: "API key for accessing this API"
};
// Create global security requirement
this.openApiSpec.security = [
oauth2 ? { oauth2: Object.keys(formattedScopes) } : {},
{ apiKey: [] }
];
}
/**
* Convert schema definitions (models).
*/
_convertSchemas() {
const schemas = this.googleApiSpec.schemas || {};
for (const [schemaName, schemaDef] of Object.entries(schemas)) {
const convertedSchema = this._convertSchemaObject(schemaDef);
this.openApiSpec.components.schemas[schemaName] = convertedSchema;
}
}
/**
* Recursively convert a Google API schema object to OpenAPI schema.
*
* @param schemaDef Google API schema definition
* @returns Converted OpenAPI schema object
*/
_convertSchemaObject(schemaDef) {
const result = {};
// Convert the type
if (schemaDef.type) {
const gtype = schemaDef.type;
if (gtype === "object") {
result.type = "object";
// Handle properties
if (schemaDef.properties) {
result.properties = {};
for (const [propName, propDef] of Object.entries(schemaDef.properties)) {
result.properties[propName] = this._convertSchemaObject(propDef);
}
}
// Handle required fields
const requiredFields = [];
for (const [propName, propDef] of Object.entries(schemaDef.properties || {})) {
if (propDef.required) {
requiredFields.push(propName);
}
}
if (requiredFields.length > 0) {
result.required = requiredFields;
}
}
else if (gtype === "array") {
result.type = "array";
if (schemaDef.items) {
result.items = this._convertSchemaObject(schemaDef.items);
}
}
else if (gtype === "any") {
// OpenAPI doesn't have direct "any" type
// Use oneOf with multiple options as alternative
result.oneOf = [
{ type: "object" },
{ type: "array" },
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
{ type: "null" }
];
}
else {
// Handle other primitive types
result.type = gtype;
}
}
// Handle references
if (schemaDef.$ref) {
let ref = schemaDef.$ref;
// Google refs use "#" at start, OpenAPI uses "#/components/schemas/"
if (ref.startsWith("#")) {
ref = ref.replace("#", "#/components/schemas/");
}
else {
ref = "#/components/schemas/" + ref;
}
result.$ref = ref;
}
// Handle format
if (schemaDef.format) {
result.format = schemaDef.format;
}
// Handle enum values
if (schemaDef.enum) {
result.enum = schemaDef.enum;
}
// Handle description
if (schemaDef.description) {
result.description = schemaDef.description;
}
// Handle pattern
if (schemaDef.pattern) {
result.pattern = schemaDef.pattern;
}
// Handle default value
if (schemaDef.default !== undefined) {
result.default = schemaDef.default;
}
return result;
}
/**
* Recursively convert all resources and their methods.
*
* @param resources Dictionary of resources from the Google API spec
* @param parentPath The parent path prefix for nested resources
*/
_convertResources(resources, parentPath = "") {
for (const [resourceName, resourceData] of Object.entries(resources)) {
// Process methods for this resource
const resourcePath = `${parentPath}/${resourceName}`;
const methods = resourceData.methods || {};
this._convertMethods(methods, resourcePath);
// Process nested resources recursively
const nestedResources = resourceData.resources || {};
if (Object.keys(nestedResources).length > 0) {
this._convertResources(nestedResources, resourcePath);
}
}
}
/**
* Convert methods for a specific resource path.
*
* @param methods Dictionary of methods from the Google API spec
* @param resourcePath The path of the resource these methods belong to
*/
_convertMethods(methods, resourcePath) {
for (const [methodName, methodData] of Object.entries(methods)) {
const httpMethod = (methodData.httpMethod || "GET").toLowerCase();
// Determine the actual endpoint path
// Google often has the format something like 'users.messages.list'
// flatPath is preferred as it provides the actual path, while path
// might contain variables like {+projectId}
let restPath = methodData.flatPath || methodData.path || "/";
if (!restPath.startsWith("/")) {
restPath = "/" + restPath;
}
const pathParams = this._extractPathParameters(restPath);
// Create path entry if it doesn't exist
if (!this.openApiSpec.paths[restPath]) {
this.openApiSpec.paths[restPath] = {};
}
// Add the operation for this method
this.openApiSpec.paths[restPath][httpMethod] = this._convertOperation(methodData, pathParams);
}
}
/**
* Extract path parameters from a URL path.
*
* @param path The URL path with path parameters
* @returns List of parameter names
*/
_extractPathParameters(path) {
const params = [];
const segments = path.split("/");
for (const segment of segments) {
// Google APIs often use {param} format for path parameters
if (segment.startsWith("{") && segment.endsWith("}")) {
const paramName = segment.substring(1, segment.length - 1);
params.push(paramName);
}
}
return params;
}
/**
* Convert a Google API method to an OpenAPI operation.
*
* @param methodData Google API method data
* @param pathParams List of path parameter names
* @returns OpenAPI operation object
*/
_convertOperation(methodData, pathParams) {
const operation = {
operationId: methodData.id || "",
summary: methodData.description || "",
description: methodData.description || "",
parameters: [],
responses: {
"200": { description: "Successful operation" },
"400": { description: "Bad request" },
"401": { description: "Unauthorized" },
"403": { description: "Forbidden" },
"404": { description: "Not found" },
"500": { description: "Server error" }
}
};
// Add path parameters
for (const paramName of pathParams) {
const param = {
name: paramName,
in: "path",
required: true,
schema: { type: "string" }
};
operation.parameters.push(param);
}
// Add query parameters
for (const [paramName, paramData] of Object.entries(methodData.parameters || {})) {
// Skip parameters already included in path
if (pathParams.includes(paramName)) {
continue;
}
const param = {
name: paramName,
in: "query",
description: paramData.description || "",
required: paramData.required || false,
schema: this._convertParameterSchema(paramData)
};
operation.parameters.push(param);
}
// Handle request body
if (methodData.request) {
let requestRef = methodData.request.$ref || "";
if (requestRef) {
if (requestRef.startsWith("#")) {
// Convert Google's reference format to OpenAPI format
requestRef = requestRef.replace("#", "#/components/schemas/");
}
else {
requestRef = "#/components/schemas/" + requestRef;
}
operation.requestBody = {
description: "Request body",
content: { "application/json": { schema: { $ref: requestRef } } },
required: true
};
}
}
// Handle response body
if (methodData.response) {
let responseRef = methodData.response.$ref || "";
if (responseRef) {
if (responseRef.startsWith("#")) {
// Convert Google's reference format to OpenAPI format
responseRef = responseRef.replace("#", "#/components/schemas/");
}
else {
responseRef = "#/components/schemas/" + responseRef;
}
operation.responses["200"].content = {
"application/json": { schema: { $ref: responseRef } }
};
}
}
// Add scopes if available
const scopes = methodData.scopes || [];
if (scopes.length > 0) {
// Add method-specific security requirement if different from global
operation.security = [{ oauth2: scopes }];
}
return operation;
}
/**
* Convert a parameter definition to an OpenAPI schema.
*
* @param paramData Google API parameter data
* @returns OpenAPI schema for the parameter
*/
_convertParameterSchema(paramData) {
const schema = {};
// Convert type
const paramType = paramData.type || "string";
schema.type = paramType;
// Handle enum values
if (paramData.enum) {
schema.enum = paramData.enum;
}
// Handle format
if (paramData.format) {
schema.format = paramData.format;
}
// Handle default value
if (paramData.default !== undefined) {
schema.default = paramData.default;
}
// Handle pattern
if (paramData.pattern) {
schema.pattern = paramData.pattern;
}
return schema;
}
/**
* Save the OpenAPI specification to a file.
*
* @param outputPath Path where the OpenAPI spec should be saved
*/
saveOpenApiSpec(outputPath) {
fs.writeFileSync(outputPath, JSON.stringify(this.openApiSpec, null, 2), 'utf-8');
console.log(`OpenAPI specification saved to ${outputPath}`);
}
}
exports.GoogleApiToOpenApiConverterImpl = GoogleApiToOpenApiConverterImpl;
/**
* Command line interface for the converter.
*
* Note: This is included for compatibility with the Python implementation,
* but would typically be implemented differently in a TypeScript project.
*/
async function main(args) {
if (args.length < 2) {
console.error("Required arguments: <api_name> <api_version> [--output path]");
return 1;
}
const apiName = args[0];
const apiVersion = args[1];
// Check for output path
let outputPath = "openapi_spec.json";
const outputIndex = args.indexOf("--output");
if (outputIndex !== -1 && outputIndex < args.length - 1) {
outputPath = args[outputIndex + 1];
}
try {
// Create and run the converter
const converter = new GoogleApiToOpenApiConverterImpl(apiName, apiVersion);
await converter.convert();
converter.saveOpenApiSpec(outputPath);
console.log(`Successfully converted ${apiName} ${apiVersion} to OpenAPI v3`);
console.log(`Output saved to ${outputPath}`);
}
catch (error) {
console.error("Conversion failed:", error);
return 1;
}
return 0;
}
// If this file is run directly (not imported)
if (require.main === module) {
main(process.argv.slice(2))
.then(exitCode => process.exit(exitCode))
.catch(error => {
console.error("Unhandled error:", error);
process.exit(1);
});
}