Appendix B: Representation and Contract Workshops
Extend known boundaries with complete XML/module/message workshops and clearly separated historical runtime-format probes.
An order arrives as JSON, a lookup returns JSON, and the final response is JSON as well — and the transform still fails, because it reads payload.items after the lookup, when payload is now the lookup’s response. The language did exactly what it was asked to do; the flow changed what the names referred to. Every workshop here extends a boundary the book has already crossed, and that one is the boundary most people cross twice. Use chapters 3–5 for expressions and reuse, chapter 10 for targets, and chapter 15 for representation before starting. None is a hidden prerequisite for the core order service.
Retain the whole response Message
That failure is the reason this first workshop exists. A lookup replaces the message, so an order field read after the call is read out of the wrong document. The fix is to leave the order where it is and give the response a name of its own.
Example 075 — Retain response status with its body
Source: workshops/representations/src/main/mule/app.xml.
Open book/workshops/representations as an ACB project and start the controlled dependency on port 18882. Choose Flow List → retain-response-message. The Listener accepts POST /message on port 18887. Select Request: connection Dependency_HTTP, Method GET, Path /customers/C-42. In Advanced, set Target Variable to customerReply and Target Value to expression message.
Open the following Transform Message’s Payload script:
%dw 2.0
output application/json
---
{orderId: payload.orderId, customer: vars.customerReply.payload.name,
upstreamStatus: vars.customerReply.attributes.statusCode}
The same project supplies normalize-xml-order, use-pricing-module and inspect-java-value. Select each from Flow List. The first two transforms load normalize-xml.dwl and total.dwl; open those resources in Explorer for the following exercises. The Java route first produces application/java, then serializes it as JSON.
The complete workshop listens on 18887. POST the calculation fixture to /message while the controlled dependency is running. Target Value message stores the response Message, so its body is vars.customerReply.payload and status is vars.customerReply.attributes.statusCode. The incoming order remains payload. The observed response is order A-1001, customer Dana and upstream status 200.
What a target does not do is make the retained message cheap. Saving an entire input message in a variable also retains its data representation — it is not a promise that a large stream has become an independent in-memory snapshot.
The file also supplies the following three routes; their scripts are examined separately below. Global configuration and project dependencies are included in the workshop directory, so the shown routes do not depend on an unnamed application.
Normalize XML at the boundary
Example 076 — Normalize repeated XML items
Source: workshops/representations/src/main/resources/normalize-xml.dwl.
%dw 2.0
output application/json
---
{orderId: payload.order.@id,
items: payload.order.*item map (item) -> {
sku: item.@sku, price: item.price as Number, qty: item.qty as Number
}}
Send book/fixtures/order.xml with Content-Type: application/xml to /xml. .@id selects the order attribute; .*item collects repeated elements. Prices and quantities are text at this boundary, and explicit casts produce numeric item fields. The fixture normalizes to the same three supplied-price lines totaling 32.
An absent item collection needs a stated policy. This adapter expects the supplied document; an intake adapter could normalize absence to an empty array, then let business validation reject empty orders. Malformed numeric text should fail conversion rather than silently become zero. Namespace-sensitive feeds also require selectors based on the actual namespace contract, not just a convenient local element name. Naming the translation here, at the adapter, is what keeps the source’s vocabulary out of every later transform — and it makes a dependency migration local, because the rest of the flow can keep reading qty while only the adapter knows how the partner spells it.
Put a helper in a module
Example 077 — Extract a pure pricing helper
Source: workshops/representations/src/main/resources/modules/Pricing.dwl.
%dw 2.0
fun lineTotal(item) = item.price * item.qty
fun orderTotal(items) = sum(items map (item) -> lineTotal(item))
Example 078 — Call the imported pricing helper
Source: workshops/representations/src/main/resources/total.dwl.
%dw 2.0
import orderTotal from modules::Pricing
output application/json
---
{orderId: payload.orderId, total: orderTotal(payload.items)}
POST the calculation fixture to /module; the observed result is {orderId: "A-1001", total: 32}. The module receives items as an argument and knows nothing about a listener, variable or production hostname — one that knew any of them would be harder to reuse and harder to test honestly. The Mule-facing script supplies payload data. That separation permits small pure-function tests and separate event-boundary tests without confusing their evidence.
Inspect a Java boundary without inventing a mapping table
GET /java in the workshop returns the order values and class name java.util.LinkedHashMap. That is an actual Mule Java-writer boundary for this object. It does not establish how every numeric value maps or how a custom constructor/setter behaves.
Example 079 — Inspect two Java numeric values
Source: workshops/historical-formats/java-numbers.xml.
In the historical root application, locate Flow List → javaFormat. Select its Transform Message components in execution order and inspect these Payload scripts. This remains the historical probe described below, not an additional freshly validated ACB checkpoint.
%dw 2.0
output application/java
---
{integer:42,decimal:1.5}
%dw 2.0
output application/json
---
{integerClass:payload.integer.^class,decimalClass:payload.decimal.^class}
The complete executable parent is the companion root project, using port 18081, not the new representation workshop. The historical run observed 42 as java.lang.Integer and 1.5 as java.lang.Double. Two values are not a general Java type table. A Java method expecting BigDecimal, a large integer or a non-null primitive needs a test against its actual class and input range. Representation belongs in debugging as well: when a selector fails, inspect the input’s representation and metadata before rewriting a transformation that was working — JSON text held as a plain string, a Java map and a parsed XML object can carry the same visible characters while presenting different shapes to the same selector. Defaulting unknown to zero can make a call succeed while corrupting meaning, and a conversion that always succeeds can be worse than a visible rejection.
Follow deferred consumption to its consumer
Example 080 — Consume deferred JSON at the listener
Source: workshops/historical-formats/deferred-json.xml.
In the historical root application, locate Flow List → deferred. Select its Transform Message components in execution order and inspect these Payload scripts. This remains the historical probe described below, not an additional freshly validated ACB checkpoint.
%dw 2.0
input payload application/json streaming=true
output application/json deferred=true
---
payload map (order) -> {id:order.orderId,total:order.total}
The historical /lab/deferred route defers output consumption until the HTTP boundary. A serialization failure reached during that consumption need not occur where the transform was declared. deferred=true changes when output is consumed; it does not make every upstream operation constant-memory or guarantee the transform’s enclosing handler sees every later failure.
A streaming reader followed by global grouping or sorting can still materialize substantial data. Full-payload logging can traverse or buffer the very stream you meant to inspect cheaply. Measure the complete reader-transform-writer-consumer path with representative input, including failures near the end. No throughput or memory benchmark is inferred from this small probe.
Preserve the optional format probes
Example 081 — Round-trip a small Excel sheet
Source: workshops/historical-formats/excel-roundtrip.xml.
In the historical root application, locate Flow List → xlsxFormat. Select its Transform Message components in execution order and inspect these Payload scripts. This remains the historical probe described below, not an additional freshly validated ACB checkpoint.
%dw 2.0
output application/json
var bytes = write({Orders:[{id:"A-1001",total:32}]},"application/xlsx")
---
read(bytes,"application/xlsx")
The root project’s /lab/xlsx probe writes and reads a small workbook under the historical pinned runtime. Its recorded success is evidence for that shape, not arbitrary formula evaluation, formatting fidelity or very large workbooks. The selected sheet and column layout are part of the data contract.
Example 082 — Read a fixed-width schema
Source: workshops/historical-formats/fixed-width.xml.
In the historical root application, locate Flow List → flatfileFormat. Select its Transform Message components in execution order and inspect these Payload scripts. This remains the historical probe described below, not an additional freshly validated ACB checkpoint.
%dw 2.0
output application/json
---
readUrl("classpath://customers.txt", "application/flatfile", {schemaPath:"customers.ffd"})
The referenced .ffd schema and input resource remain in the root project’s resources. Field widths, alignment, padding and encoding determine which bytes belong to each value — a manual character slice is a different adapter, and it needs its own tests for short records and multibyte input. Do not imply that a CSV delimiter option implements fixed-width parsing.
These format probes remain separate from the progressive fixtures so their historical observations are not rewritten to match a new order. The format appendix in DataWeave in Depth supplies additional language examples and their runtime distinctions.
Express the same acceptance contract in OpenAPI
Example 083 — Describe order acceptance in OpenAPI
Source: workshops/contracts/orders.openapi.yaml.
{
"openapi": "3.0.3",
"info": {
"title": "Orders acceptance contract workshop",
"version": "1.0.0"
},
"servers": [
{
"url": "http://127.0.0.1:18881/api"
}
],
"paths": {
"/orders": {
"post": {
"operationId": "acceptOrder",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrderCommand"
},
"example": {
"orderId": "A-1001",
"items": [
{
"sku": "PEN-01",
"qty": 4
},
{
"sku": "PAD-22",
"qty": 2
},
{
"sku": "CLP-08",
"qty": 10
}
]
}
}
}
},
"responses": {
"200": {
"description": "Recorded result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrderResponse"
}
}
}
},
"201": {
"description": "Accepted order",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrderResponse"
}
}
}
},
"400": {
"description": "Invalid command",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"401": {
"description": "Missing application principal",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"409": {
"description": "Command conflict",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"500": {
"description": "Internal failure",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"503": {
"description": "Dependency unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
}
}
}
},
"/orders/{orderId}": {
"parameters": [
{
"name": "orderId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9-]{1,40}$"
}
}
],
"get": {
"operationId": "getOrder",
"responses": {
"200": {
"description": "Recorded result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrderResponse"
}
}
}
},
"401": {
"description": "Missing application principal",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"404": {
"description": "Order not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
},
"500": {
"description": "Internal failure",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"OrderCommand": {
"type": "object",
"additionalProperties": false,
"required": [
"orderId",
"items"
],
"properties": {
"orderId": {
"type": "string",
"pattern": "^[A-Za-z0-9-]{1,40}$"
},
"items": {
"type": "array",
"minItems": 1,
"maxItems": 20,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"sku",
"qty"
],
"properties": {
"sku": {
"type": "string",
"enum": [
"PEN-01",
"PAD-22",
"CLP-08"
]
},
"qty": {
"type": "integer",
"minimum": 1,
"maximum": 1000
}
}
}
}
}
},
"OrderResponse": {
"type": "object",
"additionalProperties": false,
"required": [
"orderId",
"tenantId",
"currency",
"state",
"total",
"items"
],
"properties": {
"orderId": {
"type": "string"
},
"tenantId": {
"type": "string"
},
"currency": {
"type": "string",
"enum": [
"USD"
]
},
"state": {
"type": "string",
"enum": [
"ACCEPTED"
]
},
"total": {
"type": "number"
},
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"sku",
"qty",
"unitPrice",
"lineTotal"
],
"properties": {
"sku": {
"type": "string"
},
"qty": {
"type": "integer"
},
"unitPrice": {
"type": "number"
},
"lineTotal": {
"type": "number"
}
}
}
}
}
},
"Problem": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
This complete OAS 3.0.3 document describes the command, accepted response, replay status and error bodies of the recovery service. It is an alternative contract workshop; the executable APIkit checkpoints use RAML as their authority. The OpenAPI file has not been routed through APIkit here.
OpenAPI places required object fields in a required array; a required path parameter is a separate declaration. Check both schemas and examples after conversion. Choosing one authority avoids maintaining two drifting definitions by hand. A shape-compatible conversion still cannot prove authentication, business ownership or transaction behavior.
A mock can help a consumer test display and error handling before the implementation exists — it does not prove that the provider can honor those examples. Provider tests should run the contract examples against the implementation and include absent, invalid and dependency-failure cases.
Secure-property configuration worksheet
For an encrypted-property variant, install the Secure Configuration Properties module using Code Builder and retain the generated dependency pin. Generate ciphertext using the Java 17 tool and disposable values with matching AES/CBC/random-IV settings; put the result inside ![...] in the secure YAML. Configure secure-properties:config with the file and independently supplied key, and select values through secure::.
The tool invocation for dummy values is java -cp secure-properties-tool-j17.jar com.mulesoft.tools.SecurePropertiesTool string encrypt AES CBC '<disposable-key>' '<disposable-value>' --use-random-iv. Real secrets do not belong in visible command arguments or committed launch configuration. A wrong key, wrong mode or mismatched IV option should be tested as a startup failure. Secure Configuration Properties.
This is an optional configuration procedure, not an executed encrypted-secret project. The core cloud reference uses its declared runtime secret mechanism instead. Neither approach makes it safe to print a decrypted value.
Try it
1. One item, no items, malformed quantity. Which differences belong to the XML adapter and which to order validation?
Show answer
Repeated-element selection and numeric conversion establish representation. An explicit absence policy can produce an empty array; business validation decides whether an empty order is acceptable. Malformed numeric text fails conversion rather than becoming zero.
2. A pure helper passes, but the endpoint returns a lookup response. Which boundary needs a test?
Show answer
The Mule event and connector target boundary. Test the real invoking flow with an arranged dependency result and assert the final order shape. A pure pricing test cannot establish which payload reaches it.
3. Choose one contract authority. What evidence is still needed after a RAML-to-OAS conversion validates syntactically?
Show answer
Compare required fields, numeric bounds, error bodies and examples, then exercise the selected APIkit parser/implementation. Syntax and conversion do not prove runtime behavior or business semantics.
Comments