Skip to content

Commit e142111

Browse files
committed
🚧 work in progress tree mutation tdd
1 parent b3cb43a commit e142111

File tree

5 files changed

+450
-1
lines changed

5 files changed

+450
-1
lines changed

src/TreeOperations/interface.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { ParserField, TypeDefinition } from '@/Models';
2+
import { createParserField } from '@/shared';
3+
import { filterNotNull } from '@/TreeOperations/shared';
4+
5+
const updateNodeByInterfaceAddField = (interfaceNode: ParserField) => (node: ParserField) => {
6+
interfaceNode.args.forEach((ia) => {
7+
const sameFieldInNode = node.args.findIndex((na) => na.name === ia.name);
8+
if (sameFieldInNode === -1) {
9+
node.args.push(
10+
createParserField({
11+
...ia,
12+
fromInterface: [interfaceNode.name],
13+
}),
14+
);
15+
return;
16+
}
17+
});
18+
};
19+
20+
export const deleteFieldFromInterface = (
21+
nodes: ParserField[],
22+
updatedInterfaceNode: ParserField,
23+
fieldName: string,
24+
) => {
25+
nodes
26+
.filter((n) => n.interfaces.includes(updatedInterfaceNode.name))
27+
.forEach((nodeWithThisInterface) => {
28+
nodeWithThisInterface.args = nodeWithThisInterface.args
29+
.map((a) => {
30+
if (a.name !== fieldName || !a.fromInterface) return a;
31+
if (a.fromInterface.length === 1) return null;
32+
return {
33+
...a,
34+
fromInterface: a.fromInterface.filter((ai) => ai !== updatedInterfaceNode.name),
35+
};
36+
})
37+
.filter(filterNotNull);
38+
});
39+
};
40+
41+
export const updateInterfaceNodeAddField = (nodes: ParserField[], interfaceNode: ParserField) => {
42+
const updateWithInterface = updateNodeByInterfaceAddField(interfaceNode);
43+
nodes.filter((n) => n.interfaces.includes(interfaceNode.name)).forEach(updateWithInterface);
44+
};
45+
const replaceField = (oldField: ParserField, newField: ParserField) => (node: ParserField) => {
46+
const fieldToChange = node.args.findIndex((na) => na.name === oldField.name);
47+
const argToChange = node.args[fieldToChange];
48+
if (node.args[fieldToChange] && argToChange.fromInterface) {
49+
node.args[fieldToChange] = createParserField({
50+
...newField,
51+
fromInterface: [...argToChange.fromInterface],
52+
});
53+
}
54+
};
55+
56+
export const changeInterfaceField = (
57+
nodes: ParserField[],
58+
interfaceNode: ParserField,
59+
oldField: ParserField,
60+
newField: ParserField,
61+
) => {
62+
const updateWithOldField = replaceField(oldField, newField);
63+
nodes.filter((n) => n.interfaces.includes(interfaceNode.name)).forEach(updateWithOldField);
64+
};
65+
66+
export const renameInterfaceNode = (nodes: ParserField[], newName: string, oldName: string) => {
67+
nodes
68+
.filter((n) => n.interfaces.includes(oldName))
69+
.forEach((n) => {
70+
n.interfaces = n.interfaces.filter((i) => i !== oldName).concat([newName]);
71+
n.args.forEach((a) => {
72+
a.fromInterface = a.fromInterface?.filter((fi) => fi !== oldName).concat([newName]);
73+
});
74+
});
75+
};
76+
const getAllConnectedInterfaces = (nodes: ParserField[], interfaces: string[]) => {
77+
const computedInterfaces: string[] = [];
78+
const computeConnectedInterfaces = (nodes: ParserField[], interfaces: string[], interfacesToPush: string[]) => {
79+
const allInterfaces = nodes.filter((ni) => ni.data.type === TypeDefinition.InterfaceTypeDefinition);
80+
interfacesToPush.push(...interfaces.filter((ii) => !interfacesToPush.includes(ii)));
81+
for (const i of interfaces) {
82+
const hasInterface = allInterfaces.find((interfaceObject) => interfaceObject.name === i);
83+
if (hasInterface?.interfaces && hasInterface.interfaces.length) {
84+
computeConnectedInterfaces(nodes, hasInterface.interfaces, interfacesToPush);
85+
}
86+
}
87+
};
88+
computeConnectedInterfaces(nodes, interfaces, computedInterfaces);
89+
return computedInterfaces;
90+
};
91+
export const implementInterfaceOnNode = (nodes: ParserField[], node: ParserField, interfaceNode: ParserField) => {
92+
const interfacesToPush = getAllConnectedInterfaces(nodes, [interfaceNode.name]);
93+
node.interfaces.push(...interfacesToPush);
94+
const argsToPush = interfaceNode.args?.filter((a) => !node.args?.find((na) => na.name === a.name)) || [];
95+
node.args = node.args.map((a) => {
96+
if (interfaceNode.args.find((interfaceArg) => interfaceArg.name === a.name)) {
97+
return {
98+
...a,
99+
fromInterface: (a.fromInterface || []).concat([interfaceNode.name]),
100+
};
101+
}
102+
return a;
103+
});
104+
node.args = node.args?.concat(
105+
argsToPush.map((atp) => ({
106+
...atp,
107+
fromInterface: [interfaceNode.name],
108+
})),
109+
);
110+
};
111+
112+
export const deImplementInterfaceOnNode = (nodes: ParserField[], node: ParserField, interfaceName: string) => {
113+
const interfacesToDeImplement = getAllConnectedInterfaces(nodes, [interfaceName]);
114+
node.interfaces = node.interfaces.filter((ni) => !interfacesToDeImplement.includes(ni));
115+
node.args = node.args
116+
.map((a) => {
117+
if (!a.fromInterface?.length) return a;
118+
a.fromInterface = a.fromInterface.filter((fi) => !interfacesToDeImplement.includes(fi));
119+
if (a.fromInterface.length === 0) return null;
120+
return a;
121+
})
122+
.filter(filterNotNull);
123+
};
124+
125+
// export const deleteInterfaceExtensionNode = (nodes: ParserField[], node: ParserField) => {};
126+
127+
// export const deleteInterfaceNode = (nodes: ParserField[], node: ParserField) => {};

