@optimizely-opal/opal-tools-sdk
Version:
SDK for creating Opal-compatible tools services
135 lines (120 loc) • 4.08 kB
text/typescript
import 'reflect-metadata';
import { ParameterType, Parameter, AuthRequirement } from './models';
import { registry } from './registry';
interface ParameterDefinition {
name: string;
type: ParameterType;
description: string;
required: boolean;
}
interface ToolOptions {
name: string;
description: string;
parameters?: ParameterDefinition[];
authRequirements?: {
provider: string;
scopeBundle: string;
required?: boolean;
};
}
/**
* Map a TypeScript type to a ParameterType
* @param type TypeScript type
*/
function mapTypeToParameterType(type: any): ParameterType {
if (type === String || type.name === 'String') {
return ParameterType.String;
} else if (type === Number || type.name === 'Number') {
return ParameterType.Number;
} else if (type === Boolean || type.name === 'Boolean') {
return ParameterType.Boolean;
} else if (type === Array || type.name === 'Array') {
return ParameterType.List;
} else if (type === Object || type.name === 'Object') {
return ParameterType.Dictionary;
}
// Default to string
return ParameterType.String;
}
/**
* Extract parameters from a TypeScript interface
* @param paramType Parameter type object
*/
function extractParameters(paramType: any): Parameter[] {
const parameters: Parameter[] = [];
// This is very basic and doesn't handle complex types
// For production use, this would need to be more sophisticated
for (const key in paramType) {
if (paramType.hasOwnProperty(key)) {
const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
const required = true; // In a real implementation, we'd detect optional parameters
parameters.push(new Parameter(
key,
mapTypeToParameterType(type),
'', // Description - in a real impl we'd use TypeDoc or similar
required
));
}
}
return parameters;
}
/**
* Decorator to register a function as an Opal tool
* @param options Tool options including:
* - name: Name of the tool
* - description: Description of the tool
* - authRequirements: (Optional) Authentication requirements
* Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
* Example: { provider: "google", scopeBundle: "calendar", required: true }
*
* Note: If your tool requires authentication, define your handler function with two parameters:
* ```
* async function myTool(parameters: ParameterInterface, authData?: any): Promise<any> {
* // Your tool implementation
* }
* ```
*/
export function tool(options: ToolOptions) {
return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
const isMethod = propertyKey && descriptor;
const handler = isMethod ? descriptor.value : target;
// Generate endpoint from name - ensure hyphens instead of underscores
const endpoint = `/tools/${options.name.replace(/_/g, '-')}`;
// Convert parameter definitions to Parameter objects
const parameters: Parameter[] = [];
if (options.parameters && options.parameters.length > 0) {
// Use the explicitly provided parameter definitions
for (const paramDef of options.parameters) {
parameters.push(new Parameter(
paramDef.name,
paramDef.type,
paramDef.description,
paramDef.required
));
}
}
// Create auth requirements if specified
let authRequirements: AuthRequirement[] | undefined;
if (options.authRequirements) {
authRequirements = [
new AuthRequirement(
options.authRequirements.provider,
options.authRequirements.scopeBundle,
options.authRequirements.required ?? true
)
];
}
// Register the tool with all services
for (const service of registry.services) {
service.registerTool(
options.name,
options.description,
handler,
parameters,
endpoint,
authRequirements
);
}
return isMethod ? descriptor : target;
};
}