@udt/parser-utils 0.2.0 → 0.3.1

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,435 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import {
3
- parseData,
4
- type ParseDesignTokenDataFn,
5
- type ParseGroupDataFn,
6
- type IsDesignTokenDataFn,
7
- type ParserConfig,
8
- InvalidDataError,
9
- AddChildFn,
10
- } from "./parseData.js";
11
-
12
- interface TestGroup {
13
- type: "group";
14
- }
15
-
16
- interface TestDesignToken {
17
- type: "token";
18
- }
19
-
20
- interface TestParentContext {
21
- dummyData?: number;
22
- }
23
-
24
- interface ParseDataCall {
25
- type: "group" | "token";
26
- path: string[];
27
- }
28
-
29
- interface AddChildCall {
30
- type: "addChild";
31
- name: string;
32
- }
33
-
34
- // Used to track the order in which parseXyzData and addChild
35
- // functions are called
36
- let parseDataCalls: (ParseDataCall | AddChildCall)[] = [];
37
-
38
- /*
39
- * For the purpose of these tests, any object with a
40
- * value property will be considered a design token.
41
- *
42
- * Intentionally not using (current) DTCG syntax, to
43
- * demonstrate that parseData() could be used to parse
44
- * other formats too, as long as they use the same
45
- * nested object structure for groups and tokens.
46
- */
47
- const mockIsDesignTokenData = vi.fn<IsDesignTokenDataFn>(
48
- (data) => data.value !== undefined
49
- );
50
-
51
- const mockParseGroupData = vi.fn<
52
- ParseGroupDataFn<TestGroup, TestParentContext>
53
- >((_data, path, contextFromParent) => {
54
- parseDataCalls.push({
55
- type: "group",
56
- path,
57
- });
58
- return {
59
- group: {
60
- type: "group",
61
- },
62
- // pass through contextFromParent
63
- contextForChildren: contextFromParent,
64
- };
65
- });
66
-
67
- const mockAddChildToGroup = vi.fn<AddChildFn<TestGroup, TestDesignToken>>(
68
- (_parent, name, _child) => {
69
- parseDataCalls.push({
70
- type: "addChild",
71
- name,
72
- });
73
- }
74
- );
75
-
76
- const mockParseDesignTokenData = vi.fn<
77
- ParseDesignTokenDataFn<TestDesignToken, TestParentContext>
78
- >((_data, path, _contextFromParent) => {
79
- parseDataCalls.push({ type: "token", path });
80
- const result: TestDesignToken = {
81
- type: "token",
82
- };
83
- return result;
84
- });
85
-
86
- const defaultParserConfig: ParserConfig<
87
- TestDesignToken,
88
- TestGroup,
89
- TestParentContext
90
- > = {
91
- isDesignTokenData: mockIsDesignTokenData,
92
- groupPropsToExtract: [],
93
- parseGroupData: mockParseGroupData,
94
- parseDesignTokenData: mockParseDesignTokenData,
95
- };
96
-
97
- describe("parseData()", () => {
98
- let parserConfig: ParserConfig<TestDesignToken, TestGroup, TestParentContext>;
99
-
100
- beforeEach(() => {
101
- // Reset stuff
102
- parserConfig = defaultParserConfig;
103
- parseDataCalls = [];
104
- parserConfig.groupPropsToExtract = [];
105
- mockIsDesignTokenData.mockClear();
106
- mockParseGroupData.mockClear();
107
- mockParseDesignTokenData.mockClear();
108
- });
109
-
110
- describe("parsing an empty group object", () => {
111
- let parsedGroupOrToken: TestGroup | TestDesignToken | undefined;
112
-
113
- beforeEach(() => {
114
- parsedGroupOrToken = parseData({}, parserConfig);
115
- });
116
-
117
- it("returns a group", () => {
118
- expect(parsedGroupOrToken?.type).toBe("group");
119
- });
120
-
121
- it("calls isDesignTokenData function once", () => {
122
- expect(mockIsDesignTokenData).toHaveBeenCalledOnce();
123
- });
124
-
125
- it("class isDesignTokenData function with input data", () => {
126
- expect(mockIsDesignTokenData).toHaveBeenCalledWith({});
127
- });
128
-
129
- it("calls parseGroupData function once", () => {
130
- expect(mockParseGroupData).toHaveBeenCalledOnce();
131
- });
132
-
133
- it("calls parseGroupData function with empty group and empty path array", () => {
134
- expect(mockParseGroupData).toHaveBeenCalledWith({}, [], undefined);
135
- });
136
-
137
- it("does not call parseDesignTokenData function", () => {
138
- expect(mockParseDesignTokenData).not.toHaveBeenCalled();
139
- });
140
- });
141
-
142
- describe("parsing a design token object", () => {
143
- const testTokenData = {
144
- value: "whatever", // <-- this makes it a token
145
- other: "thing",
146
- stuff: 123,
147
- notAGroup: {},
148
- };
149
- let parsedGroupOrToken: TestGroup | TestDesignToken | undefined;
150
-
151
- beforeEach(() => {
152
- parsedGroupOrToken = parseData(testTokenData, parserConfig);
153
- });
154
-
155
- it("returns a design token", () => {
156
- expect(parsedGroupOrToken?.type).toBe("token");
157
- });
158
-
159
- it("does not call parseGroupData function", () => {
160
- expect(mockParseGroupData).not.toHaveBeenCalled();
161
- });
162
-
163
- it("calls parseDesignTokenData function once", () => {
164
- expect(mockParseDesignTokenData).toHaveBeenCalledOnce();
165
- });
166
-
167
- it("calls parseDesignTokenData function with complete data and empty path array", () => {
168
- expect(mockParseDesignTokenData).toHaveBeenCalledWith(
169
- testTokenData,
170
- [],
171
- undefined
172
- );
173
- });
174
- });
175
-
176
- describe("parsing a group object containing design tokens and nested groups", () => {
177
- /*
178
- Contains:
179
-
180
- - 4 groups: <root>, emptyGroup,
181
- groupWithTokens, nestedGroup
182
-
183
- - 5 tokens: nestedToken, anotherNestedToken,
184
- deeplyNestedToken, token, anotherToken
185
- */
186
- const testData = {
187
- specialGroupProp: {
188
- value: "not a token value",
189
- description: "This is not a group or token!",
190
- },
191
- emptyGroup: {},
192
- groupWithTokens: {
193
- nestedToken: {
194
- value: "a",
195
- },
196
- anotherNestedToken: {
197
- value: "b",
198
- },
199
- nestedGroup: {
200
- deeplyNestedToken: {
201
- value: "x",
202
- },
203
- },
204
- },
205
- token: {
206
- value: 1,
207
- },
208
- anotherToken: {
209
- value: 2,
210
- },
211
- };
212
-
213
- beforeEach(() => {
214
- parserConfig.groupPropsToExtract = ["specialGroupProp"];
215
- parseData(testData, parserConfig);
216
- });
217
-
218
- it("calls parseDesignTokenData function 5 times", () => {
219
- expect(mockParseDesignTokenData).toHaveBeenCalledTimes(5);
220
- });
221
-
222
- it("calls parseGroupData function 4 times", () => {
223
- expect(mockParseGroupData).toHaveBeenCalledTimes(4);
224
- });
225
-
226
- it("only passed extracted group props to the parseGroupData function", () => {
227
- expect(mockParseGroupData).toHaveBeenNthCalledWith(
228
- 1,
229
- { specialGroupProp: testData.specialGroupProp },
230
- [],
231
- undefined
232
- );
233
- });
234
-
235
- it("traverses the data depth-first", () => {
236
- // Start with root group
237
- expect(parseDataCalls[0]).toStrictEqual({
238
- type: "group",
239
- path: [],
240
- });
241
-
242
- // "emptyGroup" next
243
- expect(parseDataCalls[1]).toStrictEqual({
244
- type: "group",
245
- path: ["emptyGroup"],
246
- });
247
-
248
- // "groupWithTokens" next
249
- expect(parseDataCalls[2]).toStrictEqual({
250
- type: "group",
251
- path: ["groupWithTokens"],
252
- });
253
-
254
- // "nestedToken" next
255
- expect(parseDataCalls[3]).toStrictEqual({
256
- type: "token",
257
- path: ["groupWithTokens", "nestedToken"],
258
- });
259
-
260
- // "anotherNestedToken" next
261
- expect(parseDataCalls[4]).toStrictEqual({
262
- type: "token",
263
- path: ["groupWithTokens", "anotherNestedToken"],
264
- });
265
-
266
- // "nestedGroup" next
267
- expect(parseDataCalls[5]).toStrictEqual({
268
- type: "group",
269
- path: ["groupWithTokens", "nestedGroup"],
270
- });
271
-
272
- // "deeplyNestedToken" next
273
- expect(parseDataCalls[6]).toStrictEqual({
274
- type: "token",
275
- path: ["groupWithTokens", "nestedGroup", "deeplyNestedToken"],
276
- });
277
-
278
- // "token" next
279
- expect(parseDataCalls[7]).toStrictEqual({
280
- type: "token",
281
- path: ["token"],
282
- });
283
-
284
- // "anotherToken" next
285
- expect(parseDataCalls[8]).toStrictEqual({
286
- type: "token",
287
- path: ["anotherToken"],
288
- });
289
- });
290
- });
291
-
292
- describe("with context data", () => {
293
- const testData = { group: {}, token: { value: 123 } };
294
- const testContext: TestParentContext = { dummyData: 42 };
295
-
296
- beforeEach(() => {
297
- parseData(testData, parserConfig, testContext);
298
- });
299
-
300
- it("passes the context data to outermost parseGroupData call", () => {
301
- // 1st call is the root group
302
- expect(mockParseGroupData).toHaveBeenNthCalledWith(
303
- 1,
304
- {},
305
- [],
306
- testContext
307
- );
308
- });
309
-
310
- it("passes context from parent group to parseGroupData calls for child groups", () => {
311
- expect(mockParseGroupData).toHaveBeenNthCalledWith(
312
- 2,
313
- {},
314
- ["group"],
315
- testContext
316
- );
317
- });
318
-
319
- it("passes context from parent group to parseDesignTokenData calls for child tokens", () => {
320
- expect(mockParseDesignTokenData).toHaveBeenCalledWith(
321
- testData.token,
322
- ["token"],
323
- testContext
324
- );
325
- });
326
- });
327
-
328
- describe("using addChildToGroup function", () => {
329
- const testData = {
330
- tokenA: { value: 1 },
331
- tokenB: { value: 2 },
332
- groupC: { tokenX: { value: 99 }, tokenY: { value: 100 } },
333
- };
334
-
335
- beforeEach(() => {
336
- mockAddChildToGroup.mockClear();
337
- parserConfig.addChildToGroup = mockAddChildToGroup;
338
- parseData(testData, parserConfig);
339
- });
340
-
341
- it("calls addChildToGroup for every child of every group", () => {
342
- // Root group contains 3 children: "tokenA", "tokenB" and "groupC"
343
- // "groupC" contains 2 children: "tokenX" and "tokenY"
344
- // 3 + 2 = 5
345
- expect(mockAddChildToGroup).toHaveBeenCalledTimes(5);
346
- });
347
-
348
- it("adds a nested group to its parent before parsing its children", () => {
349
- // Steps should be:
350
- // 0 parse root group
351
- expect(parseDataCalls[0]).toStrictEqual({ type: "group", path: [] });
352
-
353
- // 1 parse token "tokenA"
354
- expect(parseDataCalls[1]).toStrictEqual({
355
- type: "token",
356
- path: ["tokenA"],
357
- });
358
-
359
- // 2 addChild "tokenA" to root group
360
- expect(parseDataCalls[2]).toStrictEqual({
361
- type: "addChild",
362
- name: "tokenA",
363
- });
364
-
365
- // 3 parse token "tokenB"
366
- expect(parseDataCalls[3]).toStrictEqual({
367
- type: "token",
368
- path: ["tokenB"],
369
- });
370
-
371
- // 4 addChild "tokenB" to root group
372
- expect(parseDataCalls[4]).toStrictEqual({
373
- type: "addChild",
374
- name: "tokenB",
375
- });
376
-
377
- // 5 parse group "groupC"
378
- expect(parseDataCalls[5]).toStrictEqual({
379
- type: "group",
380
- path: ["groupC"],
381
- });
382
-
383
- // 6 addChild "groupC" to root group
384
- // NOTE that this happens *before* any of groupC's
385
- // children are parsed.
386
- expect(parseDataCalls[6]).toStrictEqual({
387
- type: "addChild",
388
- name: "groupC",
389
- });
390
-
391
- // 7 parse token "tokenX"
392
- expect(parseDataCalls[7]).toStrictEqual({
393
- type: "token",
394
- path: ["groupC", "tokenX"],
395
- });
396
-
397
- // 8 addChild "tokenX" to "groupC"
398
- expect(parseDataCalls[8]).toStrictEqual({
399
- type: "addChild",
400
- name: "tokenX",
401
- });
402
-
403
- // 9 parse token "tokenY"
404
- expect(parseDataCalls[9]).toStrictEqual({
405
- type: "token",
406
- path: ["groupC", "tokenY"],
407
- });
408
-
409
- // 10 addChild "tokenY" to "groupC"
410
- expect(parseDataCalls[10]).toStrictEqual({
411
- type: "addChild",
412
- name: "tokenY",
413
- });
414
- });
415
- });
416
-
417
- it("throws an InvalidDataError when given a non-object input", () => {
418
- expect(() => parseData(123, parserConfig)).toThrowError(InvalidDataError);
419
- });
420
-
421
- it("throws an InvalidDataError when encountering a non-object child", () => {
422
- expect(() => parseData({ brokenThing: 123 }, parserConfig)).toThrowError(
423
- InvalidDataError
424
- );
425
- });
426
-
427
- it("includes the path and data when throwing an InvalidDataError", () => {
428
- try {
429
- parseData({ brokenThing: 123 }, parserConfig);
430
- } catch (error) {
431
- expect((error as InvalidDataError).path).toStrictEqual(["brokenThing"]);
432
- expect((error as InvalidDataError).data).toBe(123);
433
- }
434
- });
435
- });
package/src/parseData.ts DELETED
@@ -1,280 +0,0 @@
1
- import { isPlainObject, type PlainObject } from "./isJsonObject.js";
2
- import { extractProperties } from "./extractProperties.js";
3
-
4
- /**
5
- * A function that checks whether an object is a design token
6
- * or not (if not, it is assumed to be a group).
7
- *
8
- * E.g. for DTCG data, this could check for the presence of a
9
- * `$value` property.
10
- *
11
- * @param data A plain object (guaranteed not to `null` or an
12
- * array)
13
- *
14
- * @returns `true` if `data` is design token data, `false` if
15
- * it is group data.
16
- */
17
- export type IsDesignTokenDataFn = (data: PlainObject) => boolean;
18
-
19
- /**
20
- * A function that parses design token data.
21
- *
22
- * @param data A plain object containing design token data
23
- * (guaranteed not to be `null` or an array)
24
- * @param path The path to the design token data.
25
- * @param contextFromParent Context data (if any) that was
26
- * returned by the `parseGroupData()` call that
27
- * parsed the group containing this design token.
28
- *
29
- * @returns The parsed representation of the design token.
30
- * May be `undefined` if there is no useful result
31
- * to return from `parseData()` - e.g. if just
32
- * logging design token info or something like that.
33
- */
34
- export type ParseDesignTokenDataFn<ParsedDesignToken, T> = (
35
- data: PlainObject,
36
- path: string[],
37
- contextFromParent?: T
38
- ) => ParsedDesignToken;
39
-
40
- /**
41
- * A function that adds a parsed group or design token
42
- * as a child of the given parsed group.
43
- *
44
- * @param parent The parent group to add a child to
45
- * @param name The name of the child group or design token
46
- * @param child The group or design token to add
47
- */
48
- export type AddChildFn<ParsedGroup, ParsedDesignToken> = (
49
- parent: ParsedGroup,
50
- name: string,
51
- child: ParsedGroup | ParsedDesignToken
52
- ) => void;
53
-
54
- /**
55
- * The return value of a `ParseGroupDataFn`.
56
- */
57
- export interface ParseGroupResult<ParsedGroup, T> {
58
- /**
59
- * The parsed representation of the group.
60
- *
61
- * May be omitted if there is no useful result to
62
- * return and we only need to pass along context
63
- * data.
64
- */
65
- group?: ParsedGroup;
66
-
67
- /**
68
- * Optional context data to be passed into the
69
- * `parseGroupData()` and `parseDesignTokenData()` calls
70
- * for any nested group or design token data.
71
- *
72
- * Useful if those functions need access to some data from
73
- * higher up in the original data structure. For example, if
74
- * parsing DTCG data, this could be used to pass inherited
75
- * properties like `$type` down.
76
- */
77
- contextForChildren?: T;
78
- }
79
-
80
- /**
81
- * A function that parses group data.
82
- *
83
- * @param data A plain object containing group data
84
- * (guaranteed not to be `null` or an array)
85
- * @param path The path to the group data.
86
- * @param contextFromParent Context data (if any) that was
87
- * returned by the `parseGroupData()` call that
88
- * parsed the group containing this group.
89
- *
90
- * @returns The parsed representation of the group and,
91
- * optionally, some context data to pass down
92
- * when child data is parsed.
93
- */
94
- export type ParseGroupDataFn<ParsedGroup, T> = (
95
- data: PlainObject,
96
- path: string[],
97
- contextFromParent?: T
98
- ) => ParseGroupResult<ParsedGroup, T>;
99
-
100
- export interface ParserConfig<ParsedDesignToken, ParsedGroup, T> {
101
- /**
102
- * A function that determines whether an object in the input
103
- * data is a design token or a group.
104
- */
105
- isDesignTokenData: IsDesignTokenDataFn;
106
-
107
- /**
108
- * Array of strings and/or RegExp which match
109
- * properties of group objects that are NOT
110
- * names of child design tokens or groups.
111
- *
112
- * E.g. for DTCG data, this could be a RegEx
113
- * like /^$/ which would match all $-prefixed
114
- * format properties
115
- */
116
- groupPropsToExtract: (string | RegExp)[];
117
-
118
- /**
119
- * Function which is called for each group data object
120
- * that is encountered.
121
- *
122
- * Is given the extracted properties of that group and its
123
- * path, and should parse that data into whatever structure
124
- * is desired.
125
- */
126
- parseGroupData?: ParseGroupDataFn<ParsedGroup, T>;
127
-
128
- /**
129
- * Function which is called for each design token
130
- *data object that is encountered.
131
- *
132
- * Is given the design token data and its path, and
133
- * should parse that data into whatever structure is
134
- * desired.
135
- */
136
- parseDesignTokenData: ParseDesignTokenDataFn<ParsedDesignToken, T>;
137
-
138
- /**
139
- * Optional function that will add parsed groups
140
- * or design tokens as children of another parsed group.
141
- *
142
- * Intended for cases where the parsed representation
143
- * of a group needs to contain its children. If not
144
- * needed, this property can be omitted.
145
- */
146
- addChildToGroup?: AddChildFn<ParsedGroup, ParsedDesignToken>;
147
- }
148
-
149
- /**
150
- * Thrown when `parseData()` encounters group or design token
151
- * data that is not a plain object.
152
- */
153
- export class InvalidDataError extends Error {
154
- /**
155
- * Path to the value that is not a plain object
156
- */
157
- public path: string[];
158
-
159
- /**
160
- * The offending value.
161
- */
162
- public data: unknown;
163
-
164
- constructor(path: string[], data: unknown) {
165
- super(
166
- `Expected object at path "${path.join(
167
- "."
168
- )}", but got ${typeof data} instead`
169
- );
170
- this.name = "InvalidDataError";
171
- this.path = path;
172
- this.data = data;
173
- }
174
- }
175
-
176
- /**
177
- * The internal data parsing implementation.
178
- *
179
- * Recursively calls itself for nested group and
180
- * design token data.
181
- *
182
- * @param data The input data to traverse and parse
183
- * @param config Parser config
184
- * @param contextFromParent Context data passed in from
185
- * parent calls to this function.
186
- * @param path The path to the input data
187
- * @param addToParent A function to add the parsed data
188
- * to the parent group.
189
- * @returns The parsed design token or group.
190
- */
191
- function parseDataImpl<ParsedDesignToken, ParsedGroup, T>(
192
- data: unknown,
193
- config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
194
- contextFromParent?: T,
195
- path: string[] = [],
196
- parentGroup?: ParsedGroup
197
- ): ParsedDesignToken | ParsedGroup | undefined {
198
- if (!isPlainObject(data)) {
199
- throw new InvalidDataError(path, data);
200
- }
201
-
202
- const {
203
- isDesignTokenData,
204
- groupPropsToExtract,
205
- parseGroupData,
206
- parseDesignTokenData,
207
- addChildToGroup,
208
- } = config;
209
-
210
- let groupOrToken: ParsedGroup | ParsedDesignToken | undefined = undefined;
211
- if (isDesignTokenData(data)) {
212
- // looks like a token
213
- groupOrToken = parseDesignTokenData(data, path, contextFromParent);
214
- if (addChildToGroup && path.length > 0 && parentGroup !== undefined) {
215
- addChildToGroup(parentGroup, path[path.length - 1], groupOrToken);
216
- }
217
- } else {
218
- // must be a group
219
- const { extracted: groupData, rest: children } = extractProperties(
220
- data,
221
- groupPropsToExtract
222
- );
223
-
224
- let contextForChildren: T | undefined;
225
- if (parseGroupData) {
226
- const parseResult = parseGroupData(groupData, path, contextFromParent);
227
- contextForChildren = parseResult.contextForChildren;
228
- groupOrToken = parseResult.group;
229
- }
230
-
231
- if (
232
- addChildToGroup &&
233
- path.length > 0 &&
234
- parentGroup !== undefined &&
235
- groupOrToken !== undefined
236
- ) {
237
- addChildToGroup(parentGroup, path[path.length - 1], groupOrToken);
238
- }
239
-
240
- for (const childName in children) {
241
- parseDataImpl(
242
- children[childName],
243
- config,
244
- contextForChildren,
245
- [...path, childName],
246
- groupOrToken
247
- );
248
- }
249
- }
250
-
251
- return groupOrToken;
252
- }
253
-
254
- /**
255
- * Parses a nested object structure representing groups
256
- * and design tokens, such as the data obtained by reading
257
- * and JSON-parsing a DTCG file.
258
- *
259
- * It will recursively traverse the input data (depth first)
260
- * and, using the functions provided in the config:
261
- *
262
- * 1. Check if the object is design token or group data
263
- * 2. Pass that data to the parsed or processed by the
264
- * relevant function
265
- * 3. Return the outermost parsed group or design token
266
- *
267
- * @param data The input data to traverse and parse
268
- * @param config Configuration for this parser
269
- * @param contextFromParent Optional context data to
270
- * pass into the top-most design token
271
- * or group parser function call.
272
- * @returns The outermost parsed group or design token
273
- */
274
- export function parseData<ParsedDesignToken, ParsedGroup, T>(
275
- data: unknown,
276
- config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
277
- contextFromParent?: T
278
- ): ParsedDesignToken | ParsedGroup | undefined {
279
- return parseDataImpl(data, config, contextFromParent);
280
- }