Skip to content

Instantly share code, notes, and snippets.

@tchak
Last active June 30, 2023 17:36
Show Gist options
  • Save tchak/caa4c72d600884c889edb2b8cddb734b to your computer and use it in GitHub Desktop.
Save tchak/caa4c72d600884c889edb2b8cddb734b to your computer and use it in GitHub Desktop.
DS + Datomic

C'est une implémentation minimale du modèle vers lequel je propose de se diriger. L'implémentation est en TypeScript car les types aident beaucoup à la compréhension de ce genre de code. Et je ne connais pas bien Sorbet :)

  • Field -> TypeDeChamp
  • Datom -> Champ
  • Attribute -> le Champ projeté à travers un TypeDeChamp
  • Section -> Avec FieldCardinality.ONE c'est une section ou un bloc simple, avec FieldCardinality.MANY c'est un bloc répétable

Points intéressants :

  • Les TypeDeChamp sont une liste à un seul niveau. Il n'y a pas d'arborescence même pour les bloc répétable.
  • Les Champs sont également une liste à un seul niveau. Un TypeDeChamp peut être représenté par plusieurs champs. Dans cet exemple, ils sont simples. La plupart des types de champs ont un seul attribut value, mais il est possible d'avoir code_postal et code_insee pour un TypeDeChamp «Commune» avec le même modèle Champ. Tous les attributs (y compris ceux des répétitions) peuvent être filtrés et triés via un seul index [id, attribute, value].
  • J'introduis le TypeDeChamp END, qui permet d'interrompre une Section sans en commencer une nouvelle.
  • En ajoutant un timestamp au Datom, le modèle de stockage peut devenir «immutable». Lors de la projection, seule le Datom le plus récent est pris en compte.
