Skip to content

Commit eb50003

Browse files
authored
♻️ Split code of entityGraph into sub-helpers
1 parent d5bc351 commit eb50003

File tree

4 files changed

+274
-143
lines changed

4 files changed

+274
-143
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Arbitrary } from '../../check/arbitrary/definition/Arbitrary';
2+
import { Value } from '../../check/arbitrary/definition/Value';
3+
import type { Random } from '../../random/generator/Random';
4+
import { Stream } from '../../stream/Stream';
5+
import { safeMap, safePush } from '../../utils/globals';
6+
import { integer } from '../integer';
7+
import { noBias } from '../noBias';
8+
import { option } from '../option';
9+
import { uniqueArray } from '../uniqueArray';
10+
import { createDepthIdentifier, type DepthIdentifier } from './helpers/DepthContext';
11+
import type { Arity, EntityRelations, ProducedLinks } from './interfaces/EntityGraphTypes';
12+
13+
const safeObjectCreate = Object.create;
14+
15+
/** @internal */
16+
function computeLinkIndex(
17+
arity: Arity,
18+
countInTargetType: number,
19+
currentEntityDepth: DepthIdentifier,
20+
mrng: Random,
21+
biasFactor: number | undefined,
22+
): number[] | number | undefined {
23+
const linkArbitrary = noBias(integer({ min: 0, max: countInTargetType }));
24+
switch (arity) {
25+
case '0-1':
26+
return option(linkArbitrary, { nil: undefined, depthIdentifier: currentEntityDepth }).generate(mrng, biasFactor)
27+
.value;
28+
case '1':
29+
return linkArbitrary.generate(mrng, biasFactor).value;
30+
case 'many': {
31+
let randomUnicity = 0;
32+
const values = uniqueArray(linkArbitrary, {
33+
depthIdentifier: currentEntityDepth,
34+
selector: (v) => (v === countInTargetType ? v + ++randomUnicity : v),
35+
}).generate(mrng, biasFactor).value;
36+
let offset = 0;
37+
return safeMap(values, (v) => (v === countInTargetType ? v + offset++ : v));
38+
}
39+
}
40+
}
41+
42+
/** @internal */
43+
class OnTheFlyLinksForEntityGraphArbitrary<
44+
TEntityFields,
45+
TEntityRelations extends EntityRelations<TEntityFields>,
46+
> extends Arbitrary<ProducedLinks<TEntityFields, TEntityRelations>> {
47+
constructor(
48+
readonly relations: TEntityRelations,
49+
readonly defaultEntities: (keyof TEntityFields)[],
50+
) {
51+
super();
52+
}
53+
54+
generate(mrng: Random, biasFactor: number | undefined): Value<ProducedLinks<TEntityFields, TEntityRelations>> {
55+
// The set of all produced links between entities.
56+
const producedLinks: ProducedLinks<TEntityFields, TEntityRelations> = safeObjectCreate(null);
57+
for (const name in this.relations) {
58+
producedLinks[name as Extract<keyof TEntityFields, string>] = [];
59+
}
60+
// Made of any entity whose links have to be created before building the whole graph.
61+
const toBeProducedEntities: { type: keyof TEntityFields; indexInType: number; depth: number }[] = [];
62+
for (const name of this.defaultEntities) {
63+
safePush(toBeProducedEntities, { type: name, indexInType: producedLinks[name].length, depth: 0 });
64+
safePush(producedLinks[name], safeObjectCreate(null));
65+
}
66+
67+
// Ideally toBeProducedEntities should be a queue, but given JavaScript built-ins arrays perform badly in queue mode,
68+
// we decided to consider an always growing array that will grow up to the numer of entities before being dropped.
69+
let lastTreatedEntities = -1;
70+
while (++lastTreatedEntities < toBeProducedEntities.length) {
71+
const currentEntity = toBeProducedEntities[lastTreatedEntities];
72+
const currentRelations = this.relations[currentEntity.type];
73+
const currentProducedLinks = producedLinks[currentEntity.type];
74+
// Create all the links going from the current entity to others
75+
const currentLinks = currentProducedLinks[currentEntity.indexInType];
76+
const currentEntityDepth = createDepthIdentifier();
77+
currentEntityDepth.depth = currentEntity.depth;
78+
for (const name in currentRelations) {
79+
const relation = currentRelations[name];
80+
const targetType = relation.type;
81+
const producedLinksInTargetType = producedLinks[targetType];
82+
const countInTargetType = producedLinksInTargetType.length;
83+
const linkOrLinks = computeLinkIndex(
84+
relation.arity,
85+
producedLinksInTargetType.length,
86+
currentEntityDepth,
87+
mrng,
88+
biasFactor,
89+
);
90+
currentLinks[name] = { type: targetType, index: linkOrLinks };
91+
const links = linkOrLinks === undefined ? [] : typeof linkOrLinks === 'number' ? [linkOrLinks] : linkOrLinks;
92+
for (const link of links) {
93+
if (link >= countInTargetType) {
94+
safePush(toBeProducedEntities, { type: targetType, indexInType: link, depth: currentEntity.depth + 1 }); // indexInType should be equal to producedLinksInTargetType.length
95+
safePush(producedLinksInTargetType, safeObjectCreate(null));
96+
}
97+
}
98+
}
99+
}
100+
// Drop any item from the array
101+
toBeProducedEntities.length = 0;
102+
103+
return new Value(producedLinks, undefined);
104+
}
105+
106+
canShrinkWithoutContext(value: unknown): value is ProducedLinks<TEntityFields, TEntityRelations> {
107+
return false; // for now, we reject any shrink without context
108+
}
109+
110+
shrink(
111+
_value: ProducedLinks<TEntityFields, TEntityRelations>,
112+
_context: unknown | undefined,
113+
): Stream<Value<ProducedLinks<TEntityFields, TEntityRelations>>> {
114+
return Stream.nil(); // for now, we don't support any shrink
115+
}
116+
}
117+
118+
/** @internal */
119+
export function onTheFlyLinksForEntityGraph<TEntityFields, TEntityRelations extends EntityRelations<TEntityFields>>(
120+
relations: TEntityRelations,
121+
defaultEntities: (keyof TEntityFields)[],
122+
): Arbitrary<ProducedLinks<TEntityFields, TEntityRelations>> {
123+
return new OnTheFlyLinksForEntityGraphArbitrary(relations, defaultEntities);
124+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Arbitrary } from '../../check/arbitrary/definition/Arbitrary';
2+
import { array } from '../array';
3+
import { record } from '../record';
4+
import type { RecordConstraints } from '../record';
5+
import type { Arbitraries, UnlinkedEntities } from './interfaces/EntityGraphTypes';
6+
7+
const safeObjectCreate = Object.create;
8+
9+
/** @internal */
10+
export function unlinkedEntitiesForEntityGraph<TEntityFields>(
11+
arbitraries: Arbitraries<TEntityFields>,
12+
countFor: (entityName: keyof TEntityFields) => number,
13+
constraints: Omit<RecordConstraints, 'requiredKeys'>,
14+
): Arbitrary<UnlinkedEntities<TEntityFields>> {
15+
const recordModel: { [K in keyof TEntityFields]: Arbitrary<TEntityFields[K][]> } = safeObjectCreate(null);
16+
for (const name in arbitraries) {
17+
const entityRecordModel = arbitraries[name];
18+
const count = countFor(name);
19+
recordModel[name] = array(record(entityRecordModel, constraints), {
20+
minLength: count,
21+
maxLength: count,
22+
}) as any;
23+
}
24+
// @ts-expect-error - We probably have a fishy typing issue in `record`, as we are supposed to produce `UnlinkedEntities<TEntityFields>`
25+
return record<UnlinkedEntities<TEntityFields>>(recordModel);
26+
}
Lines changed: 19 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,9 @@
1-
import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
2-
import type { Value } from '../check/arbitrary/definition/Value';
3-
import type { Random } from '../random/generator/Random';
4-
import { Stream } from '../stream/Stream';
5-
import { safeMap, safePush } from '../utils/globals';
6-
import type { DepthIdentifier } from './_internals/helpers/DepthContext';
7-
import { createDepthIdentifier } from './_internals/helpers/DepthContext';
8-
import type {
9-
Arbitraries,
10-
Arity,
11-
EntityGraphValue,
12-
EntityRelations,
13-
ProducedLinks,
14-
UnlinkedEntities,
15-
} from './_internals/interfaces/EntityGraphTypes';
1+
import type { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
2+
import type { Arbitraries, EntityGraphValue, EntityRelations } from './_internals/interfaces/EntityGraphTypes';
163
import { unlinkedToLinkedEntitiesMapper } from './_internals/mappers/UnlinkedToLinkedEntities';
17-
import { array } from './array';
18-
import { integer } from './integer';
19-
import { noBias } from './noBias';
20-
import { option } from './option';
21-
import { record } from './record';
22-
import { uniqueArray } from './uniqueArray';
4+
import { onTheFlyLinksForEntityGraph } from './_internals/OnTheFlyLinksForEntityGraphArbitrary';
5+
import { unlinkedEntitiesForEntityGraph } from './_internals/UnlinkedEntitiesForEntityGraph';
236

24-
const safeObjectCreate = Object.create;
257
const safeObjectKeys = Object.keys;
268

279
export type { EntityGraphValue, Arbitraries as EntityGraphArbitraries, EntityRelations as EntityGraphRelations };
@@ -40,126 +22,6 @@ export type EntityGraphContraints = {
4022
noNullPrototype?: boolean;
4123
};
4224

43-
// Internal class containing the implementation
44-
class EntityGraphArbitrary<TEntityFields, TEntityRelations extends EntityRelations<TEntityFields>> extends Arbitrary<
45-
EntityGraphValue<TEntityFields, TEntityRelations>
46-
> {
47-
constructor(
48-
readonly arbitraries: Arbitraries<TEntityFields>,
49-
readonly relations: TEntityRelations,
50-
readonly constraints: { defaultEntities: (keyof TEntityFields)[] } & EntityGraphContraints,
51-
) {
52-
super();
53-
}
54-
55-
private static computeLinkIndex(
56-
arity: Arity,
57-
countInTargetType: number,
58-
currentEntityDepth: DepthIdentifier,
59-
mrng: Random,
60-
biasFactor: number | undefined,
61-
): number[] | number | undefined {
62-
const linkArbitrary = noBias(integer({ min: 0, max: countInTargetType }));
63-
switch (arity) {
64-
case '0-1':
65-
return option(linkArbitrary, { nil: undefined, depthIdentifier: currentEntityDepth }).generate(mrng, biasFactor)
66-
.value;
67-
case '1':
68-
return linkArbitrary.generate(mrng, biasFactor).value;
69-
case 'many': {
70-
let randomUnicity = 0;
71-
const values = uniqueArray(linkArbitrary, {
72-
depthIdentifier: currentEntityDepth,
73-
selector: (v) => (v === countInTargetType ? v + ++randomUnicity : v),
74-
}).generate(mrng, biasFactor).value;
75-
let offset = 0;
76-
return safeMap(values, (v) => (v === countInTargetType ? v + offset++ : v));
77-
}
78-
}
79-
}
80-
81-
generate(mrng: Random, biasFactor: number | undefined): Value<EntityGraphValue<TEntityFields, TEntityRelations>> {
82-
// The set of all produced links between entities.
83-
const producedLinks: ProducedLinks<TEntityFields, TEntityRelations> = safeObjectCreate(null);
84-
for (const name in this.arbitraries) {
85-
producedLinks[name] = [];
86-
}
87-
// Made of any entity whose links have to be created before building the whole graph.
88-
const toBeProducedEntities: { type: keyof TEntityFields; indexInType: number; depth: number }[] = [];
89-
for (const name of this.constraints.defaultEntities) {
90-
safePush(toBeProducedEntities, { type: name, indexInType: producedLinks[name].length, depth: 0 });
91-
safePush(producedLinks[name], safeObjectCreate(null));
92-
}
93-
94-
// STEP I - Producing links between entities...
95-
// Ideally toBeProducedEntities should be a queue, but given JavaScript built-ins arrays perform badly in queue mode,
96-
// we decided to consider an always growing array that will grow up to the numer of entities before being dropped.
97-
let lastTreatedEntities = -1;
98-
while (++lastTreatedEntities < toBeProducedEntities.length) {
99-
const currentEntity = toBeProducedEntities[lastTreatedEntities];
100-
const currentRelations = this.relations[currentEntity.type];
101-
const currentProducedLinks = producedLinks[currentEntity.type];
102-
// Create all the links going from the current entity to others
103-
const currentLinks = currentProducedLinks[currentEntity.indexInType];
104-
const currentEntityDepth = createDepthIdentifier();
105-
currentEntityDepth.depth = currentEntity.depth;
106-
for (const name in currentRelations) {
107-
const relation = currentRelations[name];
108-
const targetType = relation.type;
109-
const producedLinksInTargetType = producedLinks[targetType];
110-
const countInTargetType = producedLinksInTargetType.length;
111-
const linkOrLinks = EntityGraphArbitrary.computeLinkIndex(
112-
relation.arity,
113-
producedLinksInTargetType.length,
114-
currentEntityDepth,
115-
mrng,
116-
biasFactor,
117-
);
118-
currentLinks[name] = { type: targetType, index: linkOrLinks };
119-
const links = linkOrLinks === undefined ? [] : typeof linkOrLinks === 'number' ? [linkOrLinks] : linkOrLinks;
120-
for (const link of links) {
121-
if (link >= countInTargetType) {
122-
safePush(toBeProducedEntities, { type: targetType, indexInType: link, depth: currentEntity.depth + 1 }); // indexInType should be equal to producedLinksInTargetType.length
123-
safePush(producedLinksInTargetType, safeObjectCreate(null));
124-
}
125-
}
126-
}
127-
}
128-
// Drop any item from the array
129-
toBeProducedEntities.length = 0;
130-
131-
// STEP II - Producing entities themselves
132-
const recordContraints = { noNullPrototype: this.constraints.noNullPrototype };
133-
const recordModel: { [K in keyof TEntityFields]: Arbitrary<TEntityFields[K][]> } = safeObjectCreate(null);
134-
for (const name in this.arbitraries) {
135-
const entityRecordModel = this.arbitraries[name];
136-
const count = producedLinks[name].length;
137-
recordModel[name] = array(record(entityRecordModel, recordContraints), {
138-
minLength: count,
139-
maxLength: count,
140-
}) as any;
141-
}
142-
return record<UnlinkedEntities<TEntityFields>>(recordModel)
143-
.map((unlinkedEntities) => {
144-
// @ts-expect-error - We probably have a fishy typing issue in `record`, as we are supposed to produce `UnlinkedEntities<TEntityFields>`
145-
const safeUnlinkedEntities: UnlinkedEntities<TEntityFields> = unlinkedEntities;
146-
return unlinkedToLinkedEntitiesMapper(safeUnlinkedEntities, producedLinks);
147-
})
148-
.generate(mrng, biasFactor);
149-
}
150-
151-
canShrinkWithoutContext(value: unknown): value is EntityGraphValue<TEntityFields, TEntityRelations> {
152-
return false; // for now, we reject any shrink without any context
153-
}
154-
155-
shrink(
156-
_value: unknown,
157-
_context: unknown | undefined,
158-
): Stream<Value<EntityGraphValue<TEntityFields, TEntityRelations>>> {
159-
return Stream.nil(); // for now, we don't support any shrink
160-
}
161-
}
162-
16325
/**
16426
* Generate values based on a schema. Produced values will automatically come with links between each others when requested to.
16527
*
@@ -189,5 +51,19 @@ export function entityGraph<TEntityFields, TEntityRelations extends EntityRelati
18951
constraints: EntityGraphContraints = {},
19052
): Arbitrary<EntityGraphValue<TEntityFields, TEntityRelations>> {
19153
const defaultEntities = safeObjectKeys(arbitraries) as (keyof typeof arbitraries)[];
192-
return new EntityGraphArbitrary(arbitraries, relations, { ...constraints, defaultEntities });
54+
const unlinkedContraints = { noNullPrototype: constraints.noNullPrototype };
55+
56+
return (
57+
// Step 1, Producing links between entities
58+
onTheFlyLinksForEntityGraph(relations, defaultEntities).chain((producedLinks) =>
59+
// Step 2, Producing entities themselves
60+
// As the number of entities for each kind requires the links to be produced,
61+
// it has to be executed as a chained computation
62+
unlinkedEntitiesForEntityGraph(arbitraries, (name) => producedLinks[name].length, unlinkedContraints).map(
63+
(unlinkedEntities) =>
64+
// Step 3, Glueing links and entities together
65+
unlinkedToLinkedEntitiesMapper(unlinkedEntities, producedLinks),
66+
),
67+
)
68+
);
19369
}

0 commit comments

Comments
 (0)