@firefliesai/schema-forge
Version:
Transform TypeScript classes into JSON Schema definitions with automatic support for OpenAI, Anthropic, and Google Gemini function calling (tool) formats
244 lines (243 loc) • 14.9 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
require("reflect-metadata");
const lodash_1 = require("lodash");
const complex_class_tool_dto_1 = require("./fixture/complex-class.tool.dto");
const simple_class_tool_dto_1 = require("./fixture/simple-class.tool.dto");
const schema_forge_1 = require("./schema-forge");
describe('schema-forge test', () => {
it('1 simple classes: classToJsonSchema, inheritance, classToJsonSchema with temp updated property, updateSchemaProperty (permanently)', async () => {
const user2JsonSchemaTempChangeID2 = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2, {
propertyOverrides: {
id2: { description: 'temp updated id2 description' },
},
});
expect(user2JsonSchemaTempChangeID2).toMatchSnapshot('1-1 inheritance class: classToJsonSchema with temp updated property');
const user2JsonSchema = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2);
expect(user2JsonSchema).toMatchSnapshot('1-2 inheritance class: classToJsonSchema and should not affected by temp updated property');
(0, schema_forge_1.updateSchemaProperty)(simple_class_tool_dto_1.User2, 'id2', {
description: 'permanently updated id2 description',
});
const user2JsonSchemaPersistChangeID2 = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User2);
expect(user2JsonSchemaPersistChangeID2).toMatchSnapshot('1-3 inheritance class: updateSchemaProperty desc (permanently) and classToJsonSchema');
const userJsonSchema = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User);
expect(userJsonSchema).toMatchSnapshot('1-4 parent class: classToJsonSchema (should not be affected by child class update)');
});
it('2 complex: array and enum class: classToJsonSchema, classToOpenAITool, updateSchemaProperty (permanently) w/ enum, ', async () => {
const gameCharSchema = (0, schema_forge_1.classToJsonSchema)(complex_class_tool_dto_1.GameCharacter);
expect(gameCharSchema).toMatchSnapshot('2-1 complex classToJsonSchema');
const gameCharTool = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacter);
expect(gameCharTool.type).toBe('function');
expect(gameCharTool.function.name).toBe(complex_class_tool_dto_1.GameCharacterToolName);
expect(gameCharTool.function.description).toBe(complex_class_tool_dto_1.GameCharacterToolDesc);
expect((0, lodash_1.isEqual)(gameCharTool.function.parameters, gameCharSchema)).toBe(true);
/** updateSchemaProperty: enum, enum array */
(0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'roles', {
enum: ['hero'],
});
(0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'status', {
enum: ['unknown'],
});
(0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacter, 'level', {
description: 'Updated level description',
});
const gameCharUpdatedSchema = (0, schema_forge_1.classToJsonSchema)(complex_class_tool_dto_1.GameCharacter);
gameCharSchema.properties.status.enum = ['unknown'];
gameCharSchema.properties.roles.items.enum = ['hero'];
gameCharSchema.properties.level.description = 'Updated level description';
expect((0, lodash_1.isEqual)(gameCharUpdatedSchema, gameCharSchema)).toBe(true);
});
it('3 complex nested_object class: classToOpenAITool, updateSchemaProperty (permanently) w/ enum, ', async () => {
const gameCharV2Tool = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacterV2);
expect(gameCharV2Tool).toMatchSnapshot('3-1 complex nested_object: classToOpenAITool');
(0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.GameCharacterV2, 'banks.bankName', {
description: 'New bankname description',
});
const gameCharV2Tool2 = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.GameCharacterV2, {
propertyOverrides: {
location: {
description: 'New location description',
},
'location.country': {
description: 'New country description',
},
},
});
gameCharV2Tool.function.parameters.properties.location.description = 'New location description';
gameCharV2Tool.function.parameters.properties.banks.items.properties.bankName.description =
'New bankname description';
gameCharV2Tool.function.parameters.properties.location.properties.country.description =
'New country description';
expect((0, lodash_1.isEqual)(gameCharV2Tool2, gameCharV2Tool)).toBe(true);
});
it('4 complex nested nested three layer class: classToOpenAITool,updateSchemaProperty (permanently) w/ enum ', async () => {
(0, schema_forge_1.updateSchemaProperty)(complex_class_tool_dto_1.FirstLevelDto, 'secondLevelObj.thirdLevelObjs.name', {
enum: ['E', 'F', 'G', 'H'],
});
const firstLevelDto = (0, schema_forge_1.classToOpenAITool)(complex_class_tool_dto_1.FirstLevelDto);
expect(firstLevelDto).toMatchSnapshot('4-1 complex nested nested three layer class: updateSchemaProperty and classToOpenAITool');
});
it('5 addSchemaProperty case', async () => {
class TicketLLMAnswer {
}
(0, schema_forge_1.addSchemaProperty)(TicketLLMAnswer, 'ticketTitle1', {
type: 'string',
description: 'answer',
});
(0, schema_forge_1.addSchemaProperty)(TicketLLMAnswer, 'ticketTitle2', {
enum: ['optionName1', 'optionName2'],
isOptional: true,
});
const ticketLLMAnswerSchema = (0, schema_forge_1.classToJsonSchema)(TicketLLMAnswer);
expect(ticketLLMAnswerSchema).toMatchSnapshot('5-1 addSchemaProperty case');
});
it('6 class with ToolProp() case', async () => {
class SimpleAnswer {
}
__decorate([
(0, schema_forge_1.ToolProp)(),
__metadata("design:type", String)
], SimpleAnswer.prototype, "answer", void 0);
const schema = (0, schema_forge_1.classToJsonSchema)(SimpleAnswer);
expect(schema).toMatchSnapshot('6-1 class with ToolProp()');
});
it('7 structured output enhancement', async () => {
// Test enhanced JSON Schema
const userSchemaEnhanced = (0, schema_forge_1.classToJsonSchema)(simple_class_tool_dto_1.User, { forStructuredOutput: true });
expect(userSchemaEnhanced).toMatchSnapshot('7-1 enhanced JSON Schema');
// Test OpenAI function calling format
const userToolEnhanced = (0, schema_forge_1.classToOpenAITool)(simple_class_tool_dto_1.User, { forStructuredOutput: true });
expect(userToolEnhanced).toMatchSnapshot('7-2 enhanced OpenAI function calling format');
// Test OpenAI response_format
const userJsonSchemaFormat = (0, schema_forge_1.classToOpenAIResponseFormatJsonSchema)(simple_class_tool_dto_1.User, {
forStructuredOutput: true,
});
expect(userJsonSchemaFormat).toMatchSnapshot('7-3 OpenAI JSON Schema format for response_format');
});
it('8 different LLM formats', async () => {
// Test Gemini tool format
const geminiTool = (0, schema_forge_1.classToGeminiTool)(simple_class_tool_dto_1.User);
expect(geminiTool).toMatchSnapshot('8-1 Gemini tool format');
// Test Anthropic tool format
const anthropicTool = (0, schema_forge_1.classToAnthropicTool)(simple_class_tool_dto_1.User);
expect(anthropicTool).toMatchSnapshot('8-2 Anthropic tool format');
// Test Gemini response schema
const geminiResponseSchema = (0, schema_forge_1.classToGeminiResponseSchema)(simple_class_tool_dto_1.User);
expect(geminiResponseSchema).toMatchSnapshot('8-3 Gemini response schema');
});
it('9 optional properties handling for different LLM providers', async () => {
// Define nested DTOs first
class AddressDto {
}
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Street address' }),
__metadata("design:type", String)
], AddressDto.prototype, "street", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'City name', isOptional: true }),
__metadata("design:type", String)
], AddressDto.prototype, "city", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Postal code' }),
__metadata("design:type", String)
], AddressDto.prototype, "postalCode", void 0);
class ContactDto {
}
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Contact name' }),
__metadata("design:type", String)
], ContactDto.prototype, "name", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Contact email', isOptional: true }),
__metadata("design:type", String)
], ContactDto.prototype, "email", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Phone number' }),
__metadata("design:type", String)
], ContactDto.prototype, "phone", void 0);
// Create a test class with optional properties including nested objects
class TestWithOptionals {
}
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'User ID' }),
__metadata("design:type", String)
], TestWithOptionals.prototype, "id", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Username', isOptional: true }),
__metadata("design:type", String)
], TestWithOptionals.prototype, "username", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Age of the user', isOptional: true }),
__metadata("design:type", Number)
], TestWithOptionals.prototype, "age", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({
description: 'Tags for the user',
items: { type: 'string' },
isOptional: true,
}),
__metadata("design:type", Array)
], TestWithOptionals.prototype, "tags", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({ description: 'Primary address information', isOptional: true }),
__metadata("design:type", AddressDto)
], TestWithOptionals.prototype, "address", void 0);
__decorate([
(0, schema_forge_1.ToolProp)({
description: 'List of contact methods',
items: { type: ContactDto },
isOptional: true,
}),
__metadata("design:type", Array)
], TestWithOptionals.prototype, "contacts", void 0);
// Test OpenAI format handles optional properties with ["type", "null"]
const openaiTool = (0, schema_forge_1.classToOpenAITool)(TestWithOptionals, { forStructuredOutput: true });
expect(openaiTool.function.parameters.properties.username.type).toEqual(['string', 'null']);
expect(openaiTool.function.parameters.properties.age.type).toEqual(['number', 'null']);
expect(openaiTool.function.parameters.properties.tags.type).toEqual(['array', 'null']);
expect(openaiTool.function.parameters.properties.address.type).toEqual(['object', 'null']);
expect(openaiTool.function.parameters.properties.contacts.type).toEqual(['array', 'null']);
expect(openaiTool).toMatchSnapshot('9-1 OpenAI optional properties with nested objects');
// Test Gemini format properly marks properties as optional (not in required array)
const geminiTool = (0, schema_forge_1.classToGeminiTool)(TestWithOptionals);
// Check that optional properties are not in the required array
expect(geminiTool.parameters.required).not.toContain('username');
expect(geminiTool.parameters.required).not.toContain('age');
expect(geminiTool.parameters.required).not.toContain('tags');
expect(geminiTool.parameters.required).not.toContain('address');
expect(geminiTool.parameters.required).not.toContain('contacts');
// Required properties should be in the required array
expect(geminiTool.parameters.required).toContain('id');
expect(geminiTool).toMatchSnapshot('9-2 Gemini optional properties handling');
// Test Gemini response schema also properly marks properties as optional
const geminiResponseSchema = (0, schema_forge_1.classToGeminiResponseSchema)(TestWithOptionals);
// Check that optional properties are not in the required array
expect(geminiResponseSchema.required).not.toContain('username');
expect(geminiResponseSchema.required).not.toContain('age');
expect(geminiResponseSchema.required).not.toContain('tags');
expect(geminiResponseSchema.required).not.toContain('address');
expect(geminiResponseSchema.required).not.toContain('contacts');
// Required properties should be in the required array
expect(geminiResponseSchema.required).toContain('id');
expect(geminiResponseSchema).toMatchSnapshot('9-3 Gemini response schema optional properties handling');
// Verify that optionals are properly handled in nested objects
expect(openaiTool.function.parameters.properties.address.properties.city.type).toEqual([
'string',
'null',
]);
// For Gemini, verify city is not in address's required array
const addressProps = geminiTool.parameters.properties.address;
expect(addressProps.properties.city).toBeDefined();
const addressRequired = addressProps.required || [];
expect(addressRequired).not.toContain('city');
});
});