import { describe, it, expect } from 'vitest';
import type { Datom, Field, Attribute } from '.';
import { FieldType, FieldCardinality, projectData } from '.';
export const datoms: Datom[] = [
{ id: '1', attribute: 'value', stringValue: 'foo' },
{ id: '4', attribute: 'value', integerValue: 42 },
{ id: '10', attribute: 'value', integerValue: 1 },
{ id: '20', attribute: 'rows', stringValue: '1' },
{ id: '20', attribute: 'rows', stringValue: '2' },
{ id: '20', attribute: 'rows', stringValue: '3' },
{ id: '21', row: '1', attribute: 'value', stringValue: 'bar' },
{ id: '21', row: '3', attribute: 'value', stringValue: 'toto' },
{ id: '23', row: '3', attribute: 'value', stringValue: 'yolo' },
];
export const revision1: Field[] = [
{ type: FieldType.TEXT, id: '1' },
{ type: FieldType.SECTION, id: '2', level: 1 },
{ type: FieldType.TEXT, id: '3' },
{ type: FieldType.INTEGER, id: '4' },
{ type: FieldType.SECTION, id: '5', level: 2 },
{ type: FieldType.TEXT, id: '6' },
{ type: FieldType.SECTION, id: '7', level: 2 },
{ type: FieldType.TEXT, id: '8' },
{ type: FieldType.END, id: '9' },
{ type: FieldType.BOOLEAN, id: '10' },
{ type: FieldType.END, id: '11' },
{ type: FieldType.TEXT, id: '12' },
{ type: FieldType.SECTION, id: '13', level: 1 },
{ type: FieldType.TEXT, id: '14' },
{ type: FieldType.SECTION, id: '15', level: 1 },
{ type: FieldType.SECTION, id: '16', level: 2 },
{ type: FieldType.SECTION, id: '17', level: 3 },
{ type: FieldType.SECTION, id: '18', level: 1 },
{ type: FieldType.TEXT, id: '19' },
{
type: FieldType.SECTION,
id: '20',
level: 2,
cardinality: FieldCardinality.MANY,
},
{ type: FieldType.TEXT, id: '21' },
{ type: FieldType.SECTION, id: '22', level: 3 },
{ type: FieldType.TEXT, id: '23' },
{ type: FieldType.SECTION, id: '24', level: 1 },
{ type: FieldType.TEXT, id: '25' },
];
export const data1: Attribute[] = [
{ id: '1', type: FieldType.TEXT, value: 'foo' },
{
id: '2',
type: FieldType.SECTION,
level: 1,
children: [
{ id: '3', type: FieldType.TEXT },
{ id: '4', type: FieldType.INTEGER, value: 42 },
{
id: '5',
type: FieldType.SECTION,
level: 2,
children: [{ id: '6', type: FieldType.TEXT }],
},
{
id: '7',
type: FieldType.SECTION,
level: 2,
children: [{ id: '8', type: FieldType.TEXT }],
},
{ id: '10', type: FieldType.BOOLEAN, value: true },
],
},
{ id: '12', type: FieldType.TEXT },
{
id: '13',
type: FieldType.SECTION,
level: 1,
children: [{ id: '14', type: FieldType.TEXT }],
},
{
id: '15',
type: FieldType.SECTION,
level: 1,
children: [
{
id: '16',
type: FieldType.SECTION,
level: 2,
children: [
{ id: '17', type: FieldType.SECTION, level: 3, children: [] },
],
},
],
},
{
id: '18',
type: FieldType.SECTION,
level: 1,
children: [
{ id: '19', type: FieldType.TEXT },
{
id: '20',
type: FieldType.SECTION,
level: 2,
cardinality: FieldCardinality.MANY,
rows: [
{
id: '1',
children: [
{ id: '21', type: FieldType.TEXT, value: 'bar' },
{
id: '22',
type: FieldType.SECTION,
level: 3,
children: [{ id: '23', type: FieldType.TEXT }],
},
],
},
{
id: '2',
children: [
{ id: '21', type: FieldType.TEXT },
{
id: '22',
type: FieldType.SECTION,
level: 3,
children: [{ id: '23', type: FieldType.TEXT }],
},
],
},
{
id: '3',
children: [
{ id: '21', type: FieldType.TEXT, value: 'toto' },
{
id: '22',
type: FieldType.SECTION,
level: 3,
children: [{ id: '23', type: FieldType.TEXT, value: 'yolo' }],
},
],
},
],
},
],
},
{
id: '24',
type: FieldType.SECTION,
level: 1,
children: [{ id: '25', type: FieldType.TEXT }],
},
];
export const revision2: Field[] = [
{ type: FieldType.TEXT, id: '1' },
{ type: FieldType.SECTION, id: '2', level: 1 },
{ type: FieldType.TEXT, id: '3' },
{ type: FieldType.INTEGER, id: '4' },
{ type: FieldType.END, id: '9' },
{ type: FieldType.INTEGER, id: '10' },
];
export const data2: Attribute[] = [
{ id: '1', type: FieldType.TEXT, value: 'foo' },
{
id: '2',
type: FieldType.SECTION,
level: 1,
children: [
{ id: '3', type: FieldType.TEXT },
{ id: '4', type: FieldType.INTEGER, value: 42 },
],
},
{ id: '10', type: FieldType.INTEGER, value: 1 },
];
describe('data', () => {
it('project data1', () => {
expect(projectData(revision1, datoms)).toEqual(data1);
});
it('project data2', () => {
expect(projectData(revision2, datoms)).toEqual(data2);
});
});
export enum FieldType {
TEXT,
INTEGER,
BOOLEAN,
SECTION,
END,
}
export enum FieldCardinality {
ONE,
MANY,
}
export type Field = {
id: string;
type: FieldType;
level?: number;
cardinality?: FieldCardinality;
};
export type Attribute =
| TextAttribute
| IntegerAttribute
| BooleanAttribute
| SectionAttribute;
export type Datom = {
id: string;
attribute: string;
row?: null | string;
stringValue?: string | null;
integerValue?: number | null;
};
type TextAttribute = {
id: string;
type: FieldType.TEXT;
value?: string | null;
};
type IntegerAttribute = {
id: string;
type: FieldType.INTEGER;
value?: number | null;
};
type BooleanAttribute = {
id: string;
type: FieldType.BOOLEAN;
value?: boolean | null;
};
type SectionOneAttribute = {
id: string;
type: FieldType.SECTION;
level: number;
cardinality?: FieldCardinality.ONE;
children: Attribute[];
};
type SectionManyAttribute = {
id: string;
type: FieldType.SECTION;
level: number;
cardinality: FieldCardinality.MANY;
rows: Row[];
};
type SectionAttribute = SectionOneAttribute | SectionManyAttribute;
type Row = {
id: string;
children: Attribute[];
};
type Context = {
attributes: Attribute[];
sections: SectionAttribute[];
};
export function projectData(revision: Field[], datoms: Datom[]): Attribute[] {
const context: Context = { attributes: [], sections: [] };
for (const field of revision) {
if (field.type == FieldType.SECTION) {
const section: SectionAttribute =
field.cardinality == FieldCardinality.MANY
? {
id: field.id,
type: FieldType.SECTION,
cardinality: FieldCardinality.MANY,
level: field.level ?? 1,
rows: [],
}
: {
id: field.id,
type: FieldType.SECTION,
level: field.level ?? 1,
children: [],
};
if (isSectionMany(section)) {
datoms
.filter((d) => d.id == field.id && d.attribute == 'rows')
.forEach(({ stringValue }) => {
if (stringValue) {
section.rows.push({ id: stringValue, children: [] });
}
});
}
while (
context.sections.length > 0 &&
context.sections[context.sections.length - 1].level >= section.level
) {
context.sections.pop();
}
addChildToParent(section, datoms, context, true);
} else if (field.type == FieldType.END) {
context.sections.pop();
} else {
addChildToParent({ id: field.id, type: field.type }, datoms, context);
}
}
return context.attributes;
}
function cloneChild(child: Attribute, datom?: Datom | null) {
const clonedChild = { ...child };
if ('children' in clonedChild) {
clonedChild.children = [...clonedChild.children];
}
if (datom) {
if (clonedChild.type == FieldType.TEXT) {
clonedChild.value = datom.stringValue;
} else if (clonedChild.type == FieldType.INTEGER) {
clonedChild.value = datom.integerValue;
} else if (clonedChild.type == FieldType.BOOLEAN) {
clonedChild.value = !!datom.integerValue;
}
}
return clonedChild;
}
function findSectionInSubTree(
children: Attribute[],
parent?: SectionOneAttribute | null
): SectionOneAttribute | null {
const sections = children.filter(isSectionOne);
const maybeParent = sections.find(({ id }) => id == parent?.id);
if (maybeParent) {
return maybeParent;
}
for (const section of sections) {
const maybeParent = findSectionInSubTree(section.children, parent);
if (maybeParent) {
return maybeParent;
}
}
return null;
}
function addChildToParent(
child: Attribute,
datoms: Datom[],
context: Context,
push = false
) {
const parent = context.sections.at(-1);
const rows = context.sections.find(isSectionMany)?.rows;
if (parent && rows) {
if (push && rows.length > 0 && child.type == FieldType.SECTION) {
context.sections.push(child);
}
for (const row of rows) {
const datom = datoms.find(
(d) => d.id == child.id && d.attribute == 'value' && d.row == row.id
);
const clonedChild = cloneChild(child, datom);
if (isSectionMany(clonedChild)) {
throw new Error(`Section with cardinality MANY cannot be nested!`);
}
if (isSectionMany(parent)) {
row.children.push(clonedChild);
} else {
const section = findSectionInSubTree(row.children, parent);
if (section) {
section.children.push(clonedChild);
} else {
throw new Error(`Section not found!`);
}
}
}
} else if (!parent || isSectionOne(parent)) {
const datom = datoms.find(
(d) => d.id == child.id && d.attribute == 'value' && !d.row
);
const clonedChild = cloneChild(child, datom);
if (parent) {
parent.children.push(clonedChild);
} else {
context.attributes.push(clonedChild);
}
if (push && clonedChild.type == FieldType.SECTION) {
context.sections.push(clonedChild);
}
}
}
function isSectionOne(attribute: Attribute): attribute is SectionOneAttribute {
return (
attribute.type == FieldType.SECTION &&
attribute.cardinality != FieldCardinality.MANY
);
}
function isSectionMany(
attribute: Attribute
): attribute is SectionManyAttribute {
return (
attribute.type == FieldType.SECTION &&
attribute.cardinality == FieldCardinality.MANY
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment