Linguaggio modello per i dati JSON

Feb 21 2023
Quando la maggior parte del lavoro si esegue la trasformazione JSON, potrebbe essere necessario che questo linguaggio modello sia efficace. Motivazione Noi (Rudderstack) siamo una piattaforma di integrazione e supportiamo oltre 200 integrazioni.

Quando la maggior parte del lavoro si esegue la trasformazione JSON, potrebbe essere necessario che questo linguaggio modello sia efficace.

Motivazione

Noi ( Rudderstack ) siamo una piattaforma di integrazione e supportiamo oltre 200 integrazioni. Abbiamo implementato queste integrazioni utilizzando codice Javascript nativo per trasformare gli eventi in arrivo nel payload di destinazione, quindi, in sintesi, si tratta di manipolazione dei dati JSON. Mantenere tutte queste integrazioni è impegnativo, quindi abbiamo esplorato jsonata per scrivere meno codice per trasformare i dati JSON. Sebbene questa libreria sia eccellente, dobbiamo ancora soddisfare le nostre esigenze di prestazioni. Ad esempio, JSONata analizza il modello specificato, crea un albero di sintassi astratta (AST) e interpreta l'AST per l'input specificato. Poiché abbiamo bisogno di attraversare l'AST ogni volta, è lento, quindi volevamo creare un motore di template che generasse codice Javascript in modo che ci fosse meno sovraccarico durante il runtime.

Panoramica

Una libreria per elaborare i dati JSON utilizzando una sintassi personalizzata basata su javascript e jspath . Ringraziamo gli autori di jspath per il loro eccellente lavoro, poiché la nostra libreria è un'estensione della libreria originale. Vogliamo anche ringraziare il team IBM per il loro lavoro su jsonata , poiché abbiamo preso diverse idee dalla libreria. Puoi anche considerare la nostra libreria come alternativa a jsonata .

Questa libreria genera un codice funzione javascript dal modello e quindi utilizza la funzione per valutare i dati JSON. Emette il codice javascript nelle seguenti fasi:

  1. Lexing (tokenizzazione)
  2. Analisi (creazione AST)
  3. Traduzione (Generazione del codice)
  4. La classe del motore astrae i passaggi precedenti e fornisce un modo conveniente per utilizzare i modelli JSON per valutare gli input.
  5. Flusso di lavoro per la generazione del codice

Installare:npm install rudder-json-template-engine

const engine = new JsonTemplateEngine(`'Hello ' + .name`);
engine.evaluate({name: 'World'}); // => Hello World

Il modello è un insieme di istruzioni e il risultato dell'ultima istruzione è l'output del modello.

Variabili

const a = 1
let b = a + 2
a + b

Input si riferisce al documento JSON che vorremmo elaborare utilizzando un modello. I binding si riferiscono a dati o funzioni aggiuntivi che forniremmo per elaborare i dati in modo efficiente.

Esempi:

  • Modello:"Hello " + (.name ?? $.defaultName)
  • Valutazione:engine.evaluate({name: 'World'}, {defaultName: 'World'});
  • {name: 'World'}è immesso.
  • ^.namesi riferisce alla proprietà "name" dell'input. Possiamo anche usare .nameper riferirci allo stesso. ^fa sempre riferimento alla radice dell'input e .fa riferimento al contesto corrente. Fare riferimento all'esempio per maggiore chiarezza.
  • {defaultName: 'World'}è vincolante.
  • $.defaultNamefa riferimento alla proprietà "defaultName" delle associazioni. Fare riferimento all'esempio per maggiore chiarezza.

let arr = [1, 2, 3, 4]
let a = arr[1, 2] // [2, 3]
let b = arr[0:2] // [1, 2]
let c = arr[-2:] // [3, 4]

Oggetti

let key = "some key"
// { "a": 1, "b": 2, "c": 3, "some key": 4 }
let obj = {a: 1, b: 2, c: 3, [key]: 4 }
let a = obj["a"] // 1
let b = obj.a // 1
let c = obj{["a", "b"]} // { "a": 1, "b": 2}
let d = obj{~["a", "b"]} // { "c": 3, "some key": 4}

