From 706454736165a403f678f3b5572f416d21d20557 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Tue, 9 Jun 2020 23:00:00 +0200
Subject: [PATCH] [dome] JSON encoders

---
 ivette/src/dome/src/renderer/data/json.ts   | 113 +++++++++++++++++++-
 ivette/src/dome/src/renderer/data/states.ts |  40 +++++--
 2 files changed, 139 insertions(+), 14 deletions(-)

diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts
index b384fff7271..666a1ba172c 100644
--- a/ivette/src/dome/src/renderer/data/json.ts
+++ b/ivette/src/dome/src/renderer/data/json.ts
@@ -40,7 +40,7 @@ export function stringify(js: any) {
 }
 
 /**
-   Export JSON (or any data) as a string with indented content.
+   Export JSON (or any data) as a string with indentation.
  */
 export function pretty(js: any) {
   return JSON.stringify(js, undefined, 2);
@@ -189,6 +189,38 @@ export function jTry<A>(fn: Loose<A>, defaultValue?: A): Loose<A> {
   };
 }
 
+/**
+   Converts maps to dictionnaries.
+ */
+export function jMap<A>(fn: Loose<A>): Safe<Map<string, A>> {
+  return (js: json) => {
+    const m = new Map<string, A>();
+    if (js !== null && typeof js === 'object' && !Array.isArray(js)) {
+      for (let k of Object.keys(js)) {
+        const v = fn(js[k]);
+        if (v !== undefined) m.set(k, v);
+      }
+    }
+    return m;
+  };
+}
+
+/**
+   Converts dictionnaries to maps.
+ */
+export function eMap<A>(fn: Encoder<A>): Encoder<Map<string, undefined | A>> {
+  return m => {
+    const js: json = {};
+    m.forEach((v, k) => {
+      if (v !== undefined) {
+        const u = fn(v);
+        if (u !== undefined) js[k] = u;
+      }
+    });
+    return js;
+  };
+}
+
 /**
    Apply the decoder on each item of a JSON array, or return `[]` otherwize.
    Can be also applied on a _loose_ decoder, but you will get
@@ -215,6 +247,22 @@ export function jList<A>(fn: Loose<A>): Safe<A[]> {
   };
 }
 
+/**
+   Exports all non-undefined elements.
+ */
+export function eList<A>(fn: Encoder<A>): Encoder<(A | undefined)[]> {
+  return m => {
+    const js: json[] = [];
+    m.forEach(v => {
+      if (v !== undefined) {
+        const u = fn(v);
+        if (u !== undefined) js.push(u);
+      }
+    });
+    return js;
+  };
+}
+
 /** Apply a pair of decoders to JSON pairs, or return `undefined`. */
 export function jPair<A, B>(
   fa: Safe<A>,
@@ -289,8 +337,13 @@ export function jObject<A>(fp: Props<A>): Loose<A> {
       const buffer = {} as A;
       for (var k of Object.keys(fp)) {
         const fn = fp[k as keyof A];
-        const fv = fn(js[k]);
-        buffer[k as keyof A] = fv;
+        if (fn !== undefined) {
+          const fj = js[k];
+          if (fj !== undefined) {
+            const fv = fn(fj);
+            if (fv !== undefined) buffer[k as keyof A] = fv;
+          }
+        }
       }
       return buffer;
     }
@@ -298,6 +351,35 @@ export function jObject<A>(fp: Props<A>): Loose<A> {
   };
 }
 
+/**
+   Encoders for each property of object type `A`.
+*/
+export type EProps<A> = {
+  [P in keyof A]?: Encoder<A[P]>;
+}
+
+/**
+   Encode an object given the provided encoders by fields.
+   The exported JSON object has only original
+   fields with some specified encoder.
+ */
+export function eObject<A>(fp: EProps<A>): Encoder<A> {
+  return (m: A) => {
+    const js: json = {};
+    for (var k of Object.keys(fp)) {
+      const fn = fp[k as keyof A];
+      if (fn !== undefined) {
+        const fv = m[k as keyof A];
+        if (fv !== undefined) {
+          const r = fn(fv);
+          if (r !== undefined) js[k] = r;
+        }
+      }
+    }
+    return js;
+  }
+}
+
 /** Type of dictionaries. */
 export type dict<A> = { [key: string]: A };
 
