Introducing Mangler.js

Mangler.js is a JavaScript object processing library that helps you transform, restructure, query, index and extract parts of your objects and JSON data. It is open source under GPLv3.

As I often work with bespoke applications where requirements change regularly, it is sometimes impossible to foresee what will need to be added in a month’s time. Due to short deadlines, there is no time to rewrite hundreds of lines of code to re-shape and re-optimise the data, just because a new JSON datasource with unexpected structure has to be integrated. This often leads to one-off conversion functions, which are a pain to write and has no use anywhere else. This library aims to help with these situations.

The project is currently in pre-alpha preview state, keep an eye on the Mangler.js project page for future updates. Documentation is being worked on, new features and breaking changes can be introduced regularly. You can download the most recent version from the Mangler.js GitHub repo.

Data extraction

So far the most useful feature of Mangler.js is the .extract() method, which creates a new slice of your data while attempting to keep object references intact. Suppose you have the following nested object:

var data = {
	mobile_os: [
		{ id: "001", name: "Android" },
		{ id: "002", name: "iOS" }
	],
	desktop_os: [
		{ id: "003", name: "Windows" },
		{ id: "004", name: "Linux", sub_os: [
			{ id: "005", name: "CentOS" },
			{ id: "006", name: "Ubuntu" }
		]}
	]
}

To extract all mobile operating systems into an array, simply include mangler.js in you code and call the .extract() method with a filter:

var os = Mangler(data).extract('mobile_os');

Simply pass the object to the Mangler() function to return a mangler object. Many mangler object methods return a reference to the mangler object itself, so you can conveniently chain methods together. The .extract() method traverses the whole object and extracts items matching the filter. To access the items in a mangler object, read its .items property. In this example, here’s what os.items looks like:

[
	{ id: "001", name: "Android" },
	{ id: "002", name: "iOS" }
]

Filters can be as vague or as specific as you want them to be and can match anything anywhere in the object, not just top-level properties:

var os;

// Extract all name properties from all levels
os = Mangler(data).extract('name');
// os.items are ["Android", "iOS", "Windows", "Linux", "CentOS", "Ubuntu"]

// Extract all mobile OS names
os = Mangler(data).extract('mobile_os[].name');
// os.items are ["Android", "iOS"]

// Extract all linux distro names
os = Mangler(data).extract('desktop_os[1].sub_os[].name');
// os.items are ["CentOS", "Ubuntu"]

Filters also support wildcard characters, use * to substitute zero or more levels of references:

// Extract all desktop OS names, including distros
os = Mangler(data).extract('desktop_os.*.name');
// os.items are ["Windows", "Linux", "CentOS", "Ubuntu"]

You can also use ? anywhere to match partial property names. The filter ?_os collects top-level objects from both the mobile_os and desktop_os arrays, yielding the following:

[
	{ id: "001", name: "Android" },
	{ id: "002", name: "iOS" },
	{ id: "003", name: "Windows" },
	{ id: "004", name: "Linux", sub_os: [
		{ id: "005", name: "CentOS" },
		{ id: "006", name: "Ubuntu" }
	]}
]

The .extract() method supports additional options as the second parameter. By default processing will stop when the filter matches and it won’t go deeper into the objects. In the example above even though sub_os would match the filter, its items are not extracted as desktop_os already matched the filter higher up in the hierarchy. To override this behaviour, use the drilldown option:

os = Mangler(data).extract('?_os', { drilldown: true });

which results in the following items array:

[
	{ id: "001", name: "Android" },
	{ id: "002", name: "iOS" },
	{ id: "003", name: "Windows" },
	{ id: "004", name: "Linux", sub_os: [
		{ id: "005", name: "CentOS" },
		{ id: "006", name: "Ubuntu" }
	]},
	{ id: "005", name: "CentOS" },
	{ id: "006", name: "Ubuntu" }
]

Indexing

I mentioned earlier that Mangler.js tries to preserve your object references. It means while the original data object is untouched, all items in the mangler object are still referencing parts of it. Changing the properties of the extracted objects WILL change the original data.

Let’s say we want to rename Linux to GNU/Linux. Items could in theory be in any order in the data, so we need to find the object we want to change by its id property. The easiest way to do it is to call the .index() method, which returns a JavaScript object keyed on the required property:

// Get a list of items in an object keyed on id
var idx = Mangler(data).extract('?_os').index('id');

// Change name
idx['004'].name = 'GNU/Linux';

Here’s how the resulting idx object looks like:

{
	"001": { id: "001", name: "Android" },
	"002": { id: "002", name: "iOS" },
	"003": { id: "003", name: "Windows" },
	"004": { id: "004", name: "GNU/Linux", sub_os: [
		{ id: "005", name: "CentOS" },
		{ id: "006", name: "Ubuntu" }
	]}
}

Because of the preserved references, you’ll see that the name didn’t just change in the mangler object, but in the original data as well, while the original data’s structure is still intact.

Wait, there’s more…

It was only a sneak peek of some of the already included features, watch out for more updates to come in the near future, as well as more posts covering extra features. I’m still working to put some documentation together, in the meantime you can have a look at the source code in the GitHub repo.

Share Button
Tagged with: , , , ,
One comment on “Introducing Mangler.js
  1. kapa says:

    Interesting project. I will check the code :).

Leave a Reply