skip to Main Content

I’m currently writing a "converter" for importing and exporting data.
The specific data are vehicles.

So basically I’m getting a vehicle from a supplier let’s say like so

{
    "id": 1234,
    "category": "CAR",
    "warranty": {
        "warranty_begin": "tomorrow",
        "warranty_end": "the day after tomorrow"
    } 
    // And so on
}

And on an import I will map it to a json format reasonable for our api like:

{
    "internal_id": 1234,
    "category": "CAR",
    "warranty": {
        "warranty_start_date": "tomorrow",
        "warranty_end_date": "the day after tomorrow"
    } 
    // And so on
}

This is pretty easy to write you just access the keys you need and map the values to your keys.

Exactly the same thing can be done for an export where we map our data to the suppliers format.

But
I’m right now looking for method to do it in a two way function manner. I imagine myself something like this.

On import

fromPathTo("id", "internalId");
fromPathTo("category", "category");
// And so on
run();

On export

fromPathTo("id", "internalId");
fromPathTo("category", "category");
// And so on
runInverse();

Where run Inverse then flips the parameters there should also be a way to write functions like:

convertWithFn(originPath: string, converter: Function, destinationPath: string)
convertWithEnum(originPath: string, enum: Array<Array<string> | string>, destinationPath: string)

And things like that

I’m pretty sure there might be a way to do it I just can’t find it. Does someone of you have an idea?

2

