import { it, describe, expect } from 'vitest'; const itEffect = (name: string, fn: () => Effect.Effect) => it(name, () => Effect.runPromise(fn())); import { Effect, Schema } from 'effect'; import { ESchema, fromType } from '../index.js'; import { ESchemaError } from '../index.js'; describe('ESchema', () => { describe('Plain schema', () => { describe('Make', () => { itEffect('creates a v1 schema and encodes with version', () => Effect.gen(function* () { const schema = ESchema.make('User', { name: Schema.String, }).build(); const encoded = yield* schema.encode({ name: 'Alice' }); expect(encoded).toEqual({ _v: 'v1', name: 'Alice' }); }), ); itEffect('decodes v1 data', () => Effect.gen(function* () { const schema = ESchema.make('Counter', { name: Schema.String, count: Schema.Number, }).build(); const decoded = yield* schema.decode({ _v: 'v1', name: 'foo', count: 10, }); expect(decoded).toEqual({ name: 'foo', count: 10 }); }), ); itEffect('defaults to latest version when _v is missing', () => Effect.gen(function* () { const schema = ESchema.make('Doc', { a: Schema.String }).build(); const decoded = yield* schema.decode({ a: 'hello' }); expect(decoded).toEqual({ a: 'hello' }); }), ); itEffect('fails on unknown version', () => Effect.gen(function* () { const schema = ESchema.make('Doc', { a: Schema.String }).build(); const error = yield* Effect.flip( schema.decode({ _v: 'v99', a: 'hello' }), ); expect(error).toBeInstanceOf(ESchemaError); expect(error.message).toBe('Unknown schema version: v99'); }), ); }); describe('Evolve', () => { itEffect('migrates v1 → v2', () => Effect.gen(function* () { const schema = ESchema.make('Item', { a: Schema.String }) .evolve('v2', { b: Schema.Number }, (v) => ({ ...v, b: 42 })) .build(); const decoded = yield* schema.decode({ _v: 'v1', a: 'hello' }); expect(decoded).toEqual({ a: 'hello', b: 42 }); }), ); itEffect('chains v1 → v2 → v3', () => Effect.gen(function* () { const schema = ESchema.make('Item', { a: Schema.String }) .evolve('v2', { b: Schema.String }, (v) => ({ ...v, b: 'added' })) .evolve('v3', { c: Schema.Number }, (v) => ({ ...v, c: 0 })) .build(); const fromV1 = yield* schema.decode({ _v: 'v1', a: 'x' }); expect(fromV1).toEqual({ a: 'x', b: 'added', c: 0 }); const fromV2 = yield* schema.decode({ _v: 'v2', a: 'x', b: 'y' }); expect(fromV2).toEqual({ a: 'x', b: 'y', c: 0 }); const fromV3 = yield* schema.decode({ _v: 'v3', a: 'x', b: 'y', c: 9, }); expect(fromV3).toEqual({ a: 'x', b: 'y', c: 9 }); }), ); itEffect('handles field removal', () => Effect.gen(function* () { const schema = ESchema.make('Item', { a: Schema.String, b: Schema.String, }) .evolve('v2', { b: null }, (v) => ({ a: v.a })) .build(); const decoded = yield* schema.decode({ _v: 'v1', a: 'keep', b: 'drop', }); expect(decoded).toEqual({ a: 'keep' }); }), ); }); describe('Views', () => { it('fields returns the latest schema fields', () => { const schema = ESchema.make('Pair', { a: Schema.String, b: Schema.Number, }).build(); expect(Object.keys(schema.fields).sort()).toEqual(['a', 'b']); }); it('schema returns an Effect Schema.Struct', () => { const schema = ESchema.make('Doc', { a: Schema.String }).build(); expect(Object.keys(schema.schema.fields)).toEqual(['a']); }); it('makePartial attaches version', () => { const schema = ESchema.make('Pair', { a: Schema.String, b: Schema.Number, }).build(); expect(schema.makePartial({ a: 'hi' })).toEqual({ a: 'hi', _v: 'v1' }); expect(schema.makePartial({})).toEqual({ _v: 'v1' }); }); }); describe('Roundtrip', () => { itEffect('encode → decode preserves data', () => Effect.gen(function* () { const schema = ESchema.make('Counter', { name: Schema.String, count: Schema.Number, }).build(); const encoded = yield* schema.encode({ name: 'test', count: 42 }); const decoded = yield* schema.decode(encoded); expect(decoded).toEqual({ name: 'test', count: 42 }); }), ); }); describe('Get descriptor', () => { it('returns JSON Schema with version literal', () => { const schema = ESchema.make('Doc', { a: Schema.String }).build(); const descriptor = schema.getDescriptor(); expect(descriptor.type).toBe('object'); expect(descriptor.properties).toHaveProperty('a'); expect(descriptor.properties).toHaveProperty('_v'); const vSchema = descriptor.properties._v as { enum?: string[] }; expect(vSchema.enum).toEqual(['v1']); }); }); describe('From type', () => { itEffect('passes unknown values through as the requested type', () => Effect.gen(function* () { type ExternalShape = { readonly expression: readonly [string, readonly unknown[]]; }; const ExternalShapeSchema = fromType(); const input = { expression: ['val', ['path']] as const }; const decoded = yield* Schema.decodeUnknownEffect(ExternalShapeSchema)(input); const encoded = yield* Schema.encodeEffect(ExternalShapeSchema)(decoded); expect(decoded.expression[0]).toBe('val'); expect(encoded).toBe(input); }), ); }); }); });