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