Answers


  1. The following code gives a rough idea, but it makes some simplifications. For example, it assumes that property names are unique across levels and thereby avoids things like fromPathTo("warranty/warranty_begin", "warranty/warranty_start_date"). And it would have to be extended to support mapping functions like convertWithFn.

    class TwoWay {
      map = [];
      fromPathTo(from, to) {
        this.map.push({ from, to });
      }
      run(payload) {
        if (typeof payload === "object") {
          var newpayload = {};
          for (var prop in payload) {
            var newprop = this.map.find(_ => _.from === prop)?.to;
            if (newprop && newprop !== prop)
              newpayload[newprop] = this.run(payload[prop]);
            else
              newpayload[prop] = this.run(payload[prop]);
          }
          return newpayload;
        } else
          return payload;
      }
      runInverse(payload) {
        if (typeof payload === "object") {
          var newpayload = {};
          for (var prop in payload) {
            var newprop = this.map.find(_ => _.to === prop)?.from;
            if (newprop && newprop !== prop)
              newpayload[newprop] = this.runInverse(payload[prop]);
            else
              newpayload[prop] = this.runInverse(payload[prop]);
          }
          return newpayload;
        } else
          return payload;
      }
    }
    var twoway = new TwoWay();
    twoway.fromPathTo("id", "internal_id");
    twoway.fromPathTo("category", "category");
    twoway.fromPathTo("warranty_begin", "warranty_start_date");
    twoway.fromPathTo("warranty_end", "warranty_end_date");
    console.log(twoway.run({
      "id": 1234,
      "category": "CAR",
      "warranty": {
        "warranty_begin": "tomorrow",
        "warranty_end": "the day after tomorrow"
      }
    }), twoway.runInverse({
      "internal_id": 1234,
      "category": "CAR",
      "warranty": {
        "warranty_start_date": "tomorrow",
        "warranty_end_date": "the day after tomorrow"
      }
    }));
    Login or Signup to reply.
  2. I’m pretty sure there might be a way to do it I just can’t find it. Does someone of you have an idea?

    Yes, there is a way, but careful if you rely on others making this work you’ll be in trouble when inevitably something goes wrong and you have to debug this beast.

    So my suggestion would be to use a simpler non generic solution, like:

    Sidenote: I’ve removed the nested warranty object in on of the models to demonstrate a maping/change of the structure of the objects, not just the property names.

    const convert = ({ 
      id, category, warranty: { warranty_begin, warranty_end }
    }) => ({
      internal_id: id, category, warranty_start_date: warranty_begin, warranty_end_date: warranty_end
    })
    
    console.log(convert({
      "id": 1234,
      "category": "CAR",
      "warranty": {
        "warranty_begin": "tomorrow",
        "warranty_end": "the day after tomorrow"
      }
    }));
    
    const convertBack = ({
      internal_id, category, warranty_start_date, warranty_end_date
    }) => ({
      id: internal_id, category, warranty: { warranty_begin: warranty_start_date, warranty_end: warranty_end_date }
    });
    
    console.log(convertBack({
      "internal_id": 1234,
      "category": "CAR",
      "warranty_start_date": "tomorrow",
      "warranty_end_date": "the day after tomorrow"
    }));

    Nonetheless here the promised converter:

    // utils
    const identity = v => v;
    const getSetNext = (get, set, next = identity) => obj => {
      set(obj, get(obj));
      return next(obj);
    };
    
    // I hope this example visualizes mapping leftToRight and rightToLeft
    const [ltr, rtl] = converter({
                           "id": "internal_id",
                     "category": "category",
      "warranty.warranty_begin": "warranty_start_date",
        "warranty.warranty_end": "warranty_end_date",
    });
    
    console.log(ltr({
      "id": 1234,
      "category": "CAR",
      "warranty": {
        "warranty_begin": "tomorrow",
        "warranty_end": "the day after tomorrow"
      }
    }));
    
    console.log(rtl({
      "internal_id": 1234,
      "category": "CAR",
      "warranty_start_date": "tomorrow",
      "warranty_end_date": "the day after tomorrow"
    }));
    
    
    
    /**
     * takes an object with `leftKey: rightKey` entries
     * returns two functions. First converts left -> right, second converts right -> left
     */
    function converter(map) {
      let ltr = identity;
      let rtl = identity;
      for (let pair of Object.entries(map).reverse()) {
        const { get: getLeft, set: setLeft } = lens("left." + pair[0]);
        const { get: getRight, set: setRight } = lens("right." + pair[1]);
    
        ltr = getSetNext(getLeft, setRight, ltr);
        rtl = getSetNext(getRight, setLeft, rtl);
      }
    
      return [
        (left) => ltr({ left, right: null }).right,
        (right) => rtl({ left: null, right }).left,
      ]
    }
    
    /**
     * takes a path and creates a getter and setter for that path on a given object.
     * 
     * @example
     * const l = lens("foo.0.bar");
     * 
     * // getter:
     * 
     * const v = l.get(someObj);
     * //does
     * const v = someObj?.foo?.[0]?.bar;
     *
     * // setter:
     * 
     * const hasChanged = l.set(someObj, 42);
     * //does:
     * if(someObj == null) throw ... // this is not handeled yet.
     * if(someObj.foo == null) someObject.foo = [];
     * if(someObj.foo[0] == null) someObject.foo[0] = {};
     * const hasChanged = someObj.foo[0].bar !== 42;
     * someObj.foo[0].bar = 42;
     */
    function lens(path) {
      if (!path) {
        return {
          get() { return undefined; },
          set() { return false; }
        };
      }
    
      const segments = path.split(".");
      const last = segments.pop();
    
      return segments.reduceRight(({ get, set }, key, index, { [index + 1]: nextKey = last }) => ({
        get: (model) => model == null ? undefined : get(model[key]),
        set: +nextKey === (nextKey>>>0) ?
          (model, value) => set(model[key] ??= [], value) :
          (model, value) => set(model[key] ??= {}, value)
      }), {
        get: model => model == null ? undefined : model[last],
        set: (model, value) => model[last] !== (model[last] = value)
      });
    }

    The other thing is, I’m not sure how to properly type this converter. The two functions in the first snippet (convert() and convertBack()) are basically self explanatory, even for the TS compiler. I even manage to make the lens() function typesafe, but deriving a model from a set of paths (not keynames) is beyond my (current) abilities. Keep that in mind when using solutions like this one.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search