The challenge
I’m an artisan baker who receives an ingredients feed for recipes from my supplier via a super modern 21st century JSON feed. I have to use this feed to create individual recipe labels. Luckily, as well as being a baker*, I can also cook up some fairly tasty dataweave transformations in MuleSoft’s Anypoint Studio. But before we jump into that, let’s have a look at the data we’re receiving and what we want to produce.
Incoming JSON feed
{
"Recipe": "Cake",
"Ingredients": [
{
"FoodPart": "Sponge",
"Amount": "115g",
"IngredientName": "Sugar"
},
{
"FoodPart": "Sponge",
"Amount": "115g",
"IngredientName": "Self raising flour"
},
{
"FoodPart": "Sponge",
"Amount": "2",
"IngredientName": "Eggs"
},
{
"FoodPart": "Sponge",
"Amount": "2 tsp",
"IngredientName": "Baking powder"
},
{
"FoodPart": "Icing",
"Amount": "140g",
"IngredientName": "Butter"
},
{
"FoodPart": "Icing",
"Amount": "280g",
"IngredientName": "Icing sugar"
}
]
}
Output – My Recipe Label
For my cake recipe above, I want to group each foodpart. In each food part, such as ‘sponge’, I want to list all the ingredients and their amounts. I want to separate each ingredient by a semi-colon and each part should be terminated with a full-stop. It should look like this:
CAKE: SPONGE: 115g Sugar; 115g Self raising flour; 2 Eggs; 2 tsp Baking Powder. ICING: 140g Butter; 280g Icing sugar.
Let’s see how we implement this is dataweave 1.0 and 2.0
Dataweave 1.0
%dw 1.0
%output application/json
%function generateLabel(recipe) (
recipe.Recipe ++ ": " ++ ((recipe.Ingredients groupBy (ingredient) ->
ingredient.FoodPart
) mapObject ((ingredients, foodPart) ->
newObject: generateStringForFoodPart(ingredients, foodPart)
) pluck $ joinBy ". " ++ ".")
)
%function generateStringForFoodPart(ingredients, foodPart) (
ingredients reduce ((curr, acc = foodPart ++ ": ") ->
acc ++ curr.Amount ++ ' ' ++ curr.IngredientName
when (acc == (foodPart ++ ": "))
otherwise
acc ++ "; " ++ curr.Amount ++ ' ' ++ curr.IngredientName
)
)
---
generateLabel(payload)
Dataweave 2.0
%dw 2.0
output application/json
fun generateLabel(recipe) = (
recipe.Recipe ++ ": " ++ ((recipe.Ingredients groupBy (ingredient) ->
ingredient.FoodPart
)
mapObject ((ingredients, foodPart) -> {
newObject: generateStringForFoodPart(ingredients, foodPart)
}) pluck $ joinBy ". " ++ ".")
)
fun generateStringForFoodPart(ingredients, foodPart) =
ingredients reduce (curr, acc = foodPart ++ ": ") ->
if(acc == (foodPart ++ ": "))
acc ++ curr.Amount ++ ' ' ++ curr.IngredientName
else
acc ++ "; " ++ curr.Amount ++ ' ' ++ curr.IngredientName
---
generateLabel(payload)
The Smarts
So what’s happening in both equivalent transformations for the label generation?
- Using
groupBy
, it groups the list of ingredients by their food type - For each food type group, it maps the object, and passes the foodPart (i.e. the grouped name) and the list of ingredients for this group to generateStringForFoodPart.
- For each foodPart,
generateStringForFoodPart
uses thereduce
function to create a formatted string, ensuring semicolons are only present when the accumulator is not at the beginning of the string. - Once each newly created object
newObject
has been generated, these are converted into an array of strings usingpluck
and then finally joined together using joinBy.
Major differences between dataweave 1.0 and 2.0
For this particular use-case, there is very little difference between DW 2.0 and 1.0.
- %function is replaced by fun in DW 2.0.
- When, otherwise is replaced by if, else
Back to the reality
*Ok, so here’s the big reveal – I’m not really a baker (but I have eaten cake).
However, the above does demonstrates the power of Mulesoft’s Anypoint Studio and dataweave. By using some grouping, reducing and plucking I have sculpted the perfect label.
In the real world I have seen businesses putting this type of transformation to use in generating label descriptions for clothing.
“Why not use that analogy then?” …you may ask. No idea my friends… no idea.