Skip to content

Commit 08254aa

Browse files
authored
Add new alphabetical sort utility in schema tab and alphabetical sort in GUI view (#979)
* Add new alphabetical sort utility in schema tab and alphabetical sort in GUI view * apply formatting changes --------- Co-authored-by: Logende <Logende@users.noreply.github.com>
1 parent 02ea3a9 commit 08254aa

8 files changed

Lines changed: 469 additions & 88 deletions

File tree

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import {describe, expect, it} from 'vitest';
2+
import {PropertySorting} from '@/settings/settingsTypes';
3+
import {
4+
childrenInAlphabeticalOrder,
5+
childrenInDataOrder,
6+
childrenInPriorityOrder,
7+
childrenInSchemaOrder,
8+
sortObjectChildren,
9+
sortSchemaPropertiesAlphabetically,
10+
} from '@/components/panels/gui-editor/sortingUtils';
11+
12+
// The sort strategies only ever read `node.data.name`, so stub nodes with just that
13+
// field are enough. We avoid importing the real GuiEditorTreeNode type to keep this
14+
// test free of the JsonSchemaWrapper module graph (which has a runtime circular import
15+
// that breaks in a vitest module-load context).
16+
type StubNode = {data: {name: string}};
17+
function makeNode(name: string): StubNode {
18+
return {data: {name}};
19+
}
20+
21+
interface FakeSchema {
22+
properties: Record<string, {deprecated?: boolean}>;
23+
required: string[];
24+
isRequired(key: string): boolean;
25+
}
26+
27+
function makeSchema(
28+
properties: Record<string, {deprecated?: boolean}>,
29+
required: string[] = []
30+
): FakeSchema {
31+
return {
32+
properties,
33+
required,
34+
isRequired: (key: string) => required.includes(key),
35+
};
36+
}
37+
38+
/**
39+
* Builds the two child-builder callbacks the sort strategies use. `schemaKeys` are the
40+
* keys defined under the schema's `properties`; `dataKeys` are the keys present in the
41+
* data. Each builder respects the filter passed to it.
42+
*/
43+
function builders(schemaKeys: string[], dataKeys: string[]) {
44+
const buildSchemaChildren = (filter: (key: string) => boolean) =>
45+
schemaKeys.filter(filter).map(makeNode) as any;
46+
const buildDataChildren = (filter: (key: string) => boolean) =>
47+
dataKeys.filter(filter).map(makeNode) as any;
48+
return {buildSchemaChildren, buildDataChildren};
49+
}
50+
51+
function namesOf(nodes: any[]): string[] {
52+
return nodes.map(n => String(n.data.name));
53+
}
54+
55+
describe('sortObjectChildren — schema order', () => {
56+
it('returns schema-declared properties first, then data-only keys', () => {
57+
const schema = makeSchema({name: {}, age: {}, zip: {}}) as any;
58+
const {buildSchemaChildren, buildDataChildren} = builders(
59+
['name', 'age', 'zip'],
60+
['name', 'extra1', 'extra2']
61+
);
62+
63+
const result = childrenInSchemaOrder(schema, buildSchemaChildren, buildDataChildren);
64+
65+
expect(namesOf(result)).toEqual(['name', 'age', 'zip', 'extra1', 'extra2']);
66+
});
67+
68+
it('drops data keys that already exist in the schema', () => {
69+
const schema = makeSchema({name: {}}) as any;
70+
const {buildSchemaChildren, buildDataChildren} = builders(['name'], ['name', 'extra']);
71+
72+
const result = childrenInSchemaOrder(schema, buildSchemaChildren, buildDataChildren);
73+
74+
expect(namesOf(result)).toEqual(['name', 'extra']);
75+
});
76+
77+
it('is dispatched by sortObjectChildren for PropertySorting.SCHEMA_ORDER', () => {
78+
const schema = makeSchema({b: {}, a: {}}) as any;
79+
const {buildSchemaChildren, buildDataChildren} = builders(['b', 'a'], []);
80+
81+
const result = sortObjectChildren(
82+
PropertySorting.SCHEMA_ORDER,
83+
schema,
84+
buildSchemaChildren,
85+
buildDataChildren
86+
);
87+
88+
expect(namesOf(result)).toEqual(['b', 'a']);
89+
});
90+
});
91+
92+
describe('sortObjectChildren — data order', () => {
93+
it('returns data properties first, then any remaining schema-only properties', () => {
94+
const {buildSchemaChildren, buildDataChildren} = builders(
95+
['alpha', 'beta', 'gamma'],
96+
['gamma', 'extra']
97+
);
98+
99+
const result = childrenInDataOrder(buildSchemaChildren, buildDataChildren);
100+
101+
// gamma comes from data first, then schema-only alpha/beta in schema order
102+
expect(namesOf(result)).toEqual(['gamma', 'extra', 'alpha', 'beta']);
103+
});
104+
105+
it('is dispatched by sortObjectChildren for PropertySorting.DATA_ORDER', () => {
106+
const schema = makeSchema({a: {}}) as any;
107+
const {buildSchemaChildren, buildDataChildren} = builders(['a'], ['z']);
108+
109+
const result = sortObjectChildren(
110+
PropertySorting.DATA_ORDER,
111+
schema,
112+
buildSchemaChildren,
113+
buildDataChildren
114+
);
115+
116+
expect(namesOf(result)).toEqual(['z', 'a']);
117+
});
118+
});
119+
120+
describe('sortObjectChildren — priority order', () => {
121+
it('groups required, optional, additional (data-only), deprecated', () => {
122+
const schema = makeSchema(
123+
{
124+
reqA: {},
125+
optB: {},
126+
depC: {deprecated: true},
127+
depReqD: {deprecated: true}, // required + deprecated should be required
128+
},
129+
['reqA', 'depReqD']
130+
) as any;
131+
const {buildSchemaChildren, buildDataChildren} = builders(
132+
['reqA', 'optB', 'depC', 'depReqD'],
133+
['reqA', 'optB', 'extra']
134+
);
135+
136+
const result = childrenInPriorityOrder(schema, buildSchemaChildren, buildDataChildren);
137+
138+
// required (reqA, depReqD because it's required even though deprecated), then optional (optB),
139+
// then additional (extra), then deprecated-but-not-required (depC).
140+
expect(namesOf(result)).toEqual(['reqA', 'depReqD', 'optB', 'extra', 'depC']);
141+
});
142+
143+
it('is dispatched by sortObjectChildren for PropertySorting.PRIORITY_ORDER', () => {
144+
const schema = makeSchema({a: {}, b: {}}, ['b']) as any;
145+
const {buildSchemaChildren, buildDataChildren} = builders(['a', 'b'], []);
146+
147+
const result = sortObjectChildren(
148+
PropertySorting.PRIORITY_ORDER,
149+
schema,
150+
buildSchemaChildren,
151+
buildDataChildren
152+
);
153+
154+
expect(namesOf(result)).toEqual(['b', 'a']);
155+
});
156+
});
157+
158+
describe('sortObjectChildren — alphabetical order', () => {
159+
it('merges schema and data-only children into a single alphabetical list', () => {
160+
const schema = makeSchema({zebra: {}, mango: {}}) as any;
161+
const {buildSchemaChildren, buildDataChildren} = builders(
162+
['zebra', 'mango'],
163+
['mango', 'apple', 'kiwi']
164+
);
165+
166+
const result = childrenInAlphabeticalOrder(schema, buildSchemaChildren, buildDataChildren);
167+
168+
expect(namesOf(result)).toEqual(['apple', 'kiwi', 'mango', 'zebra']);
169+
});
170+
171+
it('is dispatched by sortObjectChildren for PropertySorting.ALPHABETICAL_ORDER', () => {
172+
const schema = makeSchema({zebra: {}, apple: {}}) as any;
173+
const {buildSchemaChildren, buildDataChildren} = builders(['zebra', 'apple'], []);
174+
175+
const result = sortObjectChildren(
176+
PropertySorting.ALPHABETICAL_ORDER,
177+
schema,
178+
buildSchemaChildren,
179+
buildDataChildren
180+
);
181+
182+
expect(namesOf(result)).toEqual(['apple', 'zebra']);
183+
});
184+
});
185+
186+
describe('sortSchemaPropertiesAlphabetically', () => {
187+
it('sorts properties keys alphabetically and leaves other keywords untouched', () => {
188+
const result = sortSchemaPropertiesAlphabetically({
189+
type: 'object',
190+
title: 'Person',
191+
properties: {
192+
zip: {type: 'string'},
193+
age: {type: 'number'},
194+
name: {type: 'string'},
195+
},
196+
});
197+
198+
expect(Object.keys((result as any).properties)).toEqual(['age', 'name', 'zip']);
199+
expect((result as any).type).toBe('object');
200+
expect((result as any).title).toBe('Person');
201+
});
202+
203+
it('recurses into nested object schemas, items, and allOf', () => {
204+
const result = sortSchemaPropertiesAlphabetically({
205+
type: 'object',
206+
properties: {
207+
b: {
208+
type: 'object',
209+
properties: {
210+
z: {type: 'number'},
211+
a: {type: 'number'},
212+
},
213+
},
214+
a: {
215+
type: 'array',
216+
items: {
217+
type: 'object',
218+
properties: {
219+
second: {type: 'string'},
220+
first: {type: 'string'},
221+
},
222+
},
223+
},
224+
},
225+
allOf: [
226+
{
227+
type: 'object',
228+
properties: {
229+
zz: {type: 'string'},
230+
aa: {type: 'string'},
231+
},
232+
},
233+
],
234+
});
235+
236+
expect(Object.keys((result as any).properties)).toEqual(['a', 'b']);
237+
expect(Object.keys((result as any).properties.b.properties)).toEqual(['a', 'z']);
238+
expect(Object.keys((result as any).properties.a.items.properties)).toEqual(['first', 'second']);
239+
expect(Object.keys((result as any).allOf[0].properties)).toEqual(['aa', 'zz']);
240+
});
241+
242+
it('sorts $defs, definitions, patternProperties, and dependentSchemas keys', () => {
243+
const result = sortSchemaPropertiesAlphabetically({
244+
$defs: {
245+
zebra: {type: 'string'},
246+
apple: {type: 'string'},
247+
},
248+
definitions: {
249+
beta: {type: 'string'},
250+
alpha: {type: 'string'},
251+
},
252+
patternProperties: {
253+
'^z': {type: 'string'},
254+
'^a': {type: 'string'},
255+
},
256+
dependentSchemas: {
257+
foo: {type: 'object'},
258+
bar: {type: 'object'},
259+
},
260+
});
261+
262+
expect(Object.keys((result as any).$defs)).toEqual(['apple', 'zebra']);
263+
expect(Object.keys((result as any).definitions)).toEqual(['alpha', 'beta']);
264+
expect(Object.keys((result as any).patternProperties)).toEqual(['^a', '^z']);
265+
expect(Object.keys((result as any).dependentSchemas)).toEqual(['bar', 'foo']);
266+
});
267+
268+
it('leaves the order of array elements alone (only object keys are sorted)', () => {
269+
const result = sortSchemaPropertiesAlphabetically({
270+
required: ['zip', 'age', 'name'],
271+
enum: ['c', 'b', 'a'],
272+
});
273+
274+
expect((result as any).required).toEqual(['zip', 'age', 'name']);
275+
expect((result as any).enum).toEqual(['c', 'b', 'a']);
276+
});
277+
278+
it('does not mutate the input', () => {
279+
const input = {
280+
type: 'object',
281+
properties: {
282+
zip: {type: 'string'},
283+
age: {type: 'number'},
284+
},
285+
};
286+
const originalKeys = Object.keys(input.properties);
287+
288+
sortSchemaPropertiesAlphabetically(input);
289+
290+
expect(Object.keys(input.properties)).toEqual(originalKeys);
291+
});
292+
293+
it('passes primitives through', () => {
294+
expect(sortSchemaPropertiesAlphabetically(true)).toBe(true);
295+
expect(sortSchemaPropertiesAlphabetically(false)).toBe(false);
296+
expect(sortSchemaPropertiesAlphabetically(null)).toBe(null);
297+
expect(sortSchemaPropertiesAlphabetically(42)).toBe(42);
298+
expect(sortSchemaPropertiesAlphabetically('hello')).toBe('hello');
299+
});
300+
});

0 commit comments

Comments
 (0)