@firefliesai/schema-forge
Version:
Transform TypeScript classes into JSON Schema definitions with automatic support for OpenAI, Anthropic, and Google Gemini function calling (tool) formats
325 lines (324 loc) • 13.6 kB
JavaScript
;
/**
* Decorators for schema-forge
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyPropertyUpdates = applyPropertyUpdates;
exports.ToolProp = ToolProp;
exports.ToolMeta = ToolMeta;
exports.updateSchemaProperty = updateSchemaProperty;
exports.addSchemaProperty = addSchemaProperty;
const core_1 = require("./core");
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
* Applies property updates for a given property path
*/
function applyPropertyUpdates(properties, paths, updates, target) {
const [current, ...rest] = paths;
// Handle nested path
if (rest.length > 0) {
if (!properties[current]) {
properties[current] = {};
}
// Get all metadata along the target property's full path
const originalMetadata = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {};
let currentMetadata = originalMetadata;
for (const path of paths) {
currentMetadata =
currentMetadata?.[path] ||
currentMetadata?.items?.properties?.[path] ||
currentMetadata?.properties?.[path] ||
{};
}
// Handle enum update for nested property
if (updates.enum) {
const enumValues = (0, utils_1.extractEnumValues)(updates.enum);
const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number';
const targetProp = properties[current];
if (targetProp.type === 'array') {
// Update in items.properties
if (!targetProp.items.properties) {
targetProp.items.properties = {};
}
let current = targetProp.items.properties;
for (let i = 0; i < rest.length - 1; i++) {
const path = rest[i];
if (!current[path]) {
current[path] = { type: 'object', properties: {} };
}
// If this is an array type property
if (current[path].type === 'array') {
if (!current[path].items.properties) {
current[path].items.properties = {};
}
current = current[path].items.properties;
}
else {
// Object type property
if (!current[path].properties) {
current[path].properties = {};
}
current = current[path].properties;
}
}
// Get the last path segment
const lastPath = rest[rest.length - 1];
// Check if the target property should be an array of enums or just an enum
if (current[lastPath] && current[lastPath].type === 'array') {
// Update items of an array property
current[lastPath].items = {
type: enumType,
enum: enumValues,
};
}
else {
// Direct enum property
current[lastPath] = {
...(current[lastPath] || {}),
type: enumType,
enum: enumValues,
description: current[lastPath]?.description || currentMetadata.description,
};
}
}
else if (targetProp.type === 'object') {
// Update in object properties
if (!targetProp.properties) {
targetProp.properties = {};
}
let current = targetProp.properties;
for (let i = 0; i < rest.length - 1; i++) {
const path = rest[i];
if (!current[path]) {
current[path] = { type: 'object', properties: {} };
}
// If this is an array type property
if (current[path].type === 'array') {
if (!current[path].items.properties) {
current[path].items.properties = {};
}
current = current[path].items.properties;
}
else {
// Object type property
if (!current[path].properties) {
current[path].properties = {};
}
current = current[path].properties;
}
}
// Get the last path segment
const lastPath = rest[rest.length - 1];
// Update the enum property
current[lastPath] = {
...(current[lastPath] || {}),
type: enumType,
enum: enumValues,
description: current[lastPath]?.description || currentMetadata.description,
};
}
}
else {
// Update general nested path
if (properties[current].type === 'array') {
if (!properties[current].items.properties) {
properties[current].items.properties = {};
}
applyPropertyUpdates(properties[current].items.properties, rest, updates, target);
}
// Handle nested object properties
else if (properties[current].type === 'object') {
if (!properties[current].properties) {
properties[current].properties = {};
}
applyPropertyUpdates(properties[current].properties, rest, updates, target);
}
}
return;
}
// Handle non-nested path
const currentProperty = properties[current] || {};
if (updates.enum) {
const enumValues = (0, utils_1.extractEnumValues)(updates.enum);
const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number';
const type = Reflect.getMetadata('design:type', target.prototype, current);
if (type === Array) {
properties[current] = {
...currentProperty,
type: 'array',
items: {
type: enumType,
enum: enumValues,
},
};
}
else {
properties[current] = {
...currentProperty,
type: enumType,
enum: enumValues,
};
}
const { enum: _, ...remainingUpdates } = updates;
Object.assign(properties[current], remainingUpdates);
}
else {
properties[current] = {
...currentProperty,
...updates,
};
}
}
/**
* Decorator for adding schema metadata to a class property
*/
function ToolProp(options = {}) {
return function (target, propertyKey) {
const type = Reflect.getMetadata('design:type', target, propertyKey);
// Exclude isOptional, keep only other options
const { isOptional: _isOptional, ...finalOptions } = options;
const ownProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target) || {};
const ownRequiredProps = Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, target) || [];
const parentTarget = Object.getPrototypeOf(target);
let currentProperties = {};
let currentRequiredProps = [];
if (parentTarget && parentTarget !== Object.prototype) {
const parentProperties = (0, utils_1.cloneMetadata)(Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, parentTarget) || {});
const parentRequiredProps = [
...(Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, parentTarget) || []),
];
currentProperties = { ...parentProperties };
currentRequiredProps = [...parentRequiredProps];
}
currentProperties = { ...currentProperties, ...ownProperties };
currentRequiredProps = [...new Set([...currentRequiredProps, ...ownRequiredProps])];
if (type === Array) {
if (options.enum) {
const enumValues = (0, utils_1.extractEnumValues)(options.enum);
const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number';
finalOptions.type = 'array';
finalOptions.items = {
type: enumType,
enum: enumValues,
};
delete finalOptions.enum;
}
else if (!options.items) {
throw new Error(`Array property "${propertyKey}" needs explicit type information.`);
}
else if ((0, utils_1.isCustomClass)(options.items.type)) {
const nestedSchema = (0, core_1.classToJsonSchema)(options.items.type);
finalOptions.type = 'array';
finalOptions.items = nestedSchema;
}
else {
finalOptions.type = 'array';
finalOptions.items = options.items;
}
}
else if (options.enum) {
const enumValues = (0, utils_1.extractEnumValues)(options.enum);
const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number';
finalOptions.type = enumType;
finalOptions.enum = enumValues;
}
else if ((0, utils_1.isCustomClass)(type)) {
const nestedSchema = (0, core_1.classToJsonSchema)(type);
finalOptions.type = 'object';
finalOptions.properties = nestedSchema.properties;
finalOptions.required = nestedSchema.required;
}
currentProperties[propertyKey] = {
...finalOptions,
type: finalOptions.type || (0, utils_1.getJsonSchemaType)(type),
};
// Handle required props only, don't store isOptional
if (!options.isOptional) {
if (!currentRequiredProps.includes(propertyKey)) {
currentRequiredProps.push(propertyKey);
}
}
else {
currentRequiredProps = currentRequiredProps.filter((prop) => prop !== propertyKey);
}
Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, currentProperties, target);
Reflect.defineMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, currentRequiredProps, target);
};
}
/**
* Decorator for adding schema metadata to a class
*/
function ToolMeta(options = {}) {
return function (target) {
Reflect.defineMetadata('jsonSchema:options', options, target);
};
}
/**
* Updates a property in a schema with new options
*/
function updateSchemaProperty(target, propertyPath, updates) {
const paths = propertyPath.split('.');
const existingProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {};
applyPropertyUpdates(existingProperties, paths, updates, target);
Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, existingProperties, target.prototype);
}
/**
* Adds a new property to a schema
*/
function addSchemaProperty(target, propertyPath, options) {
const finalOptions = { ...options };
if (options.enum) {
const enumValues = (0, utils_1.extractEnumValues)(options.enum);
const enumType = typeof enumValues[0] === 'string' ? 'string' : 'number';
finalOptions.type = enumType;
}
else if (!options.type) {
throw new Error('Either type or enum must be specified');
}
const paths = propertyPath.split('.');
const existingProperties = Reflect.getMetadata(types_1.JSON_SCHEMA_METADATA_KEY, target.prototype) || {};
let current = existingProperties;
for (let i = 0; i < paths.length - 1; i++) {
const path = paths[i];
if (current[path]?.type === 'array') {
if (!current[path].items.properties) {
current[path].items.properties = {};
}
current = current[path].items.properties;
}
else {
if (!current[path]?.properties) {
current[path] = {
type: 'object',
properties: {},
};
}
current = current[path].properties;
}
}
const propertyKey = paths[paths.length - 1];
current[propertyKey] = {
type: finalOptions.type,
description: finalOptions.description,
};
if (finalOptions.enum) {
const enumValues = (0, utils_1.extractEnumValues)(finalOptions.enum);
current[propertyKey].enum = enumValues;
}
if (finalOptions.items) {
current[propertyKey] = {
type: 'array',
description: finalOptions.description,
items: finalOptions.items,
};
}
Reflect.defineMetadata(types_1.JSON_SCHEMA_METADATA_KEY, existingProperties, target.prototype);
if (!finalOptions.isOptional) {
const requiredProps = Reflect.getMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, target.prototype) || [];
if (!requiredProps.includes(propertyKey)) {
requiredProps.push(propertyKey);
Reflect.defineMetadata(types_1.REQUIRED_PROPS_METADATA_KEY, requiredProps, target.prototype);
}
}
}