Funzioni

Funzioni normali

let fn = function(arg1, arg2){
  arg1 + arg2
}

Funzioni Lambda/Short

let fn = array.map(lambda 2 * ?0);

let fn = array.map(function(args) {
  2 * args[0]
})

Funzioni asincrone

let fn = async function(arg1, arg2){
  const result = await doSomethingAsync(arg1, arg2)
  doSomethingSync(result)
}

let result = await doSomething(.a, .b)

I percorsi vengono usati per accedere alle proprietà in input, bindingse variables.

Percorsi semplici

I percorsi semplici supportano funzionalità di percorso limitate e vengono tradotti come istruzioni di accesso diretto alle proprietà nel codice javascript generato. a.b.cviene tradotto in a?.b?.cmodo che siano molto veloci rispetto ai percorsi ricchi . I percorsi semplici sono ideali quando conosciamo la struttura dell'oggetto.

Funzionalità supportate:

  • Selettori semplici
  • Filtri indice singolo Fare riferimento all'esempio per maggiore chiarezza.

I percorsi ricchi vengono convertiti in codice complesso per supportare diverse variazioni nei dati.

Se usiamo un ricco percorso per l'espressione: a.b.c, gestisce automaticamente le seguenti variazioni.

  • [{"a": { "b": [{"c": 2}]}}]
  • {"a": { "b": [{"c": 2}]}}
  • {"a": [{ "b": [{"c": 2}]}]}
  • Gestisce automaticamente la selezione da oggetti nidificati e matrici.

let x = a.b.c;
let y = a."some key".c

Selettori di caratteri jolly

a.*.c // selects c from any direct property of a

Selettori discendenti

// selects c from any child property of a
// a.b.c, a.b1.b2.c or a.b1.b2.b3.c
let x = a..c;
let y = a.."some key";

Indice singolo o filtri di proprietà

let x = a[0].c;
let y = a[-1].c;  // selects last element from array
let z = a["some key"].c

Indici multipli o filtri di proprietà

let x = a[0, 2, 5].c;
let y = a["some key1", "some key2"].c;

Filtri di gamma

let x = a[2:5].c;
let y = a[:-2].c;
let z = a[2:].c;

let x = obj{["a", "b"]};  // selects a and b
let y = obj{~["a", "b"]}; // selects all properties except a and b

Filtri condizionali o oggetto

let x = obj{.a > 1};

Espressioni di blocco

let x = obj.({
  a: .a + 1,
  b: .b + 2
});
let x = obj.([.a+1, .b+2]);

Variabili di contesto

.orders@order#idx.products.({
    name: .name,
    price: .price,
    orderNum: idx,
    orderId: order.id
})

Opzioni percorso

Possiamo menzionare defaultPathType durante la creazione dell'istanza del motore.

// For using simple path as default path type
// a.b.c will be treated as simple path
JsonTemplateEngine.create(`a.b.c`, {defaultPathType: PathType.SIMPLE});
// For using rich path as default path type
// a.b.c will be treated as rich path
JsonTemplateEngine.create(`a.b.c`, {defaultPathType: PathType.RICH});

// Use ~s to treat a.b.c as simple path
~s a.b.c
// Use ~r to treat a.b.c as rich path
~r a.b.c

Compilare le espressioni temporali

Le espressioni in fase di compilazione vengono valutate utilizzando l'opzione binding in fase di compilazione durante la fase di compilazione.

// {{$.a.b.c}} gets translated to 1 and
// final translated code will be "let a = 1;"
JsonTemplateEngine.create(`let a = {{$.a.b.c}};`, {
  compileTimeBindings: {
    a: {
      b: {
        c: 1
      }
    }
  }
});

Conclusione

Abbiamo creato un linguaggio semplice per elaborare i dati JSON in modo efficiente e lo abbiamo reso open-source . È stato divertente costruire una lingua e spero che tu abbia imparato qualcosa di nuovo oggi.