src/TreeOperations/shared.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { AllTypes, FieldType, Options, ParserField, TypeExtension } from '@/Models';
2+
import { generateNodeId, getTypeName } from '@/shared';
3+
4+
export function filterNotNull<T>(t: T | null): t is T {
5+
return t !== null;
6+
}
7+
8+
export const changeTypeName = (field: FieldType, newName: string) => {
9+
const changeFieldName = (field: FieldType, newName: string): void => {
10+
if (field.type === Options.array) {
11+
return changeFieldName(field.nest, newName);
12+
} else if (field.type === Options.required) {
13+
return changeFieldName(field.nest, newName);
14+
}
15+
field.name = newName;
16+
};
17+
changeFieldName(field, newName);
18+
return field;
19+
};
20+
21+
export const changeNodeOptions = (field: FieldType, newOption: Options) => {
22+
const changeOptions = (field: FieldType, newOption: Options) => {
23+
if (field.type !== Options.array && newOption === Options.array) {
24+
if (field.type === Options.required) {
25+
}
26+
changeOptions(field, Options.array);
27+
} else if (field.type !== newOption && newOption === Options.required) {
28+
changeOptions(field, Options.required);
29+
}
30+
field.type = Options.name;
31+
};
32+
changeOptions(field, newOption);
33+
return field;
34+
};
35+
36+
export const resolveValueFieldType = (
37+
name: string,
38+
fType: FieldType,
39+
isRequired = false,
40+
fn: (str: string) => string = (x) => x,
41+
): string => {
42+
if (fType.type === Options.name) {
43+
return fn(isRequired ? name : `${name} | undefined | null`);
44+
}
45+
if (fType.type === Options.array) {
46+
return resolveValueFieldType(
47+
name,
48+
fType.nest,
49+
false,
50+
isRequired ? (x) => `Array<${fn(x)}>` : (x) => `Array<${fn(x)}> | undefined | null`,
51+
);
52+
}
53+
if (fType.type === Options.required) {
54+
return resolveValueFieldType(name, fType.nest, true, fn);
55+
}
56+
throw new Error('Invalid field type');
57+
};
58+
59+
export const isExtensionNode = (t: AllTypes) =>
60+
!![
61+
TypeExtension.EnumTypeExtension,
62+
TypeExtension.InputObjectTypeExtension,
63+
TypeExtension.InterfaceTypeExtension,
64+
TypeExtension.ObjectTypeExtension,
65+
TypeExtension.ScalarTypeExtension,
66+
TypeExtension.UnionTypeExtension,
67+
].find((o) => o === t);
68+
69+
export const regenerateId = (n: ParserField) => {
70+
const id = generateNodeId(n.name, n.data.type, n.args);
71+
n.id = id;
72+
return n;
73+
};
74+
75+
export const ChangeRelatedNode = ({
76+
newName,
77+
node,
78+
oldName,
79+
}: {
80+
oldName: string;
81+
newName: string;
82+
node: ParserField;
83+
}) => {
84+
const typeName = getTypeName(node.type.fieldType);
85+
if (typeName === oldName) {
86+
changeTypeName(node.type.fieldType, newName);
87+
}
88+
if (node.args) {
89+
node.args.forEach((n) => ChangeRelatedNode({ oldName, newName, node: n }));
90+
}
91+
regenerateId(node);
92+
};
93+
94+
export const ChangeAllRelatedNodes = ({
95+
newName,
96+
nodes,
97+
oldName,
98+
}: {
99+
nodes: ParserField[];
100+
oldName: string;
101+
newName: string;
102+
}) => {
103+
nodes.forEach((n) => ChangeRelatedNode({ oldName, newName, node: n }));
104+
};

