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.
The original input has at most one own field: ratePercent or amountUnits. Presence of both is a conflict even when one is zero. A rate is a whole percentage 0–100; fixed amount is a nonnegative safe integer in the source accounting unit. Neither field means kind none. A present null is invalid.
| Case | Input record (JSON) | Expected output (JSON) |
|---|---|---|
| Tag a rate representation | {"ratePercent":10} | {"ok":true,"value":{"kind":"rate","percent":10}} |
| Reject two representations | {"ratePercent":10,"amountUnits":20} | {"ok":false,"error":"DISCOUNT_CONFLICT"} |
| Preserve explicit fixed zero | {"amountUnits":0} | {"ok":true,"value":{"kind":"fixed","amountUnits":0}} |
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 discountTag(i){
const fail=error=>({ok:false,error});if(!i||typeof i!=='object'||Array.isArray(i))return fail('DISCOUNT_OBJECT');
const rate=Object.hasOwn(i,'ratePercent'),fixed=Object.hasOwn(i,'amountUnits');
if(rate&&fixed)return fail('DISCOUNT_CONFLICT');
if(rate){if(!Number.isSafeInteger(i.ratePercent)||i.ratePercent<0||i.ratePercent>100)return fail('DISCOUNT_RATE');return {ok:true,value:{kind:'rate',percent:i.ratePercent}};}
if(fixed){if(!Number.isSafeInteger(i.amountUnits)||i.amountUnits<0)return fail('DISCOUNT_AMOUNT');return {ok:true,value:{kind:'fixed',amountUnits:i.amountUnits}};}
return {ok:true,value:{kind:'none'}};
}
// Original synthetic cases; expected outputs are literal records.
const cases = [
{
"label": "Tag a rate representation",
"input": {
"ratePercent": 10
},
"expected": {
"ok": true,
"value": {
"kind": "rate",
"percent": 10
}
}
},
{
"label": "Reject two representations",
"input": {
"ratePercent": 10,
"amountUnits": 20
},
"expected": {
"ok": false,
"error": "DISCOUNT_CONFLICT"
}
},
{
"label": "Preserve explicit fixed zero",
"input": {
"amountUnits": 0
},
"expected": {
"ok": true,
"value": {
"kind": "fixed",
"amountUnits": 0
}
}
}
];
for (const sample of cases) {
const actual = discountTag(sample.input);
assert.deepEqual(actual, sample.expected);
console.log(sample.label, JSON.stringify(actual));
}
Work through the boundary
Why is amountUnits=0 not the same record as an absent discount field?
Show the worked answer
An explicit zero amount produces {kind:"fixed",amountUnits:0}; absence produces {kind:"none"}. The tag preserves the declared representation without computing a final price.