Source contract and expected records
This is an original, fully synthetic mapping example. Inputs are plain, acyclic JSON-shaped records with the fields shown; the canonical shape and rejection codes belong to this example. Adapt the contract before using a different source.
This original two-format source declares type goods or service plus a description. Goods require nonnegative safe whole units and a nonempty unitLabel, mapped to quantity. Services require a deliverables string list and must omit units, mapped to deliverables. These are local source constraints, not a general rule about all service sales.
| Case | Input record (JSON) | Expected output (JSON) |
|---|---|---|
| Construct goods quantity record | {"type":"goods","description":"Panels","units":3,"unitLabel":"piece"} | {"ok":true,"value":{"kind":"goods","description":"Panels","quantity":{"units":3,"unit":"piece"}}} |
| Reject physical units on service shape | {"type":"service","description":"Setup","units":3,"deliverables":["Guide"]} | {"ok":false,"error":"SERVICE_QUANTITY"} |
| Preserve service deliverable list | {"type":"service","description":"Setup","deliverables":[]} | {"ok":true,"value":{"kind":"service","description":"Setup","deliverables":[]}} |
Run the local transformation
Run the complete example as a JavaScript module in Node.js. It contains the pure mapping function, all three inputs and literal expected outputs; assertions stop on a mismatch. A rejected case returns ok=false with an error code, rather than a partial canonical record.
import assert from 'node:assert/strict';
function goodsServiceLine(i){
const fail=error=>({ok:false,error});if(!i||typeof i.description!=='string'||!i.description)return fail('LINE_DESCRIPTION');
if(i.type==='goods'){
if(!Number.isSafeInteger(i.units)||i.units<0||typeof i.unitLabel!=='string'||!i.unitLabel)return fail('GOODS_QUANTITY');
return {ok:true,value:{kind:'goods',description:i.description,quantity:{units:i.units,unit:i.unitLabel}}};
}
if(i.type==='service'){
if(Object.hasOwn(i,'units'))return fail('SERVICE_QUANTITY');
if(!Array.isArray(i.deliverables)||i.deliverables.some(x=>typeof x!=='string'||!x))return fail('SERVICE_DELIVERABLES');
return {ok:true,value:{kind:'service',description:i.description,deliverables:[...i.deliverables]}};
}
return fail('LINE_TYPE');
}
// Original synthetic cases; expected outputs are literal records.
const cases = [
{
"label": "Construct goods quantity record",
"input": {
"type": "goods",
"description": "Panels",
"units": 3,
"unitLabel": "piece"
},
"expected": {
"ok": true,
"value": {
"kind": "goods",
"description": "Panels",
"quantity": {
"units": 3,
"unit": "piece"
}
}
}
},
{
"label": "Reject physical units on service shape",
"input": {
"type": "service",
"description": "Setup",
"units": 3,
"deliverables": [
"Guide"
]
},
"expected": {
"ok": false,
"error": "SERVICE_QUANTITY"
}
},
{
"label": "Preserve service deliverable list",
"input": {
"type": "service",
"description": "Setup",
"deliverables": []
},
"expected": {
"ok": true,
"value": {
"kind": "service",
"description": "Setup",
"deliverables": []
}
}
}
];
for (const sample of cases) {
const actual = goodsServiceLine(sample.input);
assert.deepEqual(actual, sample.expected);
console.log(sample.label, JSON.stringify(actual));
}
Work through the boundary
What happens to a service with an empty deliverables list?
Show the worked answer
It remains a service record with deliverables=[]. A service that also supplies units is rejected as SERVICE_QUANTITY; the mapper does not reinterpret it as goods.