ColinEberhardt / json-transforms

A recursive, pattern-matching, approach to transforming JSON structures.
MIT License
141 stars 6 forks source link

how to transform multiple items at same level? #8

Open hepabolu opened 7 years ago

hepabolu commented 7 years ago

I've studied issue #7, but it doesn't provide enough input for my problem. Building on the provided structure of #7, here's what I want to do:

object:  {  
      "id":"59afa2bf7210b",
      "forenames": {
          "firstname": "Margaret",
          "secondname": "Julia"
      },
      "surname":"Kiel",
      "emails":{  
         "default":"jaydon.farrell1@barrows.com",
         "home":"jaydon.farrell2@barrows.com",
         "couldBeAnything":"jaydon.farrell3@barrows.com"
      },
      "dateModified":1504682803
   }

What I want to accomplish is:

object: {
   "name": "Margaret",
   "surname": "Kiel",
   "email": "jaydon.farrell1@barrows.com",
   "dateModified": "2017-09-12"
}

I've tried

const rules = [
jsont.pathRule('.object', d => ({
  object: d.runner()
})),
jsont.pathRule('.fornames', d => ({
   name: d.match.firstname
})),
jsont.pathRule('.emails', d => ({
   email: d.match.default
})),
jsont.pathRule('.dateModified', d => ({
   dateModified: changeDateFormat(d.match)
})),
jsont.identity
]

But when I run this, only the first 2 rules are matched, so I end up with

object: {
   "name": "Margaret"
}

What is the correct syntax ?

ymor commented 6 years ago
const rules = [
    jsont.pathRule('.object', d => ({
      object: {
          name: d.match.forenames.firstname,
          email: d.match.emails.default,
          dateModified: changeDateFormat(d.match.dateModified)
      }
    })),
    jsont.identity
    ];

Will give the desired result. I believe the issue is once you hit the forenames rule the context has changed i.e you are at the forenames level and any further matches will now be applied below this level and not to any sibling.

moravcik commented 6 years ago

I have similar issue and I will use official example with automobiles to demostrate it.

const json = {
  "automobiles": [
    { "maker": "Nissan", "model": "Teana", "year": 2011 },
    { "maker": "Honda", "model": "Jazz", "year": 2010 },
    { "maker": "Honda", "model": "Civic", "year": 2007 },
    { "maker": "Toyota", "model": "Yaris", "year": 2008 },
    { "maker": "Honda", "model": "Accord", "year": 2011 }
  ]
};

Now I would like to define rules for both "Honda" and "Toyota". These rules could be possibly different resulting in different JSON substructure, that's why I don't use {.maker === "Honda" || .maker === "Toyota"} in the first rule:

const rules = [
  jsont.pathRule(
    '.automobiles{.maker === "Honda"}', d => ({
      Honda: d.runner()
    })
  ),
  jsont.pathRule(
    '.automobiles{.maker === "Toyota"}', d => ({
      Toyota: d.runner()
    })
  ),
  jsont.pathRule(
    '.{.maker}', d => ({
        model: d.match.model,
        year: d.match.year
    })
  ),
  jsont.identity
];

And resulting JSON looks the same as without the "Toyota" rule:

{ Honda: 
   [ { model: 'Jazz', year: 2010 },
     { model: 'Civic', year: 2007 },
     { model: 'Accord', year: 2011 } ] }

And desired output would look like:

{ Honda: 
   [ { model: 'Jazz', year: 2010 },
     { model: 'Civic', year: 2007 },
     { model: 'Accord', year: 2011 } ],
  Toyota:
   [ { model: 'Yaris', year: 2008 } ] }

Is there any solution for this use case? I need to have separate rules to achieve this, because every rule can define different transformation.

ColinEberhardt commented 6 years ago

Transforming multiple items at the same level can be a bit tricky due to a couple of features of this library:

The transform function iterates over the list of rules, in the order given, to determine whether any return a value other than null, which indicates a match.

and

NOTE: Rule order matters! - the current transform iteration stops on the first matching rule. Therefore, if you put the identity rule before the path rule in the current example, the .automobiles rule will never be reached!

So in simple terms, if you have multiple rules that could match an array of nodes, only the first match will be 'fired'.

Therefore, you need to match all your conditions with a single rule, then apply your transformation within the body of that rule.

You can perform any transformation you like here! You can also recursively match specific nodes, or arrays of nodes via d.runner(...).

For your example, we match both Toyota and Honda cars. The body of the match then separates the returned nodes by maker, recursively matching the children for each:

// https://stackoverflow.com/questions/14446511/what-is-the-most-efficient-method-to-groupby-on-a-javascript-array-of-objects
const groupBy = (xs, key) =>
  xs.reduce((rv, x) => {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});

const rules = [
  jsont.pathRule(
    '.automobiles{.maker === "Honda" || .maker === "Toyota"}',
    d => {
      // d.match is an array of automobiles, where the maker is either
      // of the required values

      // group by maker
      const grouped = groupBy(d.match, "maker");

      // we want to continue the transformation, so iterate
      // over each maker, invoking the 'runner' on each item
      Object.keys(grouped).forEach(maker => {
        grouped[maker] = d.runner(grouped[maker]);
      });
      return grouped;
    }
  ),
  jsont.pathRule(".{.maker}", d => ({
    model: d.match.model,
    year: d.match.year
  })),
  jsont.identity
];

This gives your desired result:

{ Honda: 
   [ { model: 'Jazz', year: 2010 },
     { model: 'Civic', year: 2007 },
     { model: 'Accord', year: 2011 } ],
  Toyota:
   [ { model: 'Yaris', year: 2008 } ] }

Maybe this needs to be added to the docs?

moravcik commented 6 years ago

Thank you for more detailed info, yes it would be handy in docs ;)

As I can see my case probably can't be covered with this library, just some more details:

I have JSON object with quite many root properties (let's say 30) and I would like to define a rule for each of them, just because they are transformed differently. Simplified:

const json = {
  a: 'a',
  b: 'b'
};

const rules = [
  jsont.pathRule('.{.a}', d => ({ A: d.match.a })),
  jsont.pathRule('.{.b}', d => ({ B: d.match.b })),
  jsont.identity
];
console.log(jsont.transform(json, rules));

Gives me { A: 'a' }

And I cannot write all conditions in the same rule and implement transforming logic inside, because it wouldn't be any easier than doing it directly.

Thank you anyway, maybe I will use your library for some other use case.