@aikotools/placeholder
v1.1.1
Published
Advanced placeholder template engine with generate and compare modes
Maintainers
Readme
@aikotools/placeholder
A powerful placeholder template engine with generate and compare modes for E2E testing scenarios.
Table of Contents
Bei End-to-End-Tests müssen häufig dynamische Testdaten erzeugt und verglichen werden. Typische Herausforderungen sind:
Zeitabhängige Daten: Timestamps, die relativ zu einem Teststart berechnet werden
Generierte IDs: UUIDs, Zugnummern, die konsistent bleiben müssen
Type-Preservation: JSON-Werte müssen die korrekten Typen behalten (Zahlen, nicht Strings)
Verschachtelte Platzhalter:
{{compare:startsWith:{{time:calc:0:dd.MM.yyyy}}}}Multi-Phase-Processing: Erst generieren, dann vergleichen
| Feature | Beschreibung |
|-------------------------|-----------------------------------------------------------------------------|
| Unified Syntax | Einheitliche {{…}} Syntax für alle Platzhalter |
| Type Preservation | Automatische Typ-Erhaltung in JSON (numbers bleiben numbers, nicht strings) |
| Nested Placeholders | Beliebig verschachtelte Platzhalter: {{outer:{{inner:value}}}} |
| Plugin System | Erweiterbar durch eigene Plugins (Time, Generator, Custom) |
| Transform Pipeline | Werte transformieren: {{gen:string:42|toNumber}} |
| Multi-Mode | Generate-Mode (Werte erzeugen) und Compare-Mode (Matcher erzeugen) |
| AST-Based JSON | Intelligente JSON-Verarbeitung mit Type-Preservation |
| Multi-Phase | Selektive Plugin-Ausführung (Gen → Time → Compare) |
┌─────────────────────────────────────────────────────┐
│ PlaceholderEngine │
│ (Orchestriert den gesamten Verarbeitungsprozess) │
└─────────────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ JSON │ │ Text │ │ Custom │
│Processor│ │Processor │ │Processor │
└─────────┘ └──────────┘ └──────────┘
│ │ │
└──────────────┼──────────────┘
▼
┌────────────────────────────────┐
│ PlaceholderParser │
│ (Parst {{module:action:args}})│
└────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌──────────┐
│ Time │ │ Generator │ │ Custom │
│ Plugin │ │ Plugin │ │ Plugin │
└──────────┘ └───────────┘ └──────────┘
│ │ │
└──────────────┼──────────────┘
▼
┌────────────────────────────────┐
│ Transforms │
│ (toNumber, toString, etc.) │
└────────────────────────────────┘Erzeuge konsistente Testdaten mit vorhersagbaren Werten:
{
"testId": "{{gen:uuid:test-12345}}",
"zugnummer": "{{gen:zugnummer:4837}}",
"timestamp": "{{time:calc:0:seconds}}"
}Ergebnis:
{
"testId": "test-12345",
"zugnummer": 4837,
"timestamp": 1710508545
}Berechne Timestamps relativ zum Teststart:
{
"startTime": "{{time:calc:0:seconds}}",
"endTime": "{{time:calc:300:seconds}}",
"date": "{{time:calc:0:dd.MM.yyyy}}"
}Kombiniere multiple Platzhalter in Strings:
{
"filename": "{{gen:zugnummer:4837}}_RGE_{{time:calc:0:dd.MM.yyyy}}_Start"
}Ergebnis: "4837_RGE_15.03.2025_Start"
TypeScript: Typ-sichere Entwicklung
Luxon: Robuste DateTime-Operationen mit Timezone-Support
Vite: Schneller Build und Development Server
Vitest: Modernes Testing Framework
npm install @aikotools/placeholderoder mit Yarn:
yarn add @aikotools/placeholderimport { PlaceholderEngine, TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
const engine = new PlaceholderEngine();
// Plugins registrieren
engine.registerPlugin(new TimePlugin());
engine.registerPlugin(new GeneratorPlugin());
// Standard-Transforms sind bereits registriert
// (toNumber, toString, toBoolean)const template = JSON.stringify({
id: '{{gen:uuid:test-123}}',
zugnummer: '{{gen:zugnummer:4837}}',
timestamp: '{{time:calc:300:seconds}}',
date: '{{time:calc:0:dd.MM.yyyy}}'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: Date.now()
}
});
const data = JSON.parse(result);
console.log(data);
// {
// id: "test-123",
// zugnummer: 4837, // Number!
// timestamp: 1710508545, // Number!
// date: "15.03.2025" // String
// }const text = 'Train {{gen:zugnummer:4837}} departs at {{time:calc:0:HH:mm}}';
const result = await engine.processGenerate(text, {
format: 'text',
mode: 'generate',
context: {
startTimeTest: Date.now()
}
});
console.log(result);
// "Train 4837 departs at 12:00"Alle Platzhalter folgen dem Schema:
{{module:action:arg1:arg2:...|transform}}module: Name des Plugins (z.B.
gen,time)action: Aktion des Plugins (z.B.
uuid,calc)args: Argumente, getrennt durch
:transform: Optional, Transformation (z.B.
toNumber)
Beispiele:
{{gen:uuid:abc123}}
{{gen:zugnummer:4837}}
{{time:calc:300:seconds}}
{{time:calc:0:dd.MM.yyyy}}
{{gen:string:42|toNumber}}Der Context enthält Laufzeit-Informationen:
const context = {
// Basis-Zeit für time:calc (bevorzugt)
startTimeTest: Date.now(),
// Alternative Basis-Zeit
startTimeScript: Date.now(),
// Custom-Felder
testcaseId: 'TC-001',
environment: 'test'
};interface ProcessOptions {
// Format des Templates
format: 'json' | 'text';
// Modus (generate oder compare)
mode: 'generate' | 'compare';
// Kontext für die Verarbeitung
context?: Record<string, any>;
// Nur bestimmte Plugins verwenden
includePlugins?: string[];
// Bestimmte Plugins ausschließen
excludePlugins?: string[];
}my-e2e-tests/
├── src/
│ ├── templates/
│ │ ├── train-expected.json
│ │ └── train-actual.json
│ └── tests/
│ └── train.test.ts
├── package.json
└── tsconfig.jsontemplates/train-expected.json
{
"testId": "{{gen:uuid:test-train-001}}",
"train": {
"number": "{{gen:zugnummer:4837}}",
"type": "RGE"
},
"timing": {
"startTime": "{{time:calc:0:seconds}}",
"endTime": "{{time:calc:300:seconds}}",
"date": "{{time:calc:0:dd.MM.yyyy}}"
}
}tests/train.test.ts
import { describe, it, expect } from 'vitest';
import { PlaceholderEngine, TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
import * as fs from 'fs/promises';
describe('Train E2E Test', () => {
let engine: PlaceholderEngine;
beforeEach(() => {
engine = new PlaceholderEngine();
engine.registerPlugin(new TimePlugin());
engine.registerPlugin(new GeneratorPlugin());
});
it('should generate expected train data', async () => {
const template = await fs.readFile('templates/train-expected.json', 'utf-8');
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});
const data = JSON.parse(result);
expect(data.testId).toBe('test-train-001');
expect(data.train.number).toBe(4837);
expect(typeof data.train.number).toBe('number');
expect(data.timing.date).toBe('15.03.2025');
});
});Lesen Sie die Kernkonzepte, um das System besser zu verstehen
Erkunden Sie die verfügbaren Plugins
Lernen Sie Transforms kennen
Schauen Sie sich weitere Beispiele an Eines der wichtigsten Features ist die automatische Typ-Erhaltung in JSON.
Bei herkömmlichen Template-Systemen werden alle Werte zu Strings:
// Template
{
"zugnummer": "{{gen:zugnummer:4837}}"
}
// Falsches Ergebnis (alle Strings!)
{
"zugnummer": "4837" // ❌ String statt Number
}@aikotools/placeholder nutzt AST-basierte JSON-Verarbeitung:
// Template
{
"zugnummer": "{{gen:zugnummer:4837}}"
}
// Korrektes Ergebnis
{
"zugnummer": 4837 // ✅ Number
}| Template | Ergebnis | Typ |
|--------------------------------|----------------|------------------------|
| "{{gen:number:42}}" | 42 | number |
| "{{gen:string:hello}}" | "hello" | string |
| "{{gen:boolean:true}}" | true | boolean |
| "{{time:calc:0:seconds}}" | 1710508545 | number |
| "{{time:calc:0:dd.MM.yyyy}}" | "15.03.2025" | string |
| "Value: {{gen:number:42}}" | "Value: 42" | string (Interpolation) |
| "{{gen:string:42|toNumber}}" | 42 | number (Transform) |
Wichtig: Wenn ein Platzhalter alleine in einem String steht, wird der Typ des Plugin-Ergebnisses übernommen. Bei String-Interpolation (mehrere Werte im String) bleibt das Ergebnis immer ein String.
Platzhalter können ineinander verschachtelt werden:
{{outer:{{inner:value}}}}Das System löst verschachtelte Platzhalter von innen nach außen auf:
// Template
"{{time:format:{{gen:number:1710508545}}:dd.MM.yyyy}}"
// Schritt 1: Innerster Platzhalter
"{{time:format:1710508545:dd.MM.yyyy}}"
// Schritt 2: Äußerer Platzhalter
"15.03.2024"{
"filename": "train_{{gen:zugnummer:4837}}_{{time:calc:0:yyyy-MM-dd}}.json"
}Ergebnis:
{
"filename": "train_4837_2025-03-15.json"
}Verarbeite Templates in mehreren Phasen mit selektiver Plugin-Ausführung.
In E2E-Tests gibt es oft verschiedene Phasen:
Gen-Phase: Generiere Testdaten (UUIDs, IDs)
Time-Phase: Berechne Zeitwerte
Compare-Phase: Erstelle Matcher für Vergleiche
const template = JSON.stringify({
id: '{{gen:uuid:test-123}}',
timestamp: '{{time:calc:0:seconds}}',
data: 'static'
});
// Phase 1: Nur Gen-Plugins
const afterGen = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
includePlugins: ['gen']
});
// Result: { id: "test-123", timestamp: "{{time:calc:0:seconds}}", data: "static" }
// Phase 2: Nur Time-Plugins
const afterTime = await engine.processGenerate(afterGen, {
format: 'json',
mode: 'generate',
includePlugins: ['time'],
context: { startTimeTest: Date.now() }
});
// Result: { id: "test-123", timestamp: 1710508545, data: "static" }// Nur bestimmte Plugins verwenden
{
includePlugins: ['gen', 'time']
}
// Alle außer bestimmte Plugins verwenden
{
excludePlugins: ['compare']
}Das System unterstützt zwei Modi:
Erzeugt konkrete Werte:
await engine.processGenerate(template, {
mode: 'generate',
// ...
});
// Ergebnis: Konkrete Werte
{ zugnummer: 4837, timestamp: 1710508545 }Erzeugt Matcher für Vergleiche (in späteren Phasen):
await engine.processCompare(actual, expected, {
mode: 'compare',
// ...
});
// Ergebnis: MatchResult mit success/errorsDer Context ist zentral für zeitabhängige Tests.
interface ProcessContext {
// Primäre Basis-Zeit (wird bevorzugt)
startTimeTest?: number | string;
// Alternative Basis-Zeit
startTimeScript?: number | string;
// Custom-Felder für eigene Plugins
[key: string]: any;
}TimePlugin nutzt Zeitwerte in dieser Reihenfolge:
context.startTimeTest(höchste Priorität)context.startTimeScriptAktuelle Zeit (
DateTime.utc())
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});Context-Zeitwerte können verschiedene Formate haben:
// Unix Timestamp (Millisekunden)
{ startTimeTest: 1710504000000 }
// Unix Timestamp (Sekunden, < 10 Milliarden)
{ startTimeTest: 1710504000 }
// ISO String
{ startTimeTest: '2025-03-15T12:00:00Z' }
// Timestamp als String
{ startTimeTest: '1710504000000' }Das System nutzt einen AST (Abstract Syntax Tree) für JSON.
1. JSON parsen → AST
2. AST traversieren (rekursiv)
3. Bei jedem Node:
- Ist es ein String?
- Enthält er Platzhalter?
- Ist es ein "Pure Placeholder" oder "String Interpolation"?
4. Platzhalter ersetzen
5. Typ anpassen wenn nötig
6. AST → JSONDer gesamte String ist ein einzelner Platzhalter:
{
"value": "{{gen:number:42}}"
}→ Node-Typ wird geändert zu Number:
{
"value": 42
}Der String enthält Text + Platzhalter:
{
"value": "Count: {{gen:number:42}}"
}→ Bleibt ein String:
{
"value": "Count: 42"
}Transformiere Werte nach der Placeholder-Auflösung.
{{module:action:args|transform}}| Transform | Beschreibung | Beispiel |
|-------------|-----------------------|------------------------------------------|
| toNumber | Wandelt in Number um | {{gen:string:42|toNumber}} → 42 |
| toString | Wandelt in String um | {{gen:number:42|toString}} → "42" |
| toBoolean | Wandelt in Boolean um | {{gen:string:true|toBoolean}} → true |
// String zu Number
{
"count": "{{gen:string:42|toNumber}}"
}
// Result: { count: 42 }
// Number zu String
{
"id": "{{gen:number:12345|toString}}"
}
// Result: { id: "12345" }
// String zu Boolean
{
"active": "{{gen:string:true|toBoolean}}"
}
// Result: { active: true }Sie können eigene Transforms registrieren:
import { Transform } from '@aikotools/placeholder';
class ToUpperTransform implements Transform {
readonly name = 'toUpper';
transform(value: any): any {
return String(value).toUpperCase();
}
}
engine.registerTransforms([new ToUpperTransform()]);
// Verwendung
"{{gen:string:hello|toUpper}}" → "HELLO"Plugins erweitern das System um spezifische Funktionalität. Jedes Plugin hat einen Namen (module) und bietet verschiedene Actions. Das TimePlugin bietet Funktionen für Zeit-Berechnungen und -Formatierung.
time
Berechnet einen Zeitpunkt relativ zu einer Basis-Zeit.
Syntax:
{{time:calc:offset:unit|format}}Parameter:
offset: Zeitverschiebung (z.B.300,-60,0)unit|format: Entweder Zeiteinheit ODER Datums-Format
Zeiteinheiten:
millisecondssecondsminuteshoursdaysweeksmonthsyears
Beispiele mit Zeiteinheiten:
{{time:calc:300:seconds}} → 1710508845 (Unix timestamp in seconds)
{{time:calc:-60:seconds}} → 1710508485 (60 Sekunden vor Basis-Zeit)
{{time:calc:5:minutes}} → 1710508845000 (Unix timestamp in milliseconds)
{{time:calc:2:hours}} → 1710515745000
{{time:calc:7:days}} → 1711113145000Beispiele mit Datum-Formatierung:
{{time:calc:0:dd.MM.yyyy}} → "15.03.2025"
{{time:calc:3600:dd.MM.yyyy HH}} → "15.03.2025 13" (1 Stunde später)
{{time:calc:-86400:yyyy-MM-dd}} → "2025-03-14" (1 Tag früher)
{{time:calc:0:HH}} → "12"Datums-Formate:
Das Plugin nutzt Luxon’s toFormat(). Häufige Format-Tokens:
| Token | Beschreibung | Beispiel |
|--------|-------------------------|----------|
| yyyy | 4-stelliges Jahr | 2025 |
| yy | 2-stelliges Jahr | 25 |
| MM | Monat (2-stellig) | 03 |
| M | Monat | 3 |
| dd | Tag (2-stellig) | 15 |
| d | Tag | 15 |
| HH | Stunde (24h, 2-stellig) | 14 |
| H | Stunde (24h) | 14 |
| mm | Minute (2-stellig) | 30 |
| m | Minute | 30 |
| ss | Sekunde (2-stellig) | 45 |
| s | Sekunde | 45 |
Wichtig: Format-Strings mit : funktionieren nicht, da : als Argument-Trenner genutzt wird. Nutzen Sie Leerzeichen statt ::
// ❌ Funktioniert NICHT
{{time:calc:0:HH:mm:ss}}
// ✅ Funktioniert
{{time:calc:0:HH mm ss}}Formatiert einen Unix-Timestamp.
Syntax:
{{time:format:timestamp:format}}Parameter:
timestamp: Unix-Timestamp (Sekunden < 10 Mrd., sonst Millisekunden)format: Datums-Format-String
Beispiele:
{{time:format:1710508245:dd.MM.yyyy}} → "15.03.2024"
{{time:format:1710508245000:dd.MM.yyyy HH}} → "15.03.2024 13"
{{time:format:1710508245:yyyy-MM-dd}} → "2024-03-15"TimePlugin nutzt eine Basis-Zeit für calc aus dem Context (Priorität):
context.startTimeTest(höchste)context.startTimeScriptAktuelle Zeit UTC
Beispiel:
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});WICHTIG: Alle Zeit-Operationen erfolgen in UTC, um konsistente Ergebnisse in verschiedenen Umgebungen zu garantieren.
| Action + Params | Rückgabe-Typ |
|----------------------------------|----------------------------|
| calc mit Zeiteinheit seconds | Number (Unix seconds) |
| calc mit Zeiteinheit (andere) | Number (Unix milliseconds) |
| calc mit Datums-Format | String |
| format | String |
Das GeneratorPlugin erzeugt Testdaten - entweder mit vorgegebenen Werten (vorhersagbar) oder zufällig.
gen
Erzeugt oder nutzt eine UUID/ID.
Syntax:
{{gen:uuid}} // Zufällige UUID
{{gen:uuid:my-id-123}} // Feste IDBeispiele:
{{gen:uuid}} → "a3f2e1d4-..." (zufällig)
{{gen:uuid:test-123}} → "test-123"
{{gen:uuid:12345678-1234-1234-1234-123456789012}} → "12345678-1234-1234-1234-123456789012"Wichtig: Das Plugin validiert NICHT das UUID-Format. Sie können beliebige Strings als IDs verwenden, was für Testdaten sehr praktisch ist.
Rückgabe-Typ: String
Erzeugt oder nutzt eine Zahl. zugnummer ist ein Alias für number.
Syntax:
{{gen:number}} // Zufällige Zahl (0-9999)
{{gen:number:42}} // Feste Zahl
{{gen:zugnummer:4837}} // Alias für numberBeispiele:
{{gen:number}} → 7342 (zufällig)
{{gen:number:42}} → 42
{{gen:zugnummer:4837}} → 4837
{{gen:number:-100}} → -100
{{gen:number:3.14}} → 3.14Rückgabe-Typ: Number Erzeugt oder nutzt einen String.
Syntax:
{{gen:string}} // Zufälliger String (8 Zeichen)
{{gen:string:hello}} // Fester StringBeispiele:
{{gen:string}} → "aB3xY9pQ" (zufällig)
{{gen:string:hello}} → "hello"
{{gen:string:test123}} → "test123"Rückgabe-Typ: String Erzeugt oder nutzt einen Boolean.
Syntax:
{{gen:boolean}} // Zufälliger Boolean
{{gen:boolean:true}} // Fester BooleanAkzeptierte Werte für true:
true,1,yes
Akzeptierte Werte für false:
false,0,no
Beispiele:
{{gen:boolean}} → true (zufällig)
{{gen:boolean:true}} → true
{{gen:boolean:false}} → false
{{gen:boolean:1}} → true
{{gen:boolean:0}} → false
{{gen:boolean:yes}} → trueRückgabe-Typ: Boolean
Sie können eigene Plugins erstellen, indem Sie das PlaceholderPlugin Interface implementieren.
interface PlaceholderPlugin {
// Name des Plugins (module)
readonly name: string;
// Hauptmethode: Placeholder auflösen
resolve(request: PluginResolveRequest): PlaceholderResult;
// Optional: Matcher für Compare-Mode erstellen
createMatcher?(request: PluginMatcherRequest): Matcher;
}import { PlaceholderPlugin, PluginResolveRequest, PlaceholderResult } from '@aikotools/placeholder';
export class MathPlugin implements PlaceholderPlugin {
readonly name = 'math';
resolve(request: PluginResolveRequest): PlaceholderResult {
const { action, args } = request.placeholder;
switch (action) {
case 'add':
return this.handleAdd(args);
case 'multiply':
return this.handleMultiply(args);
default:
throw new Error(`Math plugin: unknown action '${action}'`);
}
}
private handleAdd(args: string[]): PlaceholderResult {
if (args.length < 2) {
throw new Error('Math add: requires 2 arguments');
}
const a = parseFloat(args[0]);
const b = parseFloat(args[1]);
if (isNaN(a) || isNaN(b)) {
throw new Error('Math add: invalid numbers');
}
return {
value: a + b,
type: 'number'
};
}
private handleMultiply(args: string[]): PlaceholderResult {
if (args.length < 2) {
throw new Error('Math multiply: requires 2 arguments');
}
const a = parseFloat(args[0]);
const b = parseFloat(args[1]);
if (isNaN(a) || isNaN(b)) {
throw new Error('Math multiply: invalid numbers');
}
return {
value: a * b,
type: 'number'
};
}
}import { PlaceholderEngine } from '@aikotools/placeholder';
import { MathPlugin } from './MathPlugin';
const engine = new PlaceholderEngine();
engine.registerPlugin(new MathPlugin());
// Verwendung
"{{math:add:5:3}}" → 8
"{{math:multiply:4:7}}" → 28Jedes Plugin muss ein PlaceholderResult zurückgeben:
interface PlaceholderResult {
// Der Wert
value: any;
// Der Typ (für Type Preservation)
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
}Wichtig: Der type wird für Type Preservation in JSON genutzt.
Ihr Plugin kann auf den Context zugreifen:
resolve(request: PluginResolveRequest): PlaceholderResult {
const { action, args } = request.placeholder;
const { context } = request;
// Context-Werte nutzen
const env = context.environment || 'test';
const baseUrl = context.baseUrl || 'http://localhost';
// ...
}Transforms ermöglichen es, Werte nach der Placeholder-Auflösung zu transformieren. Sie werden mit dem Pipe-Symbol | an Platzhalter angehängt.
Wandelt einen Wert in eine Zahl um.
Syntax:
{{...|toNumber}}Beispiele:
{{gen:string:42|toNumber}} → 42
{{gen:string:3.14|toNumber}} → 3.14
{{gen:string:-100|toNumber}} → -100JSON mit Type Preservation:
{
"count": "{{gen:string:42|toNumber}}"
}Ergebnis:
{
"count": 42
}Fehlerbehandlung:
Wenn der Wert nicht in eine Zahl konvertiert werden kann, wird NaN zurückgegeben.
Wandelt einen Wert in einen String um.
Syntax:
{{...|toString}}Beispiele:
{{gen:number:42|toString}} → "42"
{{gen:boolean:true|toString}} → "true"JSON mit Type Preservation:
{
"id": "{{gen:number:12345|toString}}"
}Ergebnis:
{
"id": "12345"
}Wandelt einen Wert in einen Boolean um.
Syntax:
{{...|toBoolean}}True-Werte:
String:
"true","1","yes"(case-insensitive)Number:
1Boolean:
true
False-Werte:
String:
"false","0","no"(case-insensitive)Number:
0Boolean:
false
Alle anderen Werte:
Werden als "truthy" (true) oder "falsy" (false) interpretiert nach JavaScript-Regeln.
Beispiele:
{{gen:string:true|toBoolean}} → true
{{gen:string:false|toBoolean}} → false
{{gen:string:yes|toBoolean}} → true
{{gen:string:no|toBoolean}} → false
{{gen:number:1|toBoolean}} → true
{{gen:number:0|toBoolean}} → false
{{gen:string:hello|toBoolean}} → true (truthy)JSON mit Type Preservation:
{
"active": "{{gen:string:true|toBoolean}}"
}Ergebnis:
{
"active": true
}Transforms werden in der Reihenfolge angewendet, in der sie angegeben sind:
{{gen:string:42|toNumber|toString}}Plugin-Auflösung:
"42"(String)toNumber:42(Number)toString:"42"(String) Sie können eigene Transforms erstellen, indem Sie dasTransformInterface implementieren.
interface Transform {
// Name des Transforms
readonly name: string;
// Transformations-Funktion
transform(value: any): any;
}import { Transform } from '@aikotools/placeholder';
export class ToUpperTransform implements Transform {
readonly name = 'toUpper';
transform(value: any): any {
// Wandle Wert in String und dann in Uppercase
return String(value).toUpperCase();
}
}import { Transform } from '@aikotools/placeholder';
export class RoundTransform implements Transform {
readonly name = 'round';
transform(value: any): any {
const num = parseFloat(value);
if (isNaN(num)) {
throw new Error(`Round transform: invalid number '${value}'`);
}
return Math.round(num);
}
}import { PlaceholderEngine } from '@aikotools/placeholder';
import { ToUpperTransform, RoundTransform } from './transforms';
const engine = new PlaceholderEngine();
// Einzeln registrieren
engine.registerTransforms([
new ToUpperTransform(),
new RoundTransform()
]);
// Verwendung
"{{gen:string:hello|toUpper}}" → "HELLO"
"{{gen:number:3.7|round}}" → 4Aktuell unterstützen Transforms keine direkten Parameter. Wenn Sie parametrisierbare Transformationen benötigen, sollten Sie stattdessen ein Plugin erstellen.
Beispiel:
Anstatt {{value|round:2}} (funktioniert nicht), nutzen Sie:
// Ein Math-Plugin mit round-Action
{{math:round:3.14159:2}} → 3.14Nutzen Sie Transforms sparsam. Oft ist es besser, die Logik im Plugin zu haben:
// ❌ Nicht optimal
{{gen:string:42|toNumber}}
// ✅ Besser
{{gen:number:42}}Transforms ändern den Typ des Ergebnisses:
// Original (ohne Transform): Number
{
"count": "{{gen:number:42}}"
}
// Mit Transform: String
{
"count": "{{gen:number:42|toString}}"
}Ihre Transforms sollten robuste Fehlerbehandlung haben:
transform(value: any): any {
if (value === null || value === undefined) {
throw new Error('Transform: value is null or undefined');
}
const num = parseFloat(value);
if (isNaN(num)) {
throw new Error(`Transform: invalid number '${value}'`);
}
return num;
}Transforms sollten idempotent sein (mehrfache Anwendung = einmalige Anwendung):
// ✅ Idempotent
toUpper("hello") → "HELLO"
toUpper("HELLO") → "HELLO"
// ❌ Nicht idempotent (problematisch)
increment(5) → 6
increment(6) → 7Bei Transform-Fehlern erhalten Sie detaillierte Fehlermeldungen:
Error: Transform 'toNumber' failed for value 'abc': invalid number
at ToNumberTransform.transform (transforms/ToNumberTransform.ts:12)
at PlaceholderEngine.applyTransforms (core/PlaceholderEngine.ts:145)Sie können Transforms auch isoliert testen:
import { ToNumberTransform } from '@aikotools/placeholder';
const transform = new ToNumberTransform();
console.log(transform.transform('42')); // 42
console.log(transform.transform('3.14')); // 3.14
console.log(transform.transform('abc')); // NaNDie Haupt-Engine-Klasse, die alle Operationen orchestriert.
constructor()Erstellt eine neue PlaceholderEngine-Instanz mit:
Standard-Transforms (toNumber, toString, toBoolean)
Leerer Plugin-Registry
Beispiel:
import { PlaceholderEngine } from '@aikotools/placeholder';
const engine = new PlaceholderEngine();Registriert ein Plugin.
registerPlugin(plugin: PlaceholderPlugin): voidParameter:
plugin: Das zu registrierende Plugin
Beispiel:
import { TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
engine.registerPlugin(new TimePlugin());
engine.registerPlugin(new GeneratorPlugin());Registriert mehrere Plugins auf einmal.
registerPlugins(plugins: PlaceholderPlugin[]): voidParameter:
plugins: Array von Plugins
Beispiel:
engine.registerPlugins([
new TimePlugin(),
new GeneratorPlugin(),
new CustomPlugin()
]);Registriert Transforms.
registerTransforms(transforms: Transform[]): voidParameter:
transforms: Array von Transforms
Beispiel:
import { ToUpperTransform } from './transforms';
engine.registerTransforms([
new ToUpperTransform()
]);Verarbeitet ein Template im Generate-Mode.
async processGenerate(
input: string,
options: ProcessOptions
): Promise<string>Parameter:
input: Template-String (JSON oder Text)options: Verarbeitungs-Optionen
Rückgabe:
- Promise mit verarbeitetem String
Beispiel:
const result = await engine.processGenerate(
JSON.stringify({ id: '{{gen:uuid:test-123}}' }),
{
format: 'json',
mode: 'generate',
context: {
startTimeTest: Date.now()
}
}
);
console.log(JSON.parse(result));
// { id: "test-123" }Verarbeitet Templates im Compare-Mode (in Entwicklung).
async processCompare(
actual: any,
expected: any,
options: CompareOptions
): Promise<CompareResult>Optionen für die Template-Verarbeitung.
interface ProcessOptions {
// Format des Templates
format: 'json' | 'text';
// Verarbeitungs-Modus
mode: 'generate' | 'compare';
// Kontext mit Laufzeit-Daten
context?: Record<string, any>;
// Nur diese Plugins verwenden
includePlugins?: string[];
// Diese Plugins ausschließen
excludePlugins?: string[];
}Das Format des Template-Strings.
'json': JSON-Template mit AST-basierter Verarbeitung'text': Einfacher Text-Template Der Verarbeitungs-Modus.'generate': Erzeugt konkrete Werte'compare': Erzeugt Matcher (in Entwicklung) Kontext-Objekt mit Laufzeit-Daten.
Standard-Felder:
startTimeTest: Basis-Zeit für TimePlugin (bevorzugt)startTimeScript: Alternative Basis-Zeit
Custom-Felder:
Sie können beliebige Felder hinzufügen, die Ihre Plugins nutzen können.
Beispiel:
{
context: {
startTimeTest: Date.now(),
environment: 'test',
baseUrl: 'http://localhost:3000',
testcaseId: 'TC-001'
}
}Array von Plugin-Namen, die verwendet werden sollen. Alle anderen werden ignoriert.
Beispiel:
{
includePlugins: ['gen', 'time']
}Array von Plugin-Namen, die ausgeschlossen werden sollen.
Beispiel:
{
excludePlugins: ['compare']
}Interface für Plugins.
interface PlaceholderPlugin {
readonly name: string;
resolve(request: PluginResolveRequest): PlaceholderResult;
createMatcher?(request: PluginMatcherRequest): Matcher;
}Der Name des Plugins (module-Name in Platzhaltern).
Typ: string
Löst einen Platzhalter auf und erzeugt einen Wert.
resolve(request: PluginResolveRequest): PlaceholderResultParameter:
request: Request-Objekt mit Placeholder und Context
Rückgabe:
PlaceholderResultmit value und type Erzeugt einen Matcher für Compare-Mode (in Entwicklung).
createMatcher?(request: PluginMatcherRequest): MatcherRequest-Objekt für Plugin.resolve().
interface PluginResolveRequest {
placeholder: ParsedPlaceholder;
context: Record<string, any>;
registry: PluginRegistry | null;
}Das geparste Platzhalter-Objekt.
interface ParsedPlaceholder {
module: string; // Plugin-Name (z.B. "gen")
action: string; // Action-Name (z.B. "uuid")
args: string[]; // Argumente
transforms: string[]; // Transform-Namen
raw: string; // Original-String
}Kontext-Objekt mit Laufzeit-Daten (siehe ProcessOptions.context). Plugin-Registry (für fortgeschrittene Use Cases).
Rückgabe-Objekt von Plugin.resolve().
interface PlaceholderResult {
value: any;
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
}Der erzeugte Wert.
Typ: any
Der Typ des Werts (für Type Preservation).
Typ: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'
Wichtig: Dieser Typ wird für Type Preservation in JSON genutzt.
Interface für Transforms.
interface Transform {
readonly name: string;
transform(value: any): any;
}Der Name des Transforms.
Typ: string
Transformiert einen Wert.
transform(value: any): anyParameter:
value: Der zu transformierende Wert
Rückgabe:
- Der transformierte Wert
Parser für Platzhalter-Strings.
class PlaceholderParser {
parse(input: string): ParsedPlaceholder
findPlaceholders(input: string): string[]
}Parst einen einzelnen Platzhalter.
parse(input: string): ParsedPlaceholderParameter:
input: Platzhalter-String (z.B."{{gen:uuid:test}}")
Rückgabe:
ParsedPlaceholder-Objekt
Beispiel:
import { PlaceholderParser } from '@aikotools/placeholder';
const parser = new PlaceholderParser();
const parsed = parser.parse('{{gen:uuid:test-123|toUpper}}');
console.log(parsed);
// {
// module: 'gen',
// action: 'uuid',
// args: ['test-123'],
// transforms: ['toUpper'],
// raw: '{{gen:uuid:test-123|toUpper}}'
// }Findet alle Platzhalter in einem String.
findPlaceholders(input: string): string[]Parameter:
input: String mit potentiellen Platzhaltern
Rückgabe:
- Array von Platzhalter-Strings
Beispiel:
const parser = new PlaceholderParser();
const placeholders = parser.findPlaceholders(
'ID: {{gen:uuid:test}} at {{time:calc:0:HH:mm}}'
);
console.log(placeholders);
// ['{{gen:uuid:test}}', '{{time:calc:0:HH:mm}}']Registry für Plugins (normalerweise nicht direkt verwendet).
class PluginRegistry {
register(plugin: PlaceholderPlugin): void
get(name: string): PlaceholderPlugin | undefined
has(name: string): boolean
getAll(): PlaceholderPlugin[]
}Verarbeitet JSON-Templates mit Type Preservation.
class JsonProcessor {
process(
input: string,
options: ProcessOptions,
resolveFn: (placeholder: string) => Promise<any>
): Promise<string>
}// Plugins export { TimePlugin } from './plugins/TimePlugin'; export { GeneratorPlugin } from './plugins/GeneratorPlugin';
// Transforms export { ToNumberTransform, ToStringTransform, ToBooleanTransform, createStandardTransforms } from './transforms';
// Core export { PlaceholderParser } from './core/PlaceholderParser'; export { PluginRegistry } from './core/PluginRegistry';
// Processors export { JsonProcessor } from './formats/JsonProcessor'; export { TextProcessor } from './formats/TextProcessor';
// Types export type { PlaceholderPlugin, Transform, Matcher, PlaceholderResult, ParsedPlaceholder, ProcessOptions, // ... weitere Types } from './core/types';
``` highlight
const template = JSON.stringify({
id: '{{gen:uuid:test-001}}',
correlationId: '{{gen:uuid:corr-123}}'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate'
});
console.log(JSON.parse(result));
// {
// id: "test-001",
// correlationId: "corr-123"
// }import { DateTime } from 'luxon';
const baseTime = DateTime.fromISO('2025-03-15T12:00:00Z');
const template = JSON.stringify({
startTime: '{{time:calc:0:seconds}}',
endTime: '{{time:calc:300:seconds}}',
checkTime: '{{time:calc:150:seconds}}'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: baseTime.toMillis()
}
});
console.log(JSON.parse(result));
// {
// startTime: 1710504000,
// endTime: 1710504300,
// checkTime: 1710504150
// }const template = JSON.stringify({
date: '{{time:calc:0:dd.MM.yyyy}}',
time: '{{time:calc:0:HH}}',
datetime: '{{time:calc:0:yyyy-MM-dd HH}}'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T14:30:00Z').getTime()
}
});
console.log(JSON.parse(result));
// {
// date: "15.03.2025",
// time: "14",
// datetime: "2025-03-15 14"
// }import { PlaceholderEngine, TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
import { DateTime } from 'luxon';
// Setup
const engine = new PlaceholderEngine();
engine.registerPlugins([
new TimePlugin(),
new GeneratorPlugin()
]);
// Test-Basis-Zeit: 15.03.2025 12:00 UTC
const testStartTime = DateTime.fromISO('2025-03-15T12:00:00Z');
// Expected-Template für Zug-Abfahrt
const expected = JSON.stringify({
testId: '{{gen:uuid:test-train-departure-001}}',
train: {
number: '{{gen:zugnummer:4837}}',
type: 'RGE',
operator: 'DB'
},
departure: {
station: 'Berlin Hbf',
platform: '7',
scheduledTime: '{{time:calc:0:seconds}}',
actualTime: '{{time:calc:120:seconds}}', // 2 Min Verspätung
date: '{{time:calc:0:dd.MM.yyyy}}'
},
destination: {
station: 'Hamburg Hbf',
arrivalTime: '{{time:calc:5400:seconds}}' // 90 Min Fahrt
},
metadata: {
created: '{{time:calc:0:yyyy-MM-dd HH}}',
version: '{{gen:number:1}}'
}
});
// Verarbeiten
const result = await engine.processGenerate(expected, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: testStartTime.toMillis()
}
});
const data = JSON.parse(result);
console.log(data);
// {
// testId: "test-train-departure-001",
// train: {
// number: 4837,
// type: "RGE",
// operator: "DB"
// },
// departure: {
// station: "Berlin Hbf",
// platform: "7",
// scheduledTime: 1710504000,
// actualTime: 1710504120,
// date: "15.03.2025"
// },
// destination: {
// station: "Hamburg Hbf",
// arrivalTime: 1710509400
// },
// metadata: {
// created: "2025-03-15 12",
// version: 1
// }
// }
// Typ-Überprüfungen
console.assert(typeof data.train.number === 'number');
console.assert(typeof data.departure.scheduledTime === 'number');
console.assert(typeof data.metadata.version === 'number');
console.assert(typeof data.departure.date === 'string');const template = JSON.stringify({
filename: '{{gen:zugnummer:4837}}_RGE_{{time:calc:0:dd.MM.yyyy}}_{{gen:uuid:run123}}_Start'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});
console.log(JSON.parse(result));
// {
// filename: "4837_RGE_15.03.2025_run123_Start"
// }const template = JSON.stringify({
id: '{{gen:uuid:test-123}}',
zugnummer: '{{gen:zugnummer:4837}}',
timestamp: '{{time:calc:0:seconds}}',
static: 'unchanged'
});
// Phase 1: Nur Gen-Plugins
const afterGen = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
includePlugins: ['gen']
});
console.log('After Gen:', JSON.parse(afterGen));
// {
// id: "test-123",
// zugnummer: 4837,
// timestamp: "{{time:calc:0:seconds}}",
// static: "unchanged"
// }
// Phase 2: Nur Time-Plugins
const afterTime = await engine.processGenerate(afterGen, {
format: 'json',
mode: 'generate',
includePlugins: ['time'],
context: {
startTimeTest: Date.now()
}
});
console.log('After Time:', JSON.parse(afterTime));
// {
// id: "test-123",
// zugnummer: 4837,
// timestamp: 1710504000,
// static: "unchanged"
// }const template = JSON.stringify({
journey: {
id: '{{gen:uuid:journey-001}}',
train: {
number: '{{gen:zugnummer:4837}}',
type: 'RGE',
sections: [
{
from: 'Berlin Hbf',
to: 'Hamburg Hbf',
departure: '{{time:calc:0:seconds}}',
arrival: '{{time:calc:5400:seconds}}'
},
{
from: 'Hamburg Hbf',
to: 'Bremen Hbf',
departure: '{{time:calc:5700:seconds}}',
arrival: '{{time:calc:8100:seconds}}'
}
]
},
passengers: {
count: '{{gen:number:42}}',
manifest: [
{ id: '{{gen:uuid:p1}}', seat: '{{gen:number:15}}' },
{ id: '{{gen:uuid:p2}}', seat: '{{gen:number:16}}' }
]
}
}
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});
const data = JSON.parse(result);
console.log(data);
// Vollständig verschachtelte Struktur mit Type Preservationconst template = JSON.stringify({
timestamps: [
'{{time:calc:0:seconds}}',
'{{time:calc:60:seconds}}',
'{{time:calc:120:seconds}}'
],
ids: [
'{{gen:number:1}}',
'{{gen:number:2}}',
'{{gen:number:3}}'
],
status: [
'{{gen:boolean:true}}',
'{{gen:boolean:false}}',
'{{gen:boolean:true}}'
]
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: Date.now()
}
});
const data = JSON.parse(result);
// Alle Timestamps sind Numbers
console.assert(data.timestamps.every(t => typeof t === 'number'));
// Alle IDs sind Numbers
console.assert(data.ids.every(id => typeof id === 'number'));
// Alle Status sind Booleans
console.assert(data.status.every(s => typeof s === 'boolean'));const template = JSON.stringify({
// String → Number
count: '{{gen:string:42|toNumber}}',
// Number → String
id: '{{gen:number:12345|toString}}',
// String → Boolean
active: '{{gen:string:true|toBoolean}}',
// Verschachtelt mit Transform
timestamp: '{{gen:string:1710504000|toNumber}}'
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate'
});
console.log(JSON.parse(result));
// {
// count: 42,
// id: "12345",
// active: true,
// timestamp: 1710504000
// }const template = 'Train {{gen:zugnummer:4837}} departs at {{time:calc:0:HH}} from platform 7';
const result = await engine.processGenerate(template, {
format: 'text',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T14:30:00Z').getTime()
}
});
console.log(result);
// "Train 4837 departs at 14 from platform 7"const template = `
Test Report
===========
Test ID: {{gen:uuid:test-001}}
Train: {{gen:zugnummer:4837}}
Date: {{time:calc:0:dd.MM.yyyy}}
Time: {{time:calc:0:HH}}
Status: PASSED
`.trim();
const result = await engine.processGenerate(template, {
format: 'text',
mode: 'generate',
context: {
startTimeTest: new Date('2025-03-15T12:00:00Z').getTime()
}
});
console.log(result);
// Test Report
// ===========
// Test ID: test-001
// Train: 4837
// Date: 15.03.2025
// Time: 12
// Status: PASSEDimport { PlaceholderEngine, TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
import * as fs from 'fs/promises';
class TemplateHelper {
private engine: PlaceholderEngine;
constructor() {
this.engine = new PlaceholderEngine();
this.engine.registerPlugins([
new TimePlugin(),
new GeneratorPlugin()
]);
}
async loadAndProcess(
templatePath: string,
context: Record<string, any>
): Promise<any> {
const template = await fs.readFile(templatePath, 'utf-8');
const result = await this.engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context
});
return JSON.parse(result);
}
async createExpectedData(
testId: string,
zugnummer: number,
testStartTime: number
): Promise<any> {
const template = JSON.stringify({
testId: `{{gen:uuid:${testId}}}`,
zugnummer: `{{gen:zugnummer:${zugnummer}}}`,
startTime: '{{time:calc:0:seconds}}',
endTime: '{{time:calc:300:seconds}}',
date: '{{time:calc:0:dd.MM.yyyy}}'
});
const result = await this.engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: testStartTime
}
});
return JSON.parse(result);
}
}
// Verwendung
const helper = new TemplateHelper();
const expected = await helper.createExpectedData(
'test-001',
4837,
Date.now()
);
console.log(expected);import { describe, it, expect, beforeEach } from 'vitest';
import { PlaceholderEngine, TimePlugin, GeneratorPlugin } from '@aikotools/placeholder';
import { DateTime } from 'luxon';
describe('Train Journey Tests', () => {
let engine: PlaceholderEngine;
let testStartTime: DateTime;
beforeEach(() => {
engine = new PlaceholderEngine();
engine.registerPlugins([
new TimePlugin(),
new GeneratorPlugin()
]);
testStartTime = DateTime.fromISO('2025-03-15T12:00:00Z');
});
it('should generate expected train departure data', async () => {
const template = JSON.stringify({
testId: '{{gen:uuid:test-departure}}',
train: {
number: '{{gen:zugnummer:4837}}',
type: 'RGE'
},
departure: {
time: '{{time:calc:0:seconds}}',
date: '{{time:calc:0:dd.MM.yyyy}}'
}
});
const result = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
context: {
startTimeTest: testStartTime.toMillis()
}
});
const data = JSON.parse(result);
expect(data.testId).toBe('test-departure');
expect(data.train.number).toBe(4837);
expect(typeof data.train.number).toBe('number');
expect(data.departure.time).toBe(testStartTime.toSeconds());
expect(data.departure.date).toBe('15.03.2025');
});
it('should handle multi-phase processing', async () => {
const template = JSON.stringify({
id: '{{gen:uuid:test-123}}',
timestamp: '{{time:calc:0:seconds}}'
});
// Phase 1: Gen
const afterGen = await engine.processGenerate(template, {
format: 'json',
mode: 'generate',
includePlugins: ['gen']
});
const genData = JSON.parse(afterGen);
expect(genData.id).toBe('test-123');
expect(genData.timestamp).toBe('{{time:calc:0:seconds}}');
// Phase 2: Time
const afterTime = await engine.processGenerate(afterGen, {
format: 'json',
mode: 'generate',
includePlugins: ['time'],
context: {
startTimeTest: testStartTime.toMillis()
}
});
const timeData = JSON.parse(afterTime);
expect(timeData.id).toBe('test-123');
expect(timeData.timestamp).toBe(testStartTime.toSeconds());
});
});const template = JSON.stringify({
value: '{{unknown:action:arg}}'
});
try {
await engine.processGenerate(template, {
format: 'json',
mode: 'generate'
});
} catch (error) {
console.error(error.message);
// "Plugin 'unknown' not found. Available plugins: gen, time"
}const template = JSON.stringify({
value: '{{gen:invalid:arg}}'
});
try {
await engine.processGenerate(template, {
format: 'json',
mode: 'generate'
});
} catch (error) {
console.error(error.message);
// "Generator plugin: unknown action 'invalid'. Available: uuid, number, zugnummer, string, boolean"
}const template = JSON.stringify({
value: '{{time:calc:300}}'
});
try {
await engine.processGenerate(template, {
format: 'json',
mode: 'generate'
});
} catch (error) {
console.error(error.message);
// "Time plugin calc: requires 2 arguments (offset, unit/format)"
}License
MIT - See LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues, questions, or feature requests, please use the GitHub Issues page.