@@ -310,12 +392,33 @@ export function jDictionary<A>(fn: Loose<A>): Safe<dict<A>> {
     const buffer: dict<A> = {};
     if (js !== null && typeof js === 'object' && !Array.isArray(js)) {
       for (var k of Object.keys(js)) {
-        const fv = fn(js[k]);
-        if (fv) buffer[k] = fv;
+        const fd = js[k];
+        if (fd !== undefined) {
+          const fv = fn(fd);
+          if (fv !== undefined) buffer[k] = fv;
+        }
       }
     }
     return buffer;
   };
 }
 
+/**
+   Encode a dictionary into JSON, dicarding all inconsistent entries.
+   If the dictionary contains no valid entry, still returns `{}`.
+*/
+export function eDictionary<A>(fn: Encoder<A>): Encoder<dict<A>> {
+  return (d: dict<A>) => {
+    const js: json = {};
+    for (var k of Object.keys(d)) {
+      const fv = d[k];
+      if (fv !== undefined) {
+        const fv = fn(d[k]);
+        if (fv !== undefined) js[k] = fv;
+      }
+    }
+    return js;
+  };
+}
+
 // --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts
index a552a336413..b530d1ce269 100644
--- a/ivette/src/dome/src/renderer/data/states.ts
+++ b/ivette/src/dome/src/renderer/data/states.ts
@@ -119,10 +119,12 @@ abstract class Settings<A> {
 
   private readonly role: symbol;
   protected readonly decoder: JSON.Safe<A>;
+  protected readonly encoder: JSON.Encoder<A>;
 
-  constructor(role: string, decoder: JSON.Safe<A>) {
+  constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) {
     this.role = Symbol(role);
     this.decoder = decoder;
+    this.encoder = encoder;
   }
 
   validateKey(k?: string): string | undefined {
@@ -150,9 +152,13 @@ abstract class Settings<A> {
     return this.decoder(key ? this.loadData(key) : undefined)
   }
 
+  saveValue(key: string, value: A) {
+    this.saveData(key, this.encoder(value));
+  }
+
 }
 
-export function useSettings<A extends JSON.json>(
+export function useSettings<A>(
   S: Settings<A>,
   dataKey?: string,
 ): [A, (update: A) => void] {
@@ -172,7 +178,7 @@ export function useSettings<A extends JSON.json>(
   const updateValue = React.useCallback((update: A) => {
     if (!isEqual(value, update)) {
       setValue(update);
-      if (theKey) S.saveData(theKey, update);
+      if (theKey) S.saveValue(theKey, update);
     }
   }, [S, theKey]);
 
@@ -180,10 +186,10 @@ export function useSettings<A extends JSON.json>(
 
 }
 
-export class WindowSettings<A> extends Settings<A> {
+export class WindowSettingsData<A> extends Settings<A> {
 
-  constructor(role: string, decoder: JSON.Safe<A>) {
-    super(role, decoder);
+  constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) {
+    super(role, decoder, encoder);
   }
 
   event = Symbol('dome.settings');
@@ -192,10 +198,10 @@ export class WindowSettings<A> extends Settings<A> {
 
 }
 
-export class GlobalSettings<A> extends Settings<A> {
+export class GlobalSettingsData<A> extends Settings<A> {
 
-  constructor(role: string, decoder: JSON.Safe<A>) {
-    super(role, decoder);
+  constructor(role: string, decoder: JSON.Safe<A>, encoder: JSON.Encoder<A>) {
+    super(role, decoder, encoder);
   }
 
   event = Symbol('dome.globals');
@@ -204,4 +210,20 @@ export class GlobalSettings<A> extends Settings<A> {
 
 }
 
+export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> {
+
+  constructor(role: string, decoder: JSON.Safe<A>) {
+    super(role, decoder, JSON.identity);
+  }
+
+}
+
+export class GlobalSettings<A extends JSON.json> extends GlobalSettingsData<A> {
+
+  constructor(role: string, decoder: JSON.Safe<A>) {
+    super(role, decoder, JSON.identity);
+  }
+
+}
+
 // --------------------------------------------------------------------------
-- 
GitLab