@quanxiaoxiao/datav 0.4.0 → 0.5.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.
- package/README.md +680 -165
- package/dist/createArrayAccessor.d.ts +2 -0
- package/dist/createArrayAccessor.d.ts.map +1 -0
- package/dist/createArrayAccessor.js +38 -0
- package/dist/createArrayAccessor.js.map +1 -0
- package/dist/createDataAccessor.d.ts +2 -0
- package/dist/createDataAccessor.d.ts.map +1 -0
- package/dist/createDataAccessor.js +23 -0
- package/dist/createDataAccessor.js.map +1 -0
- package/dist/createDataTransformer.d.ts +14 -0
- package/dist/createDataTransformer.d.ts.map +1 -0
- package/dist/createDataTransformer.js +124 -0
- package/dist/createDataTransformer.js.map +1 -0
- package/dist/createPathAccessor.d.ts +2 -0
- package/dist/createPathAccessor.d.ts.map +1 -0
- package/dist/createPathAccessor.js +38 -0
- package/dist/createPathAccessor.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parseDotPath.d.ts +2 -0
- package/dist/parseDotPath.d.ts.map +1 -0
- package/dist/parseDotPath.js +14 -0
- package/dist/parseDotPath.js.map +1 -0
- package/dist/parseValueByType.d.ts +11 -0
- package/dist/parseValueByType.d.ts.map +1 -0
- package/dist/parseValueByType.js +122 -0
- package/dist/parseValueByType.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +22 -0
- package/dist/utils.js.map +1 -0
- package/dist/validateExpressSchema.d.ts +7 -0
- package/dist/validateExpressSchema.d.ts.map +1 -0
- package/dist/validateExpressSchema.js +50 -0
- package/dist/validateExpressSchema.js.map +1 -0
- package/package.json +46 -8
- package/src/createArrayAccessor.test.ts +181 -0
- package/src/createArrayAccessor.ts +48 -0
- package/src/createDataAccessor.test.ts +220 -0
- package/src/createDataAccessor.ts +26 -0
- package/src/createDataTransformer.test.ts +847 -0
- package/src/createDataTransformer.ts +173 -0
- package/src/createPathAccessor.test.ts +217 -0
- package/src/createPathAccessor.ts +45 -0
- package/src/index.ts +11 -0
- package/src/parseDotPath.test.ts +132 -0
- package/src/parseDotPath.ts +13 -0
- package/src/parseValueByType.test.ts +342 -0
- package/src/parseValueByType.ts +165 -0
- package/src/utils.test.ts +85 -0
- package/src/utils.ts +22 -0
- package/src/validateExpressSchema.test.ts +295 -0
- package/src/validateExpressSchema.ts +59 -0
- package/.editorconfig +0 -13
- package/eslint.config.mjs +0 -89
- package/src/checkout.mjs +0 -131
- package/src/checkout.test.mjs +0 -144
- package/src/index.mjs +0 -7
- package/src/select/check.mjs +0 -63
- package/src/select/check.test.mjs +0 -76
- package/src/select/index.mjs +0 -117
- package/src/select/index.test.mjs +0 -1145
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createDataAccessor } from './createDataAccessor.js';
|
|
2
|
+
import { parseValueByType } from './parseValueByType.js';
|
|
3
|
+
import { isEmpty, isPlainObject } from './utils.js';
|
|
4
|
+
import { type ExpressSchema,validateExpressSchema } from './validateExpressSchema.js';
|
|
5
|
+
|
|
6
|
+
interface SchemaExpress {
|
|
7
|
+
type: 'string' | 'number' | 'boolean' | 'integer' | 'object' | 'array';
|
|
8
|
+
properties?: Record<string, SchemaExpress> | [string, SchemaExpress] | SchemaExpress[];
|
|
9
|
+
resolve?: (value: unknown, root: unknown) => unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FieldHandler {
|
|
13
|
+
fieldKey: string;
|
|
14
|
+
schema: SchemaExpress;
|
|
15
|
+
transform: (data: unknown, root: unknown) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type SchemaTransformer = (data: unknown, root?: unknown) => unknown;
|
|
19
|
+
|
|
20
|
+
type TransformFn = (schema: SchemaExpress | [string, SchemaExpress]) => SchemaTransformer;
|
|
21
|
+
|
|
22
|
+
const createObjectTransformer = (
|
|
23
|
+
properties: Record<string, SchemaExpress>,
|
|
24
|
+
createTransform: TransformFn,
|
|
25
|
+
): ((data: unknown, root: unknown) => object) => {
|
|
26
|
+
const handlers: FieldHandler[] = Object.entries(properties).map(([fieldKey, schema]) => ({
|
|
27
|
+
fieldKey,
|
|
28
|
+
schema,
|
|
29
|
+
transform: createTransform(schema),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
return (data: unknown, root: unknown) => {
|
|
33
|
+
const rootData = root ?? data;
|
|
34
|
+
|
|
35
|
+
return handlers.reduce((result, handler) => {
|
|
36
|
+
const fieldValue = Array.isArray(handler.schema)
|
|
37
|
+
? handler.transform(data, rootData)
|
|
38
|
+
: handler.transform(data == null ? data : (data as Record<string, unknown>)[handler.fieldKey], rootData);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...result,
|
|
42
|
+
[handler.fieldKey]: fieldValue,
|
|
43
|
+
};
|
|
44
|
+
}, {} as object);
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 处理路径访问的数据提取
|
|
50
|
+
*/
|
|
51
|
+
const extractDataByPath = (pathname: string, data: unknown, root: unknown): unknown => {
|
|
52
|
+
if (pathname.startsWith('$')) {
|
|
53
|
+
return createDataAccessor(pathname.slice(1))(root);
|
|
54
|
+
}
|
|
55
|
+
return createDataAccessor(pathname)(data);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 创建基础类型转换器
|
|
60
|
+
*/
|
|
61
|
+
const createPrimitiveTransformer = (schema: SchemaExpress): SchemaTransformer => {
|
|
62
|
+
return (value: unknown, root?: unknown) => {
|
|
63
|
+
const rootData = root ?? value;
|
|
64
|
+
const resolvedValue = schema.resolve ? schema.resolve(value, rootData) : value;
|
|
65
|
+
return parseValueByType(resolvedValue, schema.type);
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 创建简单对象转换器(无 properties)
|
|
71
|
+
*/
|
|
72
|
+
const createPlainObjectTransformer = (): SchemaTransformer => {
|
|
73
|
+
return (value: unknown) => {
|
|
74
|
+
return isPlainObject(value) ? value : {};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 创建数组转换器(tuple 形式的 properties)
|
|
80
|
+
*/
|
|
81
|
+
const createArrayTransformer = (
|
|
82
|
+
properties: [string, SchemaExpress],
|
|
83
|
+
createTransform: TransformFn,
|
|
84
|
+
): SchemaTransformer => {
|
|
85
|
+
const [pathname, itemSchema] = properties;
|
|
86
|
+
const itemTransform = createTransform(itemSchema);
|
|
87
|
+
|
|
88
|
+
return (data: unknown, root?: unknown) => {
|
|
89
|
+
const rootData = root ?? data;
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(data)) {
|
|
92
|
+
if (pathname.startsWith('$')) {
|
|
93
|
+
const extractedData = createDataAccessor(pathname.slice(1))(rootData);
|
|
94
|
+
return extractedData == null ? [] : [itemTransform(extractedData, rootData)];
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return data.map((item) => {
|
|
100
|
+
if (pathname === '' || pathname === '.') {
|
|
101
|
+
return itemTransform(item, rootData);
|
|
102
|
+
}
|
|
103
|
+
return itemTransform(createDataAccessor(pathname)(item), rootData);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 创建数组对象转换器(对象形式的 properties)
|
|
110
|
+
*/
|
|
111
|
+
const createArrayObjectTransformer = (
|
|
112
|
+
properties: Record<string, SchemaExpress>,
|
|
113
|
+
createTransform: TransformFn,
|
|
114
|
+
): SchemaTransformer => {
|
|
115
|
+
const objectTransform = createObjectTransformer(properties, createTransform);
|
|
116
|
+
|
|
117
|
+
return (data: unknown, root?: unknown) => {
|
|
118
|
+
const rootData = root ?? data;
|
|
119
|
+
|
|
120
|
+
if (!Array.isArray(data)) {
|
|
121
|
+
return isEmpty(properties) ? [] : [objectTransform(data, rootData)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return data.map((item) => objectTransform(item, rootData));
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 创建数据转换器
|
|
130
|
+
* 根据 schema 表达式递归创建数据转换函数
|
|
131
|
+
*/
|
|
132
|
+
export const createDataTransformer: TransformFn = (schema) => {
|
|
133
|
+
// 处理路径访问形式: [pathname, schema]
|
|
134
|
+
if (Array.isArray(schema)) {
|
|
135
|
+
const [pathname, nestedSchema] = schema;
|
|
136
|
+
|
|
137
|
+
if (typeof pathname !== 'string' || !isPlainObject(nestedSchema)) {
|
|
138
|
+
throw new Error(`Invalid schema expression: ${JSON.stringify(schema)}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const nestedTransform = createDataTransformer(nestedSchema);
|
|
142
|
+
|
|
143
|
+
return (data: unknown, root?: unknown) => {
|
|
144
|
+
const rootData = root ?? data;
|
|
145
|
+
const extractedData = extractDataByPath(pathname, data, rootData);
|
|
146
|
+
return nestedTransform(extractedData, rootData);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
validateExpressSchema(schema as ExpressSchema);
|
|
151
|
+
|
|
152
|
+
if (['string', 'number', 'boolean', 'integer'].includes(schema.type)) {
|
|
153
|
+
return createPrimitiveTransformer(schema);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (schema.resolve) {
|
|
157
|
+
console.warn('Data type `array` or `object` does not support resolve function');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (schema.type === 'object') {
|
|
161
|
+
if (isEmpty(schema.properties)) {
|
|
162
|
+
return createPlainObjectTransformer();
|
|
163
|
+
}
|
|
164
|
+
return createObjectTransformer(schema.properties as Record<string, SchemaExpress>, createDataTransformer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(schema.properties)) {
|
|
168
|
+
return createArrayTransformer(schema.properties as [string, SchemaExpress], createDataTransformer);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 处理数组对象类型
|
|
172
|
+
return createArrayObjectTransformer(schema.properties as Record<string, SchemaExpress>, createDataTransformer);
|
|
173
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { createPathAccessor } from './createPathAccessor.js';
|
|
5
|
+
|
|
6
|
+
describe('createPathAccessor', () => {
|
|
7
|
+
describe('空路径', () => {
|
|
8
|
+
test('应该返回原始数据', () => {
|
|
9
|
+
const accessor = createPathAccessor([]);
|
|
10
|
+
const data = { name: 'test', value: 123 };
|
|
11
|
+
assert.deepEqual(accessor(data), data);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('应该返回原始数组', () => {
|
|
15
|
+
const accessor = createPathAccessor([]);
|
|
16
|
+
const data = [1, 2, 3];
|
|
17
|
+
assert.deepEqual(accessor(data), data);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('应该返回原始基本类型', () => {
|
|
21
|
+
const accessor = createPathAccessor([]);
|
|
22
|
+
assert.equal(accessor('hello'), 'hello');
|
|
23
|
+
assert.equal(accessor(42), 42);
|
|
24
|
+
assert.equal(accessor(null), null);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('单层路径 - 对象访问', () => {
|
|
29
|
+
test('应该正确访问对象属性', () => {
|
|
30
|
+
const accessor = createPathAccessor(['name']);
|
|
31
|
+
const data = { name: 'Alice', age: 30 };
|
|
32
|
+
assert.equal(accessor(data), 'Alice');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('不存在的属性应该返回 null', () => {
|
|
36
|
+
const accessor = createPathAccessor(['missing']);
|
|
37
|
+
const data = { name: 'Alice' };
|
|
38
|
+
assert.equal(accessor(data), null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('null 或 undefined 数据应该返回 null', () => {
|
|
42
|
+
const accessor = createPathAccessor(['name']);
|
|
43
|
+
assert.equal(accessor(null), null);
|
|
44
|
+
assert.equal(accessor(undefined), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('应该访问嵌套对象', () => {
|
|
48
|
+
const accessor = createPathAccessor(['user']);
|
|
49
|
+
const data = { user: { name: 'Bob', age: 25 } };
|
|
50
|
+
assert.deepEqual(accessor(data), { name: 'Bob', age: 25 });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('单层路径 - 数组访问', () => {
|
|
55
|
+
test('应该通过 createArrayAccessor 访问数组', () => {
|
|
56
|
+
// 假设 createArrayAccessor 支持索引访问
|
|
57
|
+
const accessor = createPathAccessor(['0']);
|
|
58
|
+
const data = ['first', 'second', 'third'];
|
|
59
|
+
// 这里的行为取决于 createArrayAccessor 的实现
|
|
60
|
+
const result = accessor(data);
|
|
61
|
+
assert.ok(result !== null);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('多层路径 - 嵌套对象', () => {
|
|
66
|
+
test('应该访问嵌套对象的属性', () => {
|
|
67
|
+
const accessor = createPathAccessor(['user', 'profile', 'name']);
|
|
68
|
+
const data = {
|
|
69
|
+
user: {
|
|
70
|
+
profile: {
|
|
71
|
+
name: 'Charlie',
|
|
72
|
+
email: 'charlie@example.com',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
assert.equal(accessor(data), 'Charlie');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('中间路径不存在应该返回 null', () => {
|
|
80
|
+
const accessor = createPathAccessor(['user', 'profile', 'name']);
|
|
81
|
+
const data = { user: {} };
|
|
82
|
+
assert.equal(accessor(data), null);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('中间路径为 null 应该返回 null', () => {
|
|
86
|
+
const accessor = createPathAccessor(['user', 'profile', 'name']);
|
|
87
|
+
const data = { user: null };
|
|
88
|
+
assert.equal(accessor(data), null);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('混合路径 - 对象和数组', () => {
|
|
93
|
+
test('应该访问对象中的数组元素', () => {
|
|
94
|
+
const accessor = createPathAccessor(['users', '0', 'name']);
|
|
95
|
+
const data = {
|
|
96
|
+
users: [
|
|
97
|
+
{ name: 'David', age: 28 },
|
|
98
|
+
{ name: 'Eve', age: 32 },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
// 行为取决于 createArrayAccessor 的实现
|
|
102
|
+
const result = accessor(data);
|
|
103
|
+
assert.ok(result !== undefined);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('应该访问数组中的对象属性', () => {
|
|
107
|
+
const accessor = createPathAccessor(['0', 'name']);
|
|
108
|
+
const data = [
|
|
109
|
+
{ name: 'Frank', age: 35 },
|
|
110
|
+
{ name: 'Grace', age: 29 },
|
|
111
|
+
];
|
|
112
|
+
const result = accessor(data);
|
|
113
|
+
assert.ok(result !== undefined);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('复杂嵌套结构', () => {
|
|
117
|
+
const accessor = createPathAccessor(['company', 'departments', '0', 'manager', 'name']);
|
|
118
|
+
const data = {
|
|
119
|
+
company: {
|
|
120
|
+
departments: [
|
|
121
|
+
{
|
|
122
|
+
name: 'Engineering',
|
|
123
|
+
manager: { name: 'Helen', id: 101 },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'Sales',
|
|
127
|
+
manager: { name: 'Ivan', id: 102 },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const result = accessor(data);
|
|
133
|
+
assert.ok(result !== undefined);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('边界情况', () => {
|
|
138
|
+
test('路径指向 undefined 值', () => {
|
|
139
|
+
const accessor = createPathAccessor(['value']);
|
|
140
|
+
const data = { value: undefined };
|
|
141
|
+
assert.equal(accessor(data), undefined);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('路径指向 0 值', () => {
|
|
145
|
+
const accessor = createPathAccessor(['count']);
|
|
146
|
+
const data = { count: 0 };
|
|
147
|
+
assert.equal(accessor(data), 0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('路径指向空字符串', () => {
|
|
151
|
+
const accessor = createPathAccessor(['text']);
|
|
152
|
+
const data = { text: '' };
|
|
153
|
+
assert.equal(accessor(data), '');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('路径指向 false 值', () => {
|
|
157
|
+
const accessor = createPathAccessor(['flag']);
|
|
158
|
+
const data = { flag: false };
|
|
159
|
+
assert.equal(accessor(data), false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('空对象', () => {
|
|
163
|
+
const accessor = createPathAccessor(['key']);
|
|
164
|
+
const data = {};
|
|
165
|
+
assert.equal(accessor(data), null);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('空数组', () => {
|
|
169
|
+
const accessor = createPathAccessor(['0']);
|
|
170
|
+
const data = [];
|
|
171
|
+
const result = accessor(data);
|
|
172
|
+
// 行为取决于 createArrayAccessor 的实现
|
|
173
|
+
assert.ok(result !== undefined);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('特殊键名', () => {
|
|
178
|
+
test('包含特殊字符的键名', () => {
|
|
179
|
+
const accessor = createPathAccessor(['user-name']);
|
|
180
|
+
const data = { 'user-name': 'Jack' };
|
|
181
|
+
assert.equal(accessor(data), 'Jack');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('数字字符串键名', () => {
|
|
185
|
+
const accessor = createPathAccessor(['123']);
|
|
186
|
+
const data = { 123: 'numeric key' };
|
|
187
|
+
assert.equal(accessor(data), 'numeric key');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('包含点号的键名', () => {
|
|
191
|
+
const accessor = createPathAccessor(['user.name']);
|
|
192
|
+
const data = { 'user.name': 'Kelly' };
|
|
193
|
+
assert.equal(accessor(data), 'Kelly');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('性能和缓存', () => {
|
|
198
|
+
test('多次调用相同访问器应该得到一致结果', () => {
|
|
199
|
+
const accessor = createPathAccessor(['user', 'name']);
|
|
200
|
+
const data = { user: { name: 'Liam' } };
|
|
201
|
+
|
|
202
|
+
assert.equal(accessor(data), 'Liam');
|
|
203
|
+
assert.equal(accessor(data), 'Liam');
|
|
204
|
+
assert.equal(accessor(data), 'Liam');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('访问器应该是纯函数', () => {
|
|
208
|
+
const accessor = createPathAccessor(['value']);
|
|
209
|
+
const data1 = { value: 'first' };
|
|
210
|
+
const data2 = { value: 'second' };
|
|
211
|
+
|
|
212
|
+
assert.equal(accessor(data1), 'first');
|
|
213
|
+
assert.equal(accessor(data2), 'second');
|
|
214
|
+
assert.equal(accessor(data1), 'first');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createArrayAccessor } from './createArrayAccessor.js';
|
|
2
|
+
|
|
3
|
+
const getObjectValue = (key: string) => (obj: Record<string, unknown> | null): unknown => {
|
|
4
|
+
if (obj == null || !Object.hasOwnProperty.call(obj, key)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return obj[key];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const traversePath = (target: unknown, pathSegments: string[]): unknown => {
|
|
11
|
+
const [currentKey, ...remainingPath] = pathSegments;
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(target)) {
|
|
14
|
+
const value = createArrayAccessor(currentKey)(target);
|
|
15
|
+
if (value == null || remainingPath.length === 0) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return traversePath(value, remainingPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const value = getObjectValue(currentKey)(target as Record<string, unknown>);
|
|
22
|
+
if (value == null || remainingPath.length === 0) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return traversePath(value, remainingPath);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function createPathAccessor(pathSegments: string[]): (data: unknown) => unknown {
|
|
30
|
+
if (pathSegments.length === 0) {
|
|
31
|
+
return (data: unknown) => data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (pathSegments.length === 1) {
|
|
35
|
+
const key = pathSegments[0];
|
|
36
|
+
return (data: unknown) => {
|
|
37
|
+
if (Array.isArray(data)) {
|
|
38
|
+
return createArrayAccessor(key)(data);
|
|
39
|
+
}
|
|
40
|
+
return getObjectValue(key)(data as Record<string, unknown>);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (data: unknown) => traversePath(data, pathSegments);
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createDataTransformer } from './createDataTransformer.js';
|
|
2
|
+
import { type DataType,parseValueByType } from './parseValueByType.js';
|
|
3
|
+
import { type ExpressSchema,validateExpressSchema } from './validateExpressSchema.js';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createDataTransformer,
|
|
7
|
+
type DataType,
|
|
8
|
+
type ExpressSchema,
|
|
9
|
+
parseValueByType,
|
|
10
|
+
validateExpressSchema,
|
|
11
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { parseDotPath } from './parseDotPath.js';
|
|
5
|
+
|
|
6
|
+
const testCases = (description: string, cases: Array<[string, string[], string?]>): void => {
|
|
7
|
+
test(description, async (t) => {
|
|
8
|
+
for (const [input, expected, testDesc] of cases) {
|
|
9
|
+
await t.test(testDesc || `输入: "${input}"`, () => {
|
|
10
|
+
assert.deepStrictEqual(parseDotPath(input), expected);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const errorCases = (description: string, cases: Array<[string, string | null, string?]>): void => {
|
|
17
|
+
test(description, async (t) => {
|
|
18
|
+
for (const [input, errorMsg, testDesc] of cases) {
|
|
19
|
+
await t.test(testDesc || `输入: "${input}"`, () => {
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => parseDotPath(input),
|
|
22
|
+
{
|
|
23
|
+
name: 'Error',
|
|
24
|
+
message: errorMsg || `Path "${input}" parse failed: contains empty segment`,
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
testCases('基本路径解析', [
|
|
33
|
+
['', [], '空字符串应返回空数组'],
|
|
34
|
+
['.', [], '单个点应返回空数组'],
|
|
35
|
+
['single', ['single'], '单个段'],
|
|
36
|
+
['a.b', ['a', 'b'], '两个段'],
|
|
37
|
+
['a.b.c', ['a', 'b', 'c'], '三个段'],
|
|
38
|
+
['first.second.third.fourth', ['first', 'second', 'third', 'fourth'], '多个段'],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
testCases('前导点处理', [
|
|
42
|
+
['.aa', ['aa'], '前导点 + 单段'],
|
|
43
|
+
['.a.b.c', ['a', 'b', 'c'], '前导点 + 多段'],
|
|
44
|
+
['.aa.bb.cc', ['aa', 'bb', 'cc'], '前导点 + 多段(长名称)'],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
testCases('转义点号处理', [
|
|
48
|
+
['a\\.b', ['a.b'], '单个转义点'],
|
|
49
|
+
['a\\.b.c', ['a.b', 'c'], '转义点在中间段'],
|
|
50
|
+
['a\\.b\\.c', ['a.b.c'], '多个转义点在同一段'],
|
|
51
|
+
['a\\.b\\.c.d', ['a.b.c', 'd'], '连续转义点 + 普通分隔'],
|
|
52
|
+
['a.b\\.c.d', ['a', 'b.c', 'd'], '混合转义和非转义'],
|
|
53
|
+
['\\.aa', ['.aa'], '转义点在开头'],
|
|
54
|
+
['.\\.aa', ['.aa'], '前导点 + 转义点'],
|
|
55
|
+
['a.b\\.', ['a', 'b.'], '转义点在结尾'],
|
|
56
|
+
['a.b.c\\.', ['a', 'b', 'c.'], '转义点在最后段末尾'],
|
|
57
|
+
['\\.\\.\\.', ['...'], '多个连续转义点'],
|
|
58
|
+
['a\\\\.b.c', ['a\\.b', 'c'], '转义反斜杠 + 点'],
|
|
59
|
+
['a\\\\.b\\\\.c', ['a\\.b\\.c'], '多个转义反斜杠'],
|
|
60
|
+
['a\\.\\.b', ['a..b'], '转义的连续点号'],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
testCases('特殊字符处理', [
|
|
64
|
+
['aa. .bb', ['aa', ' ', 'bb'], '空格段'],
|
|
65
|
+
['a-b.c_d.e123', ['a-b', 'c_d', 'e123'], '连字符和下划线'],
|
|
66
|
+
['用户.姓名.firstName', ['用户', '姓名', 'firstName'], '中文字符'],
|
|
67
|
+
['0.1.2', ['0', '1', '2'], '数字段'],
|
|
68
|
+
['_private.public', ['_private', 'public'], '下划线开头'],
|
|
69
|
+
['$var.prop', ['$var', 'prop'], '美元符号开头'],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
testCases('长路径处理', [
|
|
73
|
+
[
|
|
74
|
+
'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p',
|
|
75
|
+
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'],
|
|
76
|
+
'16段长路径',
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
'a\\.b.c\\.d.e',
|
|
80
|
+
['a.b', 'c.d', 'e'],
|
|
81
|
+
'带转义的混合路径',
|
|
82
|
+
],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
errorCases('错误:空段检测', [
|
|
86
|
+
['..aa', null, '开头的连续点'],
|
|
87
|
+
['a..b', null, '中间的连续点'],
|
|
88
|
+
['aa..bb', null, '中间的连续点(长名称)'],
|
|
89
|
+
['a...b', null, '多个连续点'],
|
|
90
|
+
['bb.', null, '结尾的点'],
|
|
91
|
+
['a.b.', null, '多段后结尾的点'],
|
|
92
|
+
['a.b.c.', null, '更多段后结尾的点'],
|
|
93
|
+
['.a.', null, '前导点 + 结尾点'],
|
|
94
|
+
['.a\\.b..c\\.d.e\\..f', null, '复杂的空段错误'],
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
test('parseDotPath - 完整兼容性测试', () => {
|
|
98
|
+
const validCases = [
|
|
99
|
+
['', []],
|
|
100
|
+
['.', []],
|
|
101
|
+
['.aa', ['aa']],
|
|
102
|
+
['a.b.c', ['a', 'b', 'c']],
|
|
103
|
+
['.aa.bb.cc', ['aa', 'bb', 'cc']],
|
|
104
|
+
['aa.bb.cc', ['aa', 'bb', 'cc']],
|
|
105
|
+
['aa\\.bb.cc', ['aa.bb', 'cc']],
|
|
106
|
+
['.\\.aa', ['.aa']],
|
|
107
|
+
['\\.aa', ['.aa']],
|
|
108
|
+
['aa. .bb', ['aa', ' ', 'bb']],
|
|
109
|
+
['a\\.b\\.c', ['a.b.c']],
|
|
110
|
+
['a\\.b.c\\.d.e', ['a.b', 'c.d', 'e']],
|
|
111
|
+
['a.b.c\\.', ['a', 'b', 'c.']],
|
|
112
|
+
['\\.\\.\\.', ['...']],
|
|
113
|
+
['a\\\\.b.c', ['a\\.b', 'c']],
|
|
114
|
+
['a\\\\.b\\\\.c', ['a\\.b\\.c']],
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
validCases.forEach(([input, expected]) => {
|
|
118
|
+
assert.deepStrictEqual(parseDotPath(input), expected, `Failed for input: "${input}"`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const errorInputs = [
|
|
122
|
+
'..aa',
|
|
123
|
+
'aa..bb',
|
|
124
|
+
'bb.',
|
|
125
|
+
'a.b.c.',
|
|
126
|
+
'.a\\.b..c\\.d.e\\..f',
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
errorInputs.forEach((input) => {
|
|
130
|
+
assert.throws(() => parseDotPath(input), `Should throw for input: "${input}"`);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function parseDotPath(path: string): string[] {
|
|
2
|
+
const normalizedPath = path.startsWith('.') ? path.slice(1) : path;
|
|
3
|
+
if (normalizedPath === '') {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const segments = normalizedPath
|
|
7
|
+
.split(/(?<!\\)\./)
|
|
8
|
+
.map((segment) => segment.replace(/\\\./g, '.'));
|
|
9
|
+
if (segments.some((segment) => segment === '')) {
|
|
10
|
+
throw new Error(`Path "${path}" parse failed: contains empty segment`);
|
|
11
|
+
}
|
|
12
|
+
return segments;
|
|
13
|
+
}
|