src/TreeOperations/tree.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { ParserField, TypeDefinition, Options, TypeSystemDefinition, TypeExtension, ParserTree } from '@/Models';
2+
import { getTypeName, createParserField } from '@/shared';
3+
import {
4+
deleteFieldFromInterface,
5+
changeInterfaceField,
6+
updateInterfaceNodeAddField,
7+
renameInterfaceNode,
8+
implementInterfaceOnNode,
9+
deImplementInterfaceOnNode,
10+
} from '@/TreeOperations/interface';
11+
import { ChangeAllRelatedNodes, filterNotNull, isExtensionNode, regenerateId } from '@/TreeOperations/shared';
12+
13+
export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
14+
const mutateParentIfField = (node: ParserField, nodeId: string) => {
15+
if (node.data.type === TypeSystemDefinition.FieldDefinition) {
16+
const parentNode = allNodes.find((an) => an.args.some((a) => a.id === nodeId));
17+
if (!parentNode) throw new Error('Invalid field definition');
18+
const fieldIndex = parentNode.args.findIndex((a) => a.id == nodeId);
19+
updateFieldOnNode(parentNode, fieldIndex, node);
20+
return;
21+
}
22+
};
23+
const deleteFieldFromNode = (n: ParserField, i: number) => {
24+
const nodeId = n.id;
25+
const argName = n.args[i].name;
26+
if (n.data.type === TypeDefinition.InterfaceTypeDefinition) {
27+
deleteFieldFromInterface(tree.nodes, n, argName);
28+
}
29+
n.args.splice(i, 1);
30+
regenerateId(n);
31+
mutateParentIfField(n, nodeId);
32+
};
33+
34+
const updateFieldOnNode = (node: ParserField, i: number, updatedField: ParserField) => {
35+
const oldField = JSON.parse(JSON.stringify(node.args[i]));
36+
const nodeId = node.id;
37+
if (node.data.type === TypeDefinition.InterfaceTypeDefinition) {
38+
changeInterfaceField(tree.nodes, node, oldField, updatedField);
39+
}
40+
node.args[i] = updatedField;
41+
regenerateId(node);
42+
mutateParentIfField(node, nodeId);
43+
};
44+
45+
const addFieldToNode = (node: ParserField, { id, ...f }: ParserField, name?: string) => {
46+
let newName = name || f.name[0].toLowerCase() + f.name.slice(1);
47+
const existingNodes = node.args?.filter((a) => a.name.match(`${newName}\d?`)) || [];
48+
if (existingNodes.length > 0) {
49+
newName = `${newName}${existingNodes.length}`;
50+
}
51+
const nodeId = id;
52+
node.args?.push(
53+
createParserField({
54+
...f,
55+
directives: [],
56+
interfaces: [],
57+
args: [],
58+
type: {
59+
fieldType: {
60+
name: f.name,
61+
type: Options.name,
62+
},
63+
},
64+
name: newName,
65+
}),
66+
);
67+
if (node.data.type === TypeDefinition.InterfaceTypeDefinition) {
68+
updateInterfaceNodeAddField(tree.nodes, node);
69+
}
70+
mutateParentIfField(node, nodeId);
71+
};
72+
const renameNode = (node: ParserField, newName: string) => {
73+
const isError = allNodes.map((n) => n.name).includes(newName);
74+
if (isError) {
75+
return;
76+
}
77+
if (node.data.type === TypeDefinition.InterfaceTypeDefinition) {
78+
renameInterfaceNode(tree.nodes, newName, node.name);
79+
}
80+
ChangeAllRelatedNodes({
81+
newName,
82+
nodes: tree.nodes,
83+
oldName: node.name,
84+
});
85+
node.name = newName;
86+
regenerateId(node);
87+
};
88+
const removeNode = (node: ParserField) => {
89+
const deletedNode = tree.nodes.findIndex((n) => n === node);
90+
if (deletedNode === -1) throw new Error('Error deleting a node');
91+
const allNodes = [...tree.nodes];
92+
// co jak usuwamy extension interface
93+
if (node.data.type === TypeExtension.InterfaceTypeExtension) {
94+
}
95+
allNodes.splice(deletedNode, 1);
96+
tree.nodes.forEach((n) => {
97+
n.args = n.args
98+
.filter((a) => {
99+
const tName = getTypeName(a.type.fieldType);
100+
if (tName === node.name && !isExtensionNode(node.data.type)) {
101+
return null;
102+
}
103+
return a;
104+
})
105+
.filter(filterNotNull);
106+
});
107+
};
108+
const implementInterface = (node: ParserField, interfaceNode: ParserField) => {
109+
implementInterfaceOnNode(tree.nodes, node, interfaceNode);
110+
};
111+
const deImplementInterface = (node: ParserField, interfaceName: string) => {
112+
deImplementInterfaceOnNode(tree.nodes, node, interfaceName);
113+
};
114+
return {
115+
deleteFieldFromNode,
116+
updateFieldOnNode,
117+
addFieldToNode,
118+
renameNode,
119+
removeNode,
120+
implementInterface,
121+
deImplementInterface,
122+
};
123+
};

0 commit comments

Comments
 (0)