persistence-schema-model.spec.ts 5.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import { describe, expect, it } from 'vitest'
  2. import { canonicalizeSchema, isArbitraryJsonSchema, schemaDigest, type SchemaNode } from './persistence-schema-model.ts'
  3. const string: SchemaNode = { kind: 'primitive', type: 'string' }
  4. const number: SchemaNode = { kind: 'primitive', type: 'number' }
  5. describe('canonical persisted type graphs', () => {
  6. it('ignores source ordering and shared versus duplicated subtypes', () => {
  7. const left: SchemaNode[] = [
  8. { kind: 'object', properties: [{ name: 'b', type: 1, optional: false }, { name: 'a', type: 1, optional: true }], indices: [] }, string,
  9. ]
  10. const right: SchemaNode[] = [
  11. string, string,
  12. { kind: 'object', properties: [{ name: 'a', type: 0, optional: true }, { name: 'b', type: 1, optional: false }], indices: [] },
  13. ]
  14. expect(canonicalizeSchema(left, 0)).toEqual(canonicalizeSchema(right, 2))
  15. })
  16. it('minimizes differently factored recursive aliases to the same graph', () => {
  17. const self: SchemaNode[] = [
  18. { kind: 'object', properties: [{ name: 'next', type: 0, optional: true }, { name: 'value', type: 1, optional: false }], indices: [] }, string,
  19. ]
  20. const mutual: SchemaNode[] = [
  21. { kind: 'object', properties: [{ name: 'value', type: 2, optional: false }, { name: 'next', type: 1, optional: true }], indices: [] },
  22. { kind: 'object', properties: [{ name: 'next', type: 0, optional: true }, { name: 'value', type: 3, optional: false }], indices: [] }, string, string,
  23. ]
  24. const normalized = canonicalizeSchema(self, 0)
  25. expect(canonicalizeSchema(mutual, 0)).toEqual(normalized)
  26. expect(canonicalizeSchema(normalized.nodes, 0)).toEqual(normalized)
  27. mutual[3] = number
  28. expect(schemaDigest(canonicalizeSchema(mutual, 0))).not.toBe(schemaDigest(normalized))
  29. })
  30. it('flattens reordered unions and removes structurally equal alternatives', () => {
  31. const flat: SchemaNode[] = [{ kind: 'union', types: [1, 2] }, string, number]
  32. const factored: SchemaNode[] = [string, number, { kind: 'union', types: [0, 3] }, { kind: 'union', types: [1, 0] }]
  33. expect(canonicalizeSchema(flat, 0)).toEqual(canonicalizeSchema(factored, 2))
  34. expect(canonicalizeSchema([{ kind: 'union', types: [1, 2] }, string, string], 0)).toEqual(canonicalizeSchema([string], 0))
  35. expect(canonicalizeSchema([{ kind: 'union', types: [1, 2] }, { kind: 'literal', value: true }, { kind: 'literal', value: false }], 0))
  36. .toEqual(canonicalizeSchema([{ kind: 'primitive', type: 'boolean' }], 0))
  37. })
  38. it('ignores recursive union, field, index signature and graph numbering order together', () => {
  39. const original: SchemaNode[] = [
  40. { kind: 'union', types: [1, 2, 3] },
  41. { kind: 'object', properties: [{ name: 'label', type: 5, optional: false }, { name: 'next', type: 0, optional: true }], indices: [] },
  42. { kind: 'array', element: 0 },
  43. { kind: 'object', properties: [], indices: [{ key: 5, value: 0 }, { key: 4, value: 1 }] },
  44. number,
  45. string,
  46. ]
  47. const reordered: SchemaNode[] = [
  48. string,
  49. { kind: 'object', properties: [], indices: [{ key: 3, value: 5 }, { key: 0, value: 4 }] },
  50. { kind: 'array', element: 4 },
  51. number,
  52. { kind: 'union', types: [1, 2, 5] },
  53. { kind: 'object', properties: [{ name: 'next', type: 4, optional: true }, { name: 'label', type: 0, optional: false }], indices: [] },
  54. ]
  55. expect(canonicalizeSchema(reordered, 4)).toEqual(canonicalizeSchema(original, 0))
  56. })
  57. it('preserves tuple positions, element absence, and rest elements', () => {
  58. const baseline: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 1, optional: false, rest: false }, { type: 2, optional: true, rest: false }] }, string, number]
  59. const reversed: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 2, optional: false, rest: false }, { type: 1, optional: true, rest: false }] }, string, number]
  60. const rest: SchemaNode[] = [{ kind: 'tuple', elements: [{ type: 1, optional: false, rest: false }, { type: 2, optional: false, rest: true }] }, string, number]
  61. const digest = schemaDigest(canonicalizeSchema(baseline, 0))
  62. expect(schemaDigest(canonicalizeSchema(reversed, 0))).not.toBe(digest)
  63. expect(schemaDigest(canonicalizeSchema(rest, 0))).not.toBe(digest)
  64. })
  65. it('recognizes arbitrary recursive JSON structurally and retains its definition', () => {
  66. const json: SchemaNode[] = [
  67. { kind: 'union', types: [1, 2, 3, 4, 5, 6] },
  68. { kind: 'primitive', type: 'null' }, { kind: 'primitive', type: 'boolean' }, number, string,
  69. { kind: 'array', element: 0 }, { kind: 'object', properties: [], indices: [{ key: 4, value: 0 }] },
  70. ]
  71. const schema = canonicalizeSchema(json, 0)
  72. expect(isArbitraryJsonSchema(schema)).toBe(true)
  73. expect(schema.nodes.some(node => node.kind === 'array' && node.element === 0)).toBe(true)
  74. json[0] = { kind: 'union', types: [1, 2, 3, 4, 5] }
  75. expect(isArbitraryJsonSchema(canonicalizeSchema(json, 0))).toBe(false)
  76. })
  77. it('rejects missing references and unproductive union cycles', () => {
  78. expect(() => canonicalizeSchema([{ kind: 'array', element: 5 }], 0)).toThrow('missing node 5')
  79. expect(() => canonicalizeSchema([{ kind: 'union', types: [0] }], 0)).toThrow('union cycle')
  80. })
  81. })