@terrazzo/parser 2.0.0-alpha.5 → 2.0.0-alpha.7

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.
@@ -1,15 +1,8 @@
1
1
  import * as momoa from '@humanwhocodes/momoa';
2
- import { bundle, getObjMember, getObjMembers, parseRef } from '@terrazzo/json-schema-tools';
2
+ import { bundle, encodeFragment, parseRef, replaceNode } from '@terrazzo/json-schema-tools';
3
3
  import type yamlToMomoa from 'yaml-to-momoa';
4
4
  import type Logger from '../logger.js';
5
- import type {
6
- ResolverModifierInline,
7
- ResolverModifierNormalized,
8
- ResolverSetInline,
9
- ResolverSetNormalized,
10
- ResolverSourceNormalized,
11
- } from '../types.js';
12
- import { validateModifier, validateSet } from './validate.js';
5
+ import type { Group, ReferenceObject, ResolverSourceNormalized } from '../types.js';
13
6
 
14
7
  export interface NormalizeResolverOptions {
15
8
  logger: Logger;
@@ -21,11 +14,46 @@ export interface NormalizeResolverOptions {
21
14
 
22
15
  /** Normalize resolver (assuming it’s been validated) */
23
16
  export async function normalizeResolver(
24
- node: momoa.DocumentNode,
25
- { filename, req, src, logger, yamlToMomoa }: NormalizeResolverOptions,
17
+ document: momoa.DocumentNode,
18
+ { logger, filename, req, src, yamlToMomoa }: NormalizeResolverOptions,
26
19
  ): Promise<ResolverSourceNormalized> {
27
- const resolverSource = momoa.evaluate(node) as unknown as ResolverSourceNormalized;
28
- const resolutionOrder = getObjMember(node.body as momoa.ObjectNode, 'resolutionOrder') as momoa.ArrayNode;
20
+ // Important note: think about sets, modifiers, and resolutionOrder all
21
+ // containing their own partial tokens documents. Now think about JSON $refs
22
+ // inside those. Because we want to treat them all as one _eventual_ document,
23
+ // we defer resolving $refs until the very last step. In most setups, this has
24
+ // no effect on the final result, however, in the scenario where remote
25
+ // documents are loaded and they conflict in unexpected ways, resolving too
26
+ // early will produce incorrect results.
27
+ //
28
+ // To prevent this, we bundle ONCE at the very top level, with the `$defs` at
29
+ // the top level now containing all partial documents (as opposed to bundling
30
+ // every sub document individually). So all that said, we are deciding to
31
+ // choose the “all-in-one“ method for closer support with DTCG aliases, but at
32
+ // the expense of some edge cases of $refs behaving unexpectedly.
33
+ const resolverBundle = await bundle([{ filename, src }], { req, yamlToMomoa });
34
+ const resolverSource = momoa.evaluate(resolverBundle.document) as unknown as ResolverSourceNormalized;
35
+
36
+ // Resolve $refs, but in a very different way than everywhere else These are
37
+ // all _evaluated_, meaning initialized in JS memory. Unlike in the AST, when
38
+ // we resolve these they’ll share memory points (which isn’t possible in the
39
+ // AST—values must be duplicated). This code is unique because it’s the only
40
+ // place where we’re dealing with shared, initialized JS memory.
41
+ replaceNode(document, resolverBundle.document); // inject $defs into the root document
42
+ for (const set of Object.values(resolverSource.sets ?? {})) {
43
+ for (const source of set.sources) {
44
+ resolvePartials(source, { resolver: resolverSource, logger });
45
+ }
46
+ }
47
+ for (const modifier of Object.values(resolverSource.modifiers ?? {})) {
48
+ for (const context of Object.values(modifier.contexts)) {
49
+ for (const source of context) {
50
+ resolvePartials(source, { resolver: resolverSource, logger });
51
+ }
52
+ }
53
+ }
54
+ for (const item of resolverSource.resolutionOrder ?? []) {
55
+ resolvePartials(item, { resolver: resolverSource, logger });
56
+ }
29
57
 
30
58
  return {
31
59
  name: resolverSource.name,
@@ -33,74 +61,73 @@ export async function normalizeResolver(
33
61
  description: resolverSource.description,
34
62
  sets: resolverSource.sets,
35
63
  modifiers: resolverSource.modifiers,
36
- resolutionOrder: await Promise.all(
37
- resolutionOrder.elements.map(async (element, i) => {
38
- const layer = element.value as momoa.ObjectNode;
39
- const members = getObjMembers(layer);
40
-
41
- // If this is an inline set or modifier it’s already been validated; we only need
42
- // to resolve & validate $refs here which haven’t yet been parsed
43
- let item = layer as unknown as ResolverSetInline | ResolverModifierInline;
44
-
45
- // 1. $ref
46
- if (members.$ref) {
47
- const entry = { group: 'parser', label: 'init', node: members.$ref, src } as const;
48
- const { url, subpath } = parseRef((members.$ref as unknown as momoa.StringNode).value);
49
- if (url === '.') {
50
- // 1a. local $ref: pull from local document
51
- if (!subpath?.[0]) {
52
- logger.error({ ...entry, message: '$ref can’t refer to the root document.' });
53
- } else if (subpath[0] !== 'sets' && subpath[0] !== 'modifiers') {
54
- // Note: technically we could allow $defs, but that’s just unnecessary shenanigans here.
55
- logger.error({
56
- ...entry,
57
- message: 'Local $ref in resolutionOrder must point to either #/sets/[set] or #/modifiers/[modifiers].',
58
- });
59
- } else {
60
- const resolvedItem = resolverSource[subpath[0] as 'sets' | 'modifiers']?.[subpath[1]!];
61
- if (!resolvedItem) {
62
- logger.error({ ...entry, message: 'Invalid $ref' });
63
- } else {
64
- item = {
65
- type: subpath[0] === 'sets' ? 'set' : 'modifier',
66
- name: subpath[1],
67
- ...(resolvedItem as any), // Note: as long as this exists, this has already been validated to be correct
68
- };
69
- }
70
- }
71
- } else {
72
- // 1b. remote $ref: load and validate
73
- const result = await bundle(
74
- [{ filename: new URL(url, filename), src: resolverSource.resolutionOrder[i]! }],
75
- {
76
- req,
77
- yamlToMomoa,
78
- },
79
- );
80
- if (result.document.body.type === 'Object') {
81
- const type = getObjMember(result.document.body, 'type');
82
- if (type?.type === 'String' && type.value === 'set') {
83
- validateSet(result.document.body as momoa.ObjectNode, true, src);
84
- item = momoa.evaluate(result.document.body) as unknown as ResolverSetInline;
85
- } else if (type?.type === 'String' && type.value === 'modifier') {
86
- validateModifier(result.document.body as momoa.ObjectNode, true, src);
87
- item = momoa.evaluate(result.document.body) as unknown as ResolverModifierInline;
88
- }
89
- }
90
- logger.error({ ...entry, message: '$ref did not resolve to a valid Set or Modifier.' });
91
- }
92
- }
93
-
94
- // 2. resolve inline sources & contexts
95
- const finalResult = await bundle([{ filename, src: item }], { req, yamlToMomoa });
96
- return momoa.evaluate(finalResult.document.body) as unknown as
97
- | ResolverSetNormalized
98
- | ResolverModifierNormalized;
99
- }),
100
- ),
64
+ resolutionOrder: resolverSource.resolutionOrder,
101
65
  _source: {
102
66
  filename,
103
- node,
67
+ document,
104
68
  },
105
69
  };
106
70
  }
71
+
72
+ /** Resolve $refs for already-initialized JS */
73
+ function resolvePartials(
74
+ source: Group | ReferenceObject,
75
+ {
76
+ resolver,
77
+ logger,
78
+ }: {
79
+ resolver: any;
80
+ logger: Logger;
81
+ },
82
+ ) {
83
+ if (!source) {
84
+ return;
85
+ }
86
+ const entry = { group: 'parser' as const, label: 'resolver' };
87
+ if (Array.isArray(source)) {
88
+ for (const item of source) {
89
+ resolvePartials(item, { resolver, logger });
90
+ }
91
+ } else if (typeof source === 'object') {
92
+ for (const k of Object.keys(source)) {
93
+ if (k === '$ref') {
94
+ const $ref = (source as any)[k] as string;
95
+ const { url, subpath = [] } = parseRef($ref);
96
+ if (url !== '.' || !subpath.length) {
97
+ logger.error({ ...entry, message: `Could not load $ref ${JSON.stringify($ref)}` });
98
+ }
99
+ const found = findObject(resolver, subpath ?? [], logger);
100
+ if (subpath[0] === 'sets' || subpath[0] === 'modifiers') {
101
+ found.type = subpath[0].replace(/s$/, '');
102
+ found.name = subpath[1];
103
+ }
104
+ if (found) {
105
+ for (const k2 of Object.keys(found)) {
106
+ (source as any)[k2] = found[k2];
107
+ }
108
+ delete (source as any).$ref;
109
+ } else {
110
+ logger.error({ ...entry, message: `Could not find ${JSON.stringify($ref)}` });
111
+ }
112
+ } else {
113
+ resolvePartials((source as any)[k], { resolver, logger });
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ function findObject(dict: Record<string, any>, path: string[], logger: Logger): any {
120
+ let node = dict;
121
+ for (const idRaw of path) {
122
+ const id = idRaw.replace(/~/g, '~0').replace(/\//g, '~1');
123
+ if (!(id in node)) {
124
+ logger.error({
125
+ group: 'parser',
126
+ label: 'resolver',
127
+ message: `Could not load $ref ${encodeFragment(path)}`,
128
+ });
129
+ }
130
+ node = node[id];
131
+ }
132
+ return node;
133
+ }
package/src/types.ts CHANGED
@@ -377,7 +377,7 @@ export interface ResolverSourceNormalized {
377
377
  resolutionOrder: (ResolverSetNormalized | ResolverModifierNormalized)[];
378
378
  _source: {
379
379
  filename?: URL;
380
- node: momoa.DocumentNode;
380
+ document: momoa.DocumentNode;
381
381
  };
382
382
  }
383
383
 
@@ -459,3 +459,10 @@ export interface TransformHookOptions {
459
459
  /** Momoa documents */
460
460
  sources: InputSourceWithDocument[];
461
461
  }
462
+
463
+ export interface RefMapEntry {
464
+ filename: string;
465
+ refChain: string[];
466
+ }
467
+
468
+ export type RefMap = Record<string, RefMapEntry>;