Bloblang Walkthrough
Bloblang is the most advanced mapping language that you'll learn from this walkthrough (probably). It is designed for readability, the power to shape even the most outrageous input documents, and to easily make erratic schemas bend to your will. Bloblang is the native mapping language of Expanso Edge, but it has been designed as a general purpose technology ready to be adopted by other tools.
In this walkthrough you'll learn how to make new friends by mapping their documents, and lose old friends as they grow jealous and bitter of your mapping abilities. The best way to execute Bloblang and follow along with this guide is to test your mappings with the expanso-edge run command or use the mapping processor in your pipelines.
Your first assignment
The primary goal of a Bloblang mapping is to construct a brand new document by using an input document as a reference, which we achieve through a series of assignments. Bloblang is traditionally used to map JSON documents and that's mostly what we'll be doing in this walkthrough. The first mapping you'll see when you open the editor is a single assignment:
root = this
On the left-hand side of the assignment is our assignment target, where root is a keyword referring to the root of the new document being constructed. On the right-hand side is a query which determines the value to be assigned, where this is a keyword that refers to the context of the mapping which begins as the root of the input document.
As you can see the input document in the editor begins as a JSON object {"message":"hello world"}, and the output panel should show the result as:
{
"message": "hello world"
}
Which is a (neatly formatted) replica of the input document. This is the result of our mapping because we assigned the entire input document to the root of our new thing. However, you won't get far in life by trapping yourself in the past, let's create a brand new document by assigning a fresh object to the root:
root = {}
root.foo = this.message
Bloblang supports a bunch of literal types, and the first line of this mapping assigns an empty object literal to the root. The second line then creates a new field foo on that object by assigning it the value of message from the input document. You should see that our output has changed to:
{
"foo": "hello world"
}
In Bloblang, when the path that we assign to contains fields that are themselves unset then they are created as empty objects. This rule also applies to root itself, which means the mapping:
root.foo.bar = this.message
root.foo."buz me".baz = "I like mapping"
Will automatically create the objects required to produce the output document:
{
"foo": {
"bar": "hello world",
"buz me": {
"baz": "I like mapping"
}
}
}
Also note that we can use quotes in order to express path segments that contain symbols or whitespace. Great, let's move on quick before our self-satisfaction gets in the way of progress.
Basic Methods and Functions
Nothing is ever good enough for you, why should the input document be any different? Usually in our mappings it's necessary to mutate values whilst we map them over, this is almost always done with methods, of which there are many. To demonstrate we're going to change our mapping to uppercase the field message from our input document:
root.foo.bar = this.message.uppercase()
root.foo."buz me".baz = "I like mapping"
As you can see the syntax for a method is similar to many languages, simply add a dot on the target value followed by the method name and arguments within brackets. With this method added our output document should look like this:
{
"foo": {
"bar": "HELLO WORLD",
"buz me": {
"baz": "I like mapping"
}
}
}
Since the result of any Bloblang query is a value you can use methods on anything, including other methods. For example, we could expand our mapping of message to also replace WORLD with EARTH using the replace_all method:
root.foo.bar = this.message.uppercase().replace_all("WORLD", "EARTH")
root.foo."buz me".baz = "I like mapping"
As you can see this method required some arguments. Methods support both nameless (like above) and named arguments, which are often literal values but can also be queries themselves. For example try out the following mapping using both named style and a dynamic argument:
root.foo.bar = this.message.uppercase().replace_all(old: "WORLD", new: this.message.capitalize())
root.foo."buz me".baz = "I like mapping"
Woah, I think that's the plot to Inception, let's move onto functions. Functions are just boring methods that don't have a target, and there are plenty of them as well. Functions are often used to extract information unrelated to the input document, such as environment variables, or to generate data such as timestamps or UUIDs.
Since we're completionists let's add one to our mapping:
root.foo.bar = this.message.uppercase().replace_all("WORLD", "EARTH")
root.foo."buz me".baz = "I like mapping"
root.foo.id = uuid_v4()
Now I can't tell you what the output looks like since it will be different each time it's mapped, how fun!
Deletions
Everything in Bloblang is an expression to be assigned, including deletions, which is a function deleted(). To illustrate let's create a field we want to delete by changing our input to the following:
{
"name": "fooman barson",
"age": 7,
"opinions": ["trucks are cool","trains are cool","chores are bad"]
}
If we wanted a full copy of this document without the field name then we can assign deleted() to it:
root = this
root.name = deleted()
And it won't be included in the output:
{
"age": 7,
"opinions": [
"trucks are cool",
"trains are cool",
"chores are bad"
]
}
An alternative way to delete fields is the method without, our above example could be rewritten as a single assignment root = this.without("name"). However, deleted() is generally more powerful and will come into play more later on.
Variables
Sometimes it's necessary to capture a value for later, but we might not want it to be added to the resulting document. In Bloblang we can achieve this with variables which are created using the let keyword, and can be referenced within subsequent queries with a dollar sign prefix:
let id = uuid_v4()
root.id_sha1 = $id.hash("sha1").encode("hex")
root.id_md5 = $id.hash("md5").encode("hex")
Variables can be assigned any value type, including objects and arrays.
Unstructured and Binary Data
So far in all of our examples both the input document and our newly mapped document are structured, but this does not need to be so. Try assigning some literal value types directly to the root, such as a string root = "hello world", or a number root = 5.
You should notice that when a value type is assigned to the root the output is the raw value, and therefore strings are not quoted. This is what makes it possible to output data of any format, including encrypted, encoded or otherwise binary data.
Unstructured mapping is not limited to the output. Rather than referencing the input document with this, where it must be structured, it is possible to reference it as a binary string with the function content, try changing your mapping to:
root = content().uppercase()
And then put any old gibberish in the input panel, the output panel should be the same gibberish but all uppercase.