@jbrowse/mobx-state-tree 5.9.2 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2699,16 +2699,27 @@ function typecheck(type, value) {
2699
2699
  }
2700
2700
  }
2701
2701
  const MAX_ERRORS_REPORTED = 10;
2702
- function validationErrorsToString(type, value, errors) {
2703
- if (errors.length === 0) {
2704
- return undefined;
2705
- }
2702
+ /**
2703
+ * @internal
2704
+ * @hidden
2705
+ * Format a list of validation errors as bullet-style lines, capped at
2706
+ * MAX_ERRORS_REPORTED entries with an overflow suffix. Shared by typecheck()
2707
+ * and union's `noMatchMessage` so prod-build union failures can include
2708
+ * candidate validation errors using the same formatting.
2709
+ */
2710
+ function formatValidationErrorLines(errors) {
2706
2711
  const shown = errors.slice(0, MAX_ERRORS_REPORTED).map(toErrorString);
2707
2712
  const overflow = errors.length - shown.length;
2708
2713
  if (overflow > 0) {
2709
2714
  shown.push(`(… and ${overflow} more error${overflow === 1 ? "" : "s"})`);
2710
2715
  }
2711
- return (`Error while converting ${shortenPrintValue(prettyPrintValue(value))} to \`${type.name}\`:\n\n ` + shown.join("\n "));
2716
+ return shown;
2717
+ }
2718
+ function validationErrorsToString(type, value, errors) {
2719
+ if (errors.length === 0) {
2720
+ return undefined;
2721
+ }
2722
+ return (`Error while converting ${shortenPrintValue(prettyPrintValue(value))} to \`${type.name}\`:\n\n ` + formatValidationErrorLines(errors).join("\n "));
2712
2723
  }
2713
2724
 
2714
2725
  let identifierCacheId = 0;
@@ -5389,6 +5400,25 @@ function enumeration(name, options) {
5389
5400
  return type;
5390
5401
  }
5391
5402
 
5403
+ // Drill through single-subtype wrappers — optional(), refinement(),
5404
+ // snapshotProcessor(), late() — to the underlying ModelType. Discriminated-
5405
+ // union scoping keys on a member's literal `type` property, but real-world
5406
+ // members are rarely bare models (jbrowse config schemas, for instance, are
5407
+ // always optional(model) or optional(snapshotProcessor(model))). Without this
5408
+ // the scoping never engages and every failure prints every member's full
5409
+ // structure. Wrappers expose their child as `_subtype` (optional/refinement/
5410
+ // snapshotProcessor) or via `getSubType()` (late); bounded to avoid cycles.
5411
+ function resolveModelType(type) {
5412
+ let current = type;
5413
+ for (let depth = 0; current && depth < 20; depth++) {
5414
+ if (current instanceof ModelType) {
5415
+ return current;
5416
+ }
5417
+ const wrapper = current;
5418
+ current = wrapper._subtype ?? wrapper.getSubType?.(false);
5419
+ }
5420
+ return undefined;
5421
+ }
5392
5422
  /**
5393
5423
  * @internal
5394
5424
  * @hidden
@@ -5438,10 +5468,74 @@ class Union extends BaseType {
5438
5468
  return type.reconcile(current, newValue, parent, subpath);
5439
5469
  }
5440
5470
  noMatchMessage(value) {
5441
- const discriminator = isPlainObject(value) && typeof value.type === "string"
5442
- ? ` for snapshot with type "${value.type}"`
5443
- : "";
5444
- return `No matching type for union ${this.name}${discriminator}`;
5471
+ const base = `No matching type for union ${this.name}`;
5472
+ if (!isPlainObject(value)) {
5473
+ return base;
5474
+ }
5475
+ const discriminator = value.type;
5476
+ if (typeof discriminator !== "string") {
5477
+ return base;
5478
+ }
5479
+ const baseWithDiscriminator = `${base} for snapshot with type "${discriminator}"`;
5480
+ // If exactly one union member has a literal `type` property matching the
5481
+ // snapshot's discriminator, run its validate() so we can append the
5482
+ // property-level reasons it didn't match. This converts the bare prod-build
5483
+ // "no matching type" into something diagnosable (e.g. which field was the
5484
+ // wrong type, which required field was missing) without re-bloating the
5485
+ // message back to every-member's full describe() output.
5486
+ const candidate = this._findCandidateByTypeDiscriminator(discriminator);
5487
+ if (!candidate) {
5488
+ return baseWithDiscriminator;
5489
+ }
5490
+ const errors = candidate.validate(value, [
5491
+ { path: "", type: candidate }
5492
+ ]);
5493
+ if (errors.length === 0) {
5494
+ return baseWithDiscriminator;
5495
+ }
5496
+ return (`${baseWithDiscriminator}:\n ` +
5497
+ formatValidationErrorLines(errors).join("\n "));
5498
+ }
5499
+ _findCandidateByTypeDiscriminator(discriminator) {
5500
+ let found;
5501
+ for (const t of this._types) {
5502
+ const model = resolveModelType(t);
5503
+ if (!model) {
5504
+ continue;
5505
+ }
5506
+ const typeProp = model.properties.type;
5507
+ if (!typeProp ||
5508
+ !(typeProp.flags & TypeFlags.Literal) ||
5509
+ !typeProp.is(discriminator)) {
5510
+ continue;
5511
+ }
5512
+ if (found) {
5513
+ // Ambiguous (two members declare the same `type` literal). Fall back
5514
+ // to the short message rather than picking arbitrarily.
5515
+ return undefined;
5516
+ }
5517
+ // Return the original (possibly wrapped) member, not the unwrapped
5518
+ // model, so validate()/is() still apply optional defaults and any
5519
+ // snapshotProcessor pre-processing.
5520
+ found = t;
5521
+ }
5522
+ return found;
5523
+ }
5524
+ // True when every member resolves to a model carrying a literal `type`
5525
+ // discriminator — i.e. a fully discriminated union, where a snapshot's `type`
5526
+ // uniquely identifies the intended member and no untagged catch-all member
5527
+ // could also accept it. Cached: membership is fixed at construction.
5528
+ _allMembersDiscriminated;
5529
+ allMembersDiscriminated() {
5530
+ if (this._allMembersDiscriminated === undefined) {
5531
+ this._allMembersDiscriminated = this._types.every(t => {
5532
+ const model = resolveModelType(t);
5533
+ const typeProp = model &&
5534
+ model.properties.type;
5535
+ return !!typeProp && (typeProp.flags & TypeFlags.Literal) !== 0;
5536
+ });
5537
+ }
5538
+ return this._allMembersDiscriminated;
5445
5539
  }
5446
5540
  determineType(value, reconcileCurrentType) {
5447
5541
  // try the dispatcher, if defined
@@ -5530,11 +5624,14 @@ class Union extends BaseType {
5530
5624
  }
5531
5625
  snapshotLooksLikeType(value, type) {
5532
5626
  // for model types, check if snapshot has all the required property keys
5533
- // and that any literal-typed properties match exactly
5534
- if (type instanceof ModelType) {
5535
- const props = type.properties;
5627
+ // and that any literal-typed properties match exactly. Unwrap optional() /
5628
+ // snapshotProcessor() / refinement() / late() so wrapped members (e.g.
5629
+ // jbrowse config schemas, which are always optional(model)) still match.
5630
+ const model = resolveModelType(type);
5631
+ if (model) {
5632
+ const props = model.properties;
5536
5633
  // use cached propertyNames from ModelType instead of Object.keys()
5537
- for (const key of type.propertyNames) {
5634
+ for (const key of model.propertyNames) {
5538
5635
  const propType = props[key];
5539
5636
  const isOptional = propType.flags & TypeFlags.Optional;
5540
5637
  const propValue = value[key];
@@ -5561,6 +5658,28 @@ class Union extends BaseType {
5561
5658
  if (this._dispatcher) {
5562
5659
  return this._dispatcher(value).validate(value, context);
5563
5660
  }
5661
+ // For plain-object snapshots carrying a `type` discriminator, validate only
5662
+ // the single member whose literal `type` matches.
5663
+ // - Clean validation short-circuits to success only when it is sound to skip
5664
+ // the other members: an eager union (first match wins) or a fully
5665
+ // discriminated one (no untagged catch-all could also match). A non-eager
5666
+ // union with a catch-all must still fall through so ambiguity is counted.
5667
+ // - A failure is the definitive, scoped error only when every member is
5668
+ // discriminated; otherwise a catch-all could still accept the value, so
5669
+ // fall through to full validation.
5670
+ if (isPlainObject(value) && !isStateTreeNode(value)) {
5671
+ const discriminator = value.type;
5672
+ if (typeof discriminator === "string") {
5673
+ const candidate = this._findCandidateByTypeDiscriminator(discriminator);
5674
+ if (candidate) {
5675
+ const errors = candidate.validate(value, context);
5676
+ const cleanAndUnique = errors.length === 0 && this._eager;
5677
+ if (cleanAndUnique || this.allMembersDiscriminated()) {
5678
+ return errors;
5679
+ }
5680
+ }
5681
+ }
5682
+ }
5564
5683
  // for plain-object snapshots, prefer union members whose literal-typed
5565
5684
  // discriminator properties match the value (e.g. {type: "MsaView"})
5566
5685
  // so error output is scoped to the intended branch instead of every member