| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Effect, Schema, SchemaAST, SchemaRepresentation } from "effect" |
| 3 | |
| 4 | describe("SchemaRepresentation.toRepresentations", () => { |
| 5 | describe("root identity and sharing", () => { |
| 6 | it("preserves root order", () => { |
| 7 | const document = SchemaRepresentation.toRepresentations([ |
| 8 | Schema.String.ast, |
| 9 | Schema.Number.ast, |
| 10 | Schema.Boolean.ast |
| 11 | ]) |
| 12 | |
| 13 | assert.deepStrictEqual(document, { |
| 14 | representations: [ |
| 15 | { _tag: "String", checks: [] }, |
| 16 | { _tag: "Number", checks: [] }, |
| 17 | { _tag: "Boolean", checks: [] } |
| 18 | ], |
| 19 | references: {} |
| 20 | }) |
| 21 | }) |
| 22 | |
| 23 | it("shares a named reference between roots", () => { |
| 24 | const shared = Schema.String.annotate({ identifier: "Shared" }) |
| 25 | const document = SchemaRepresentation.toRepresentations([shared.ast, shared.ast]) |
| 26 | |
| 27 | assert.deepStrictEqual(document, { |
| 28 | representations: [ |
| 29 | { _tag: "Reference", $ref: "Shared" }, |
| 30 | { _tag: "Reference", $ref: "Shared" } |
| 31 | ], |
| 32 | references: { |
| 33 | Shared: { |
| 34 | _tag: "String", |
| 35 | annotations: { identifier: "Shared" }, |
| 36 | checks: [] |
| 37 | } |
| 38 | } |
| 39 | }) |
| 40 | }) |
| 41 | |
| 42 | it("shares a fallback reference across contextual copies of the same encoded AST", () => { |
| 43 | const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" }) |
| 44 | const first = Schema.toCodecJson(Schema.fromJsonString(Content)) |
| 45 | const second = Schema.toCodecJson(Schema.fromJsonString(Content)) |
| 46 | const document = SchemaRepresentation.toRepresentations([first.ast, second.ast]) |
| 47 | |
| 48 | assert.notStrictEqual(first.ast, second.ast) |
| 49 | assert.deepStrictEqual(document, { |
| 50 | representations: [ |
| 51 | { _tag: "Reference", $ref: "Tool.ContentEncoded" }, |
| 52 | { _tag: "Reference", $ref: "Tool.ContentEncoded" } |
| 53 | ], |
| 54 | references: { |
| 55 | "Tool.ContentEncoded": { |
| 56 | _tag: "String", |
| 57 | annotations: { |
| 58 | expected: "a string that will be decoded as JSON", |
| 59 | contentMediaType: "application/json", |
| 60 | "~identifier": "Tool.Content" |
| 61 | }, |
| 62 | checks: [] |
| 63 | } |
| 64 | } |
| 65 | }) |
| 66 | }) |
| 67 | |
| 68 | it("shares copies of the same AST with the same Context", () => { |
| 69 | const ast = Schema.String.annotate({ identifier: "Value" }).ast |
| 70 | const context = new SchemaAST.Context(false, false) |
| 71 | const first = SchemaAST.replaceContext(ast, context) |
| 72 | const second = SchemaAST.replaceContext(ast, context) |
| 73 | |
| 74 | assert.notStrictEqual(first, second) |
| 75 | assert.deepStrictEqual(SchemaRepresentation.toRepresentations([first, second]), { |
| 76 | representations: [ |
| 77 | { _tag: "Reference", $ref: "Value" }, |
| 78 | { _tag: "Reference", $ref: "Value" } |
| 79 | ], |
| 80 | references: { |
| 81 | Value: { |
| 82 | _tag: "String", |
| 83 | annotations: { identifier: "Value" }, |
| 84 | checks: [] |
| 85 | } |
| 86 | } |
| 87 | }) |
| 88 | }) |
| 89 | |
| 90 | it("preserves the original owner through repeated Context replacements", () => { |
| 91 | const ast = Schema.String.annotate({ identifier: "Value" }).ast |
| 92 | const first = SchemaAST.replaceContext(ast, new SchemaAST.Context(true, false)) |
| 93 | const chained = SchemaAST.replaceContext(first, new SchemaAST.Context(false, true)) |
| 94 | const direct = SchemaAST.replaceContext(ast, new SchemaAST.Context(false, true)) |
| 95 | |
| 96 | assert.strictEqual(SchemaAST.getContextOwner(first), ast) |
| 97 | assert.strictEqual(SchemaAST.getContextOwner(chained), ast) |
| 98 | assert.deepStrictEqual(SchemaRepresentation.toRepresentations([chained, direct]), { |
| 99 | representations: [ |
| 100 | { _tag: "Reference", $ref: "Value" }, |
| 101 | { _tag: "Reference", $ref: "Value" } |
| 102 | ], |
| 103 | references: { |
| 104 | Value: { |
| 105 | _tag: "String", |
| 106 | annotations: { identifier: "Value" }, |
| 107 | checks: [] |
| 108 | } |
| 109 | } |
| 110 | }) |
| 111 | }) |
| 112 | |
| 113 | it("keeps a checked derivative distinct from its identified source", () => { |
| 114 | const base = Schema.String.annotate({ identifier: "Text" }) |
| 115 | const refined = base.pipe(Schema.check(Schema.isMinLength(1))) |
| 116 | const forward = SchemaRepresentation.toRepresentations([base.ast, refined.ast]) |
| 117 | const reversed = SchemaRepresentation.toRepresentations([refined.ast, base.ast]) |
| 118 | |
| 119 | assert.deepStrictEqual(forward.representations[0], { _tag: "Reference", $ref: "Text" }) |
| 120 | assert.strictEqual(forward.representations[1]._tag, "String") |
| 121 | if (forward.representations[1]._tag === "String") { |
| 122 | assert.strictEqual(forward.representations[1].checks.length, 1) |
| 123 | } |
| 124 | assert.strictEqual(reversed.representations[0]._tag, "String") |
| 125 | if (reversed.representations[0]._tag === "String") { |
| 126 | assert.strictEqual(reversed.representations[0].checks.length, 1) |
| 127 | } |
| 128 | assert.deepStrictEqual(reversed.representations[1], { _tag: "Reference", $ref: "Text" }) |
| 129 | assert.deepStrictEqual(forward.references, { |
| 130 | Text: { |
| 131 | _tag: "String", |
| 132 | annotations: { identifier: "Text" }, |
| 133 | checks: [] |
| 134 | } |
| 135 | }) |
| 136 | assert.deepStrictEqual(reversed.references, forward.references) |
| 137 | }) |
| 138 | }) |
| 139 | |
| 140 | describe.each([ |
| 141 | { name: "toType", project: SchemaAST.toType }, |
| 142 | { name: "toEncoded", project: SchemaAST.toEncoded }, |
| 143 | { name: "flip", project: SchemaAST.flip } |
| 144 | ])("contextual projections with $name", ({ project }) => { |
| 145 | it.each([ |
| 146 | { |
| 147 | name: "primitive", |
| 148 | schema: Schema.NumberFromString.pipe(Schema.annotateEncoded({ identifier: "Wire" })) |
| 149 | }, |
| 150 | { name: "struct", schema: Schema.Struct({ value: Schema.NumberFromString }) }, |
| 151 | { name: "array", schema: Schema.Array(Schema.NumberFromString) }, |
| 152 | { name: "tuple", schema: Schema.Tuple([Schema.NumberFromString, Schema.String]) }, |
| 153 | { name: "union", schema: Schema.Union([Schema.NumberFromString, Schema.Boolean]) }, |
| 154 | { name: "declaration", schema: Schema.Option(Schema.NumberFromString) }, |
| 155 | { name: "suspend", schema: Schema.suspend(() => Schema.Struct({ value: Schema.NumberFromString })) }, |
| 156 | { |
| 157 | name: "encoding chain", |
| 158 | schema: Schema.String.pipe( |
| 159 | Schema.decodeTo(Schema.NumberFromString), |
| 160 | Schema.annotateEncoded({ identifier: "Wire" }) |
| 161 | ) |
| 162 | } |
| 163 | ])("shares the $name reference while preserving each field's context", ({ schema }) => { |
| 164 | const value = schema.annotate({ identifier: "Value" }) |
| 165 | const struct = Schema.Struct({ |
| 166 | optional: Schema.optionalKey(value), |
| 167 | mutable: Schema.mutableKey(value), |
| 168 | both: Schema.optionalKey(Schema.mutableKey(value)), |
| 169 | required: value |
| 170 | }) |
| 171 | const expected = SchemaRepresentation.toRepresentation(project(value.ast)) |
| 172 | const document = SchemaRepresentation.toRepresentations([project(struct.ast), project(value.ast)]) |
| 173 | |
| 174 | assert.deepStrictEqual(document.references, expected.references) |
| 175 | assert.deepStrictEqual(document.representations, [ |
| 176 | { |
| 177 | _tag: "Objects", |
| 178 | propertySignatures: [ |
| 179 | { name: "optional", type: expected.representation, isOptional: true, isMutable: false }, |
| 180 | { name: "mutable", type: expected.representation, isOptional: false, isMutable: true }, |
| 181 | { name: "both", type: expected.representation, isOptional: true, isMutable: true }, |
| 182 | { name: "required", type: expected.representation, isOptional: false, isMutable: false } |
| 183 | ], |
| 184 | indexSignatures: [], |
| 185 | checks: [] |
| 186 | }, |
| 187 | expected.representation |
| 188 | ]) |
| 189 | }) |
| 190 | |
| 191 | it("shares recursive references across contextual copies", () => { |
| 192 | const node: SchemaAST.Suspend = new SchemaAST.Suspend( |
| 193 | () => |
| 194 | new SchemaAST.Objects([ |
| 195 | new SchemaAST.PropertySignature("value", Schema.NumberFromString.ast), |
| 196 | new SchemaAST.PropertySignature("next", SchemaAST.optionalKey(node)) |
| 197 | ], []), |
| 198 | { identifier: "Node" } |
| 199 | ) |
| 200 | const document = SchemaRepresentation.toRepresentations([ |
| 201 | project(SchemaAST.mutableKey(node)), |
| 202 | project(node) |
| 203 | ]) |
| 204 | |
| 205 | assert.deepStrictEqual(document.representations, [ |
| 206 | { _tag: "Reference", $ref: "Node" }, |
| 207 | { _tag: "Reference", $ref: "Node" } |
| 208 | ]) |
| 209 | assert.deepStrictEqual(Object.keys(document.references), ["Node"]) |
| 210 | const representation = document.references.Node |
| 211 | assert.strictEqual(representation._tag, "Suspend") |
| 212 | if (representation._tag !== "Suspend") return |
| 213 | assert.strictEqual(representation.thunk._tag, "Objects") |
| 214 | if (representation.thunk._tag !== "Objects") return |
| 215 | assert.deepStrictEqual(representation.thunk.propertySignatures[1], { |
| 216 | name: "next", |
| 217 | type: { _tag: "Reference", $ref: "Node" }, |
| 218 | isOptional: true, |
| 219 | isMutable: false |
| 220 | }) |
| 221 | }) |
| 222 | }) |
| 223 | |
| 224 | it("shares type references without losing key annotations or constructor defaults", () => { |
| 225 | const value = Schema.NumberFromString.annotate({ identifier: "Value" }) |
| 226 | const schema = Schema.toType(Schema.Struct({ |
| 227 | annotated: value.annotateKey({ description: "field" }), |
| 228 | defaulted: value.pipe(Schema.withConstructorDefault(Effect.succeed(1))), |
| 229 | required: value |
| 230 | })) |
| 231 | const document = Schema.toRepresentation(schema) |
| 232 | |
| 233 | assert.deepStrictEqual(Object.keys(document.references), ["Value"]) |
| 234 | assert.strictEqual(document.representation._tag, "Objects") |
| 235 | if (document.representation._tag !== "Objects") return |
| 236 | assert.deepStrictEqual(document.representation.propertySignatures[0], { |
| 237 | name: "annotated", |
| 238 | type: { _tag: "Reference", $ref: "Value" }, |
| 239 | isOptional: false, |
| 240 | isMutable: false, |
| 241 | annotations: { description: "field" } |
| 242 | }) |
| 243 | assert.deepStrictEqual(schema.make({ annotated: 2, required: 3 }), { annotated: 2, defaulted: 1, required: 3 }) |
| 244 | }) |
| 245 | |
| 246 | it("keeps projected checks and value annotations distinct from the source", () => { |
| 247 | const value = Schema.optionalKey(Schema.NumberFromString).annotate({ identifier: "Value" }) |
| 248 | const checked = value.check(Schema.isGreaterThan(0)) |
| 249 | const annotated = value.annotate({ description: "another value" }) |
| 250 | const document = SchemaRepresentation.toRepresentations([ |
| 251 | SchemaAST.toType(value.ast), |
| 252 | SchemaAST.toType(checked.ast), |
| 253 | SchemaAST.toType(annotated.ast) |
| 254 | ]) |
| 255 | |
| 256 | assert.deepStrictEqual(document.representations[0], { _tag: "Reference", $ref: "Value" }) |
| 257 | assert.strictEqual(document.representations[1]._tag, "Number") |
| 258 | if (document.representations[1]._tag !== "Number") return |
| 259 | assert.strictEqual(document.representations[1].checks.length, 1) |
| 260 | assert.deepStrictEqual(document.representations[2], { _tag: "Reference", $ref: "Value_1" }) |
| 261 | assert.deepStrictEqual(document.references.Value, { |
| 262 | _tag: "Number", |
| 263 | annotations: { identifier: "Value" }, |
| 264 | checks: [] |
| 265 | }) |
| 266 | assert.deepStrictEqual(document.references.Value_1, { |
| 267 | _tag: "Number", |
| 268 | annotations: { identifier: "Value_1", description: "another value" }, |
| 269 | checks: [] |
| 270 | }) |
| 271 | }) |
| 272 | |
| 273 | describe("reference policies", () => { |
| 274 | it("exposes the projected body and counts contextual copies together", () => { |
| 275 | const value = Schema.NumberFromString.annotate({ identifier: "Value" }) |
| 276 | const inputs: Array<SchemaRepresentation.ReferencePolicyInput> = [] |
| 277 | const document = SchemaRepresentation.toRepresentations([ |
| 278 | SchemaAST.toType(Schema.optionalKey(value).ast), |
| 279 | SchemaAST.toType(value.ast) |
| 280 | ], { |
| 281 | referencePolicy: (input) => { |
| 282 | inputs.push(input) |
| 283 | return SchemaAST.getLastEncoding(input.ast)._tag |
| 284 | } |
| 285 | }) |
| 286 | |
| 287 | assert.strictEqual(inputs.length, 1) |
| 288 | assert.strictEqual(inputs[0].occurrences, 2) |
| 289 | assert.strictEqual(inputs[0].ast.encoding, undefined) |
| 290 | assert.deepStrictEqual(document, { |
| 291 | representations: [ |
| 292 | { _tag: "Reference", $ref: "Number" }, |
| 293 | { _tag: "Reference", $ref: "Number" } |
| 294 | ], |
| 295 | references: { |
| 296 | Number: { _tag: "Number", annotations: { identifier: "Number" }, checks: [] } |
| 297 | } |
| 298 | }) |
| 299 | }) |
| 300 | |
| 301 | it("supports policies based on occurrence counts", () => { |
| 302 | const shared = Schema.Struct({ value: Schema.String }) |
| 303 | const equivalent = Schema.Struct({ value: Schema.String }) |
| 304 | const referencePolicy: SchemaRepresentation.ReferencePolicy = ({ ast, occurrences }) => |
| 305 | occurrences > 1 ? `${ast._tag}_` : undefined |
| 306 | |
| 307 | const single = SchemaRepresentation.toRepresentations([shared.ast], { referencePolicy }) |
| 308 | const distinct = SchemaRepresentation.toRepresentations([shared.ast, equivalent.ast], { |
| 309 | referencePolicy: ({ ast, occurrences }) => |
| 310 | ast._tag === "Objects" && occurrences > 1 ? `${ast._tag}_` : undefined |
| 311 | }) |
| 312 | const repeated = SchemaRepresentation.toRepresentations([shared.ast, shared.ast], { referencePolicy }) |
| 313 | |
| 314 | assert.deepStrictEqual(single.references, {}) |
| 315 | assert.deepStrictEqual(distinct.references, {}) |
| 316 | assert.deepStrictEqual(repeated.representations, [ |
| 317 | { _tag: "Reference", $ref: "Objects_" }, |
| 318 | { _tag: "Reference", $ref: "Objects_" } |
| 319 | ]) |
| 320 | assert.deepStrictEqual(Object.keys(repeated.references), ["Objects_"]) |
| 321 | }) |
| 322 | |
| 323 | it("suffixes policy name collisions", () => { |
| 324 | const first = Schema.Struct({ first: Schema.String }) |
| 325 | const second = Schema.Struct({ second: Schema.String }) |
| 326 | const document = SchemaRepresentation.toRepresentations([first.ast, second.ast], { |
| 327 | referencePolicy: ({ ast }) => ast._tag === "Objects" ? "Model" : undefined |
| 328 | }) |
| 329 | |
| 330 | assert.deepStrictEqual(document.representations, [ |
| 331 | { _tag: "Reference", $ref: "Model" }, |
| 332 | { _tag: "Reference", $ref: "Model_1" } |
| 333 | ]) |
| 334 | assert.deepStrictEqual(Object.keys(document.references), ["Model", "Model_1"]) |
| 335 | }) |
| 336 | |
| 337 | it("distinguishes identifiers that share the same encoded AST owner", () => { |
| 338 | const first = Schema.NumberFromString.annotate({ identifier: "First" }) |
| 339 | const second = Schema.NumberFromString.annotate({ identifier: "Second" }) |
| 340 | const inputs: Array<SchemaRepresentation.ReferencePolicyInput> = [] |
| 341 | const document = SchemaRepresentation.toRepresentations([first.ast, second.ast], { |
| 342 | referencePolicy: (input) => { |
| 343 | inputs.push(input) |
| 344 | return input.identifier |
| 345 | } |
| 346 | }) |
| 347 | |
| 348 | assert.deepStrictEqual(document.representations, [ |
| 349 | { _tag: "Reference", $ref: "FirstEncoded" }, |
| 350 | { _tag: "Reference", $ref: "SecondEncoded" } |
| 351 | ]) |
| 352 | assert.deepStrictEqual(Object.keys(document.references), ["FirstEncoded", "SecondEncoded"]) |
| 353 | assert.deepStrictEqual(inputs.map(({ identifier, occurrences }) => ({ identifier, occurrences })), [ |
| 354 | { identifier: "FirstEncoded", occurrences: 1 }, |
| 355 | { identifier: "SecondEncoded", occurrences: 1 } |
| 356 | ]) |
| 357 | assert.strictEqual(inputs[0].ast, inputs[1].ast) |
| 358 | }) |
| 359 | }) |
| 360 | |
| 361 | describe("identifier collisions", () => { |
| 362 | it("suffixes different schemas with the same identifier", () => { |
| 363 | const first = Schema.String.annotate({ identifier: "Value", description: "first" }) |
| 364 | const second = Schema.Number.annotate({ identifier: "Value", description: "second" }) |
| 365 | |
| 366 | assert.deepStrictEqual( |
| 367 | SchemaRepresentation.toRepresentations([first.ast, second.ast]), |
| 368 | { |
| 369 | representations: [ |
| 370 | { _tag: "Reference", $ref: "Value" }, |
| 371 | { _tag: "Reference", $ref: "Value_1" } |
| 372 | ], |
| 373 | references: { |
| 374 | Value: { |
| 375 | _tag: "String", |
| 376 | annotations: { identifier: "Value", description: "first" }, |
| 377 | checks: [] |
| 378 | }, |
| 379 | Value_1: { |
| 380 | _tag: "Number", |
| 381 | annotations: { identifier: "Value_1", description: "second" }, |
| 382 | checks: [] |
| 383 | } |
| 384 | } |
| 385 | } |
| 386 | ) |
| 387 | }) |
| 388 | |
| 389 | it("suffixes referentially distinct ASTs with equal representations", () => { |
| 390 | const first = Schema.String.annotate({ identifier: "Value" }) |
| 391 | const second = Schema.String.annotate({ identifier: "Value" }) |
| 392 | |
| 393 | assert.deepStrictEqual( |
| 394 | SchemaRepresentation.toRepresentations([first.ast, second.ast]), |
| 395 | { |
| 396 | representations: [ |
| 397 | { _tag: "Reference", $ref: "Value" }, |
| 398 | { _tag: "Reference", $ref: "Value_1" } |
| 399 | ], |
| 400 | references: { |
| 401 | Value: { |
| 402 | _tag: "String", |
| 403 | annotations: { identifier: "Value" }, |
| 404 | checks: [] |
| 405 | }, |
| 406 | Value_1: { |
| 407 | _tag: "String", |
| 408 | annotations: { identifier: "Value_1" }, |
| 409 | checks: [] |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | ) |
| 414 | }) |
| 415 | |
| 416 | it("suffixes fallback and explicit identifier collisions in encounter order", () => { |
| 417 | const first = Schema.String.annotate({ "~identifier": "Person" }) |
| 418 | const second = Schema.Number.annotate({ "~identifier": "Person" }) |
| 419 | const explicit = Schema.Boolean.annotate({ identifier: "PersonEncoded" }) |
| 420 | |
| 421 | assert.deepStrictEqual( |
| 422 | SchemaRepresentation.toRepresentations([first.ast, second.ast, explicit.ast]), |
| 423 | { |
| 424 | representations: [ |
| 425 | { _tag: "Reference", $ref: "PersonEncoded" }, |
| 426 | { _tag: "Reference", $ref: "PersonEncoded_1" }, |
| 427 | { _tag: "Reference", $ref: "PersonEncoded_2" } |
| 428 | ], |
| 429 | references: { |
| 430 | PersonEncoded: { |
| 431 | _tag: "String", |
| 432 | annotations: { "~identifier": "Person" }, |
| 433 | checks: [] |
| 434 | }, |
| 435 | PersonEncoded_1: { |
| 436 | _tag: "Number", |
| 437 | annotations: { "~identifier": "Person" }, |
| 438 | checks: [] |
| 439 | }, |
| 440 | PersonEncoded_2: { |
| 441 | _tag: "Boolean", |
| 442 | annotations: { identifier: "PersonEncoded_2" }, |
| 443 | checks: [] |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | ) |
| 448 | }) |
| 449 | }) |
| 450 | }) |