Skip to main content
Glama
base-schemas.test.ts13.1 kB
import { describe, it, expect } from 'vitest'; import { IdField, UuidField, WorldIdField, DateTimeField, TimestampFields, GridXField, GridYField, GridCoordinates, TacticalPosition, BoundingBox, NameField, ShortDescriptionField, LongDescriptionField, NonNegativeInt, PositiveInt, PopulationField, LevelField, DCField, HpField, AcField, PercentageField, PercentageInt, AbilityScoreField, AbilityScores, BaseSizeCategory, BaseDamageTypeEnum, BaseDamageTypeArray, ConditionTypeEnum, CurrencyFields, EncounterStatusEnum, MovementSpeedField, DiscoveryStateEnum, DirectionEnum, ExitTypeEnum, CoverTypeEnum, createEntitySchema, createUuidEntitySchema, createWorldEntitySchema, } from '../../src/schema/base-schemas'; import { z } from 'zod'; describe('base-schemas', () => { describe('Identifier Fields', () => { it('IdField rejects empty strings', () => { expect(() => IdField.parse('')).toThrow(); expect(IdField.parse('valid-id')).toBe('valid-id'); }); it('UuidField validates UUID format', () => { expect(() => UuidField.parse('not-a-uuid')).toThrow(); expect(UuidField.parse('550e8400-e29b-41d4-a716-446655440000')) .toBe('550e8400-e29b-41d4-a716-446655440000'); }); it('WorldIdField accepts any non-empty string', () => { expect(WorldIdField.parse('world-1')).toBe('world-1'); }); }); describe('Timestamp Fields', () => { it('DateTimeField validates ISO 8601 format', () => { expect(() => DateTimeField.parse('not-a-date')).toThrow(); expect(DateTimeField.parse('2024-01-15T12:00:00.000Z')) .toBe('2024-01-15T12:00:00.000Z'); }); it('TimestampFields can be spread into schemas', () => { const TestSchema = z.object({ id: z.string(), ...TimestampFields, }); const result = TestSchema.parse({ id: 'test-1', createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }); expect(result.createdAt).toBe('2024-01-15T12:00:00.000Z'); expect(result.updatedAt).toBe('2024-01-15T12:00:00.000Z'); }); }); describe('Coordinate Fields', () => { it('GridXField and GridYField reject negative values', () => { expect(() => GridXField.parse(-1)).toThrow(); expect(() => GridYField.parse(-5)).toThrow(); expect(GridXField.parse(0)).toBe(0); expect(GridYField.parse(100)).toBe(100); }); it('GridCoordinates validates x and y together', () => { const coords = GridCoordinates.parse({ x: 50, y: 75 }); expect(coords.x).toBe(50); expect(coords.y).toBe(75); }); it('TacticalPosition allows floating point and optional z', () => { const pos = TacticalPosition.parse({ x: 5.5, y: 10.25 }); expect(pos.x).toBe(5.5); expect(pos.y).toBe(10.25); expect(pos.z).toBeUndefined(); const pos3d = TacticalPosition.parse({ x: 5, y: 10, z: 2 }); expect(pos3d.z).toBe(2); }); it('BoundingBox validates all four corners', () => { const bbox = BoundingBox.parse({ minX: 0, maxX: 100, minY: 0, maxY: 50, }); expect(bbox.minX).toBe(0); expect(bbox.maxX).toBe(100); }); }); describe('Name and Text Fields', () => { it('NameField rejects empty and whitespace-only names', () => { expect(() => NameField.parse('')).toThrow(); expect(() => NameField.parse(' ')).toThrow(); expect(NameField.parse('Valid Name')).toBe('Valid Name'); }); it('NameField rejects names over 100 characters', () => { const longName = 'a'.repeat(101); expect(() => NameField.parse(longName)).toThrow(); }); it('ShortDescriptionField limits to 500 characters', () => { const longDesc = 'a'.repeat(501); expect(() => ShortDescriptionField.parse(longDesc)).toThrow(); expect(ShortDescriptionField.parse('Short desc')).toBe('Short desc'); }); it('LongDescriptionField requires minimum 10 characters', () => { expect(() => LongDescriptionField.parse('Too short')).toThrow(); expect(LongDescriptionField.parse('This is a sufficiently long description')).toBeDefined(); }); }); describe('Numeric Fields', () => { it('NonNegativeInt allows 0 but not negative', () => { expect(() => NonNegativeInt.parse(-1)).toThrow(); expect(NonNegativeInt.parse(0)).toBe(0); expect(NonNegativeInt.parse(100)).toBe(100); }); it('PositiveInt requires at least 1', () => { expect(() => PositiveInt.parse(0)).toThrow(); expect(PositiveInt.parse(1)).toBe(1); }); it('LevelField validates 1-20 range', () => { expect(() => LevelField.parse(0)).toThrow(); expect(() => LevelField.parse(21)).toThrow(); expect(LevelField.parse(1)).toBe(1); expect(LevelField.parse(20)).toBe(20); }); it('DCField validates 5-30 range', () => { expect(() => DCField.parse(4)).toThrow(); expect(() => DCField.parse(31)).toThrow(); expect(DCField.parse(5)).toBe(5); expect(DCField.parse(30)).toBe(30); }); it('PercentageField validates 0-1 decimal range', () => { expect(() => PercentageField.parse(-0.1)).toThrow(); expect(() => PercentageField.parse(1.1)).toThrow(); expect(PercentageField.parse(0.5)).toBe(0.5); }); it('PercentageInt validates 0-100 integer range', () => { expect(() => PercentageInt.parse(-1)).toThrow(); expect(() => PercentageInt.parse(101)).toThrow(); expect(PercentageInt.parse(50)).toBe(50); }); }); describe('D&D Ability Scores', () => { it('AbilityScoreField validates 0-30 range', () => { expect(() => AbilityScoreField.parse(-1)).toThrow(); expect(() => AbilityScoreField.parse(31)).toThrow(); expect(AbilityScoreField.parse(10)).toBe(10); }); it('AbilityScores validates all six abilities', () => { const scores = AbilityScores.parse({ str: 10, dex: 14, con: 12, int: 8, wis: 16, cha: 18, }); expect(scores.str).toBe(10); expect(scores.cha).toBe(18); }); }); describe('Enum Fields', () => { it('BaseSizeCategory validates D&D size categories', () => { expect(BaseSizeCategory.parse('medium')).toBe('medium'); expect(BaseSizeCategory.parse('huge')).toBe('huge'); expect(() => BaseSizeCategory.parse('invalid')).toThrow(); }); it('BaseDamageTypeEnum validates damage types', () => { expect(BaseDamageTypeEnum.parse('fire')).toBe('fire'); expect(BaseDamageTypeEnum.parse('necrotic')).toBe('necrotic'); expect(() => BaseDamageTypeEnum.parse('invalid')).toThrow(); }); it('BaseDamageTypeArray defaults to empty array', () => { const result = BaseDamageTypeArray.parse(undefined); expect(result).toEqual([]); const resistances = BaseDamageTypeArray.parse(['fire', 'cold']); expect(resistances).toEqual(['fire', 'cold']); }); it('ConditionTypeEnum validates conditions', () => { expect(ConditionTypeEnum.parse('stunned')).toBe('stunned'); expect(ConditionTypeEnum.parse('prone')).toBe('prone'); }); it('DiscoveryStateEnum validates discovery states', () => { expect(DiscoveryStateEnum.parse('unknown')).toBe('unknown'); expect(DiscoveryStateEnum.parse('discovered')).toBe('discovered'); }); it('DirectionEnum validates directions', () => { expect(DirectionEnum.parse('north')).toBe('north'); expect(DirectionEnum.parse('up')).toBe('up'); expect(DirectionEnum.parse('northeast')).toBe('northeast'); }); it('ExitTypeEnum validates exit types', () => { expect(ExitTypeEnum.parse('OPEN')).toBe('OPEN'); expect(ExitTypeEnum.parse('LOCKED')).toBe('LOCKED'); expect(ExitTypeEnum.parse('HIDDEN')).toBe('HIDDEN'); }); it('CoverTypeEnum validates cover types', () => { expect(CoverTypeEnum.parse('half')).toBe('half'); expect(CoverTypeEnum.parse('three_quarter')).toBe('three_quarter'); }); it('EncounterStatusEnum validates statuses', () => { expect(EncounterStatusEnum.parse('active')).toBe('active'); expect(EncounterStatusEnum.parse('completed')).toBe('completed'); }); }); describe('Currency Fields', () => { it('CurrencyFields defaults to zeros', () => { const currency = CurrencyFields.parse(undefined); expect(currency).toEqual({ gold: 0, silver: 0, copper: 0 }); }); it('CurrencyFields validates partial input', () => { const currency = CurrencyFields.parse({ gold: 100 }); expect(currency.gold).toBe(100); expect(currency.silver).toBe(0); }); }); describe('Movement Fields', () => { it('MovementSpeedField defaults to 30', () => { const speed = MovementSpeedField.parse(undefined); expect(speed).toBe(30); }); it('MovementSpeedField validates non-negative', () => { expect(() => MovementSpeedField.parse(-5)).toThrow(); expect(MovementSpeedField.parse(0)).toBe(0); }); }); describe('Helper Functions', () => { it('createEntitySchema creates schema with ID and timestamps', () => { const TestSchema = createEntitySchema({ name: z.string(), value: z.number(), }); const result = TestSchema.parse({ id: 'test-1', name: 'Test', value: 42, createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }); expect(result.id).toBe('test-1'); expect(result.name).toBe('Test'); expect(result.createdAt).toBeDefined(); }); it('createUuidEntitySchema requires valid UUID', () => { const TestSchema = createUuidEntitySchema({ name: z.string(), }); expect(() => TestSchema.parse({ id: 'not-a-uuid', name: 'Test', createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', })).toThrow(); const result = TestSchema.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Test', createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }); expect(result.id).toBe('550e8400-e29b-41d4-a716-446655440000'); }); it('createWorldEntitySchema includes worldId and coordinates', () => { const TestSchema = createWorldEntitySchema({ name: z.string(), type: z.string(), }); const result = TestSchema.parse({ id: 'struct-1', worldId: 'world-1', x: 50, y: 75, name: 'Test City', type: 'city', createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }); expect(result.worldId).toBe('world-1'); expect(result.x).toBe(50); expect(result.y).toBe(75); expect(result.regionId).toBeUndefined(); }); it('createWorldEntitySchema allows optional regionId', () => { const TestSchema = createWorldEntitySchema({ name: z.string(), }); const result = TestSchema.parse({ id: 'struct-1', worldId: 'world-1', regionId: 'region-5', x: 50, y: 75, name: 'Test', createdAt: '2024-01-15T12:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }); expect(result.regionId).toBe('region-5'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server