From ca05096ac7705fa501fb9f79a4bf99ef9262f30d Mon Sep 17 00:00:00 2001 From: Shubh-Raj Date: Thu, 1 Jan 2026 13:11:26 +0530 Subject: [PATCH 1/3] fix: add formatters for Duration and Period types Fixes: https://github.com/accordproject/template-playground/issues/14 When Duration or Period types were used in TemplateMark templates, they rendered as raw JSON. This adds a drafter that formats them as human-readable text (e.g., '2 days' instead of JSON). Signed-off-by: Shubh-Raj --- src/drafting/Duration/index.ts | 27 ++++++++++++++ src/drafting/index.ts | 3 ++ test/DurationDrafter.test.ts | 37 +++++++++++++++++++ .../TemplateArchiveProcessor.test.ts.snap | 4 +- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/drafting/Duration/index.ts create mode 100644 test/DurationDrafter.test.ts diff --git a/src/drafting/Duration/index.ts b/src/drafting/Duration/index.ts new file mode 100644 index 0000000..ecd462b --- /dev/null +++ b/src/drafting/Duration/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Duration = { + amount: number; + unit: string; +}; + +/** + * Creates a drafter for Duration/Period types + * @param {object} value the duration or period + * @returns {string} the text (e.g., "2 days") + */ +export default function durationDrafter(value: Duration): string { + return `${value.amount} ${value.unit}`; +} diff --git a/src/drafting/index.ts b/src/drafting/index.ts index d7c7f1c..99df566 100644 --- a/src/drafting/index.ts +++ b/src/drafting/index.ts @@ -18,6 +18,7 @@ import booleanDrafter from './Boolean'; import dateTimeDrafter from './DateTime'; import doubleDrafter from './Double'; import integerDrafter from './Integer'; +import durationDrafter from './Duration'; import longDrafter from './Long'; import monetaryAmountDrafter from './MonetaryAmount'; import { DraftFormat } from './DraftFormat'; @@ -31,6 +32,8 @@ export function getDrafter(typeName: string) : ((value:any, format?:DraftFormat) case 'Integer': return integerDrafter; case 'Long': return longDrafter; case 'org.accordproject.money@0.3.0.MonetaryAmount': return monetaryAmountDrafter; + case 'org.accordproject.time@0.3.0.Duration': return durationDrafter; + case 'org.accordproject.time@0.3.0.Period': return durationDrafter; case 'String': return stringDrafter; default: return null; } diff --git a/test/DurationDrafter.test.ts b/test/DurationDrafter.test.ts new file mode 100644 index 0000000..f4619a2 --- /dev/null +++ b/test/DurationDrafter.test.ts @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import durationDrafter from '../src/drafting/Duration'; + +describe('Duration Drafter', () => { + test('should format Duration with days', () => { + const duration = { amount: 2, unit: 'days' }; + expect(durationDrafter(duration)).toBe('2 days'); + }); + + test('should format Duration with hours', () => { + const duration = { amount: 24, unit: 'hours' }; + expect(durationDrafter(duration)).toBe('24 hours'); + }); + + test('should format Period with months', () => { + const period = { amount: 3, unit: 'months' }; + expect(durationDrafter(period)).toBe('3 months'); + }); + + test('should format Duration with singular unit', () => { + const duration = { amount: 1, unit: 'day' }; + expect(durationDrafter(duration)).toBe('1 day'); + }); +}); diff --git a/test/__snapshots__/TemplateArchiveProcessor.test.ts.snap b/test/__snapshots__/TemplateArchiveProcessor.test.ts.snap index a25d1df..a454eaf 100644 --- a/test/__snapshots__/TemplateArchiveProcessor.test.ts.snap +++ b/test/__snapshots__/TemplateArchiveProcessor.test.ts.snap @@ -4,8 +4,8 @@ exports[`template archive processor should draft a template 1`] = ` "Late Delivery and Penalty ---- -In case of delayed delivery except for Force Majeure cases, the Seller shall pay to the Buyer for every {"$class":"org.accordproject.time@0.3.0.Duration","amount":2,"unit":"days"} of delay penalty amounting to 10.5% of the total value of the Equipment whose delivery has been delayed. +In case of delayed delivery except for Force Majeure cases, the Seller shall pay to the Buyer for every 2 days of delay penalty amounting to 10.5% of the total value of the Equipment whose delivery has been delayed. 1. Any fractional part of a days is to be considered a full days. 2. The total amount of penalty shall not however, exceed 55.0% of the total value of the Equipment involved in late delivery. -3. If the delay is more than {"$class":"org.accordproject.time@0.3.0.Duration","amount":15,"unit":"days"}, the Buyer is entitled to terminate this Contract." +3. If the delay is more than 15 days, the Buyer is entitled to terminate this Contract." `; From 2f0f6513f20d365d081830a748df73ba65259cc8 Mon Sep 17 00:00:00 2001 From: Shubh-Raj Date: Thu, 8 Jan 2026 09:51:58 +0530 Subject: [PATCH 2/3] fix: add input validation to durationDrafter Signed-off-by: Shubh-Raj --- src/drafting/Duration/index.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/drafting/Duration/index.ts b/src/drafting/Duration/index.ts index ecd462b..5482dc3 100644 --- a/src/drafting/Duration/index.ts +++ b/src/drafting/Duration/index.ts @@ -23,5 +23,13 @@ type Duration = { * @returns {string} the text (e.g., "2 days") */ export default function durationDrafter(value: Duration): string { - return `${value.amount} ${value.unit}`; + // Validate input to avoid runtime errors + if (value == null || typeof value !== 'object') { + return '0 unknown'; + } + + const amount = typeof value.amount === 'number' ? value.amount : 0; + const unit = typeof value.unit === 'string' && value.unit ? value.unit : 'unknown'; + + return `${amount} ${unit}`; } From 926a14e34ac9f6ab7470001e92f7529f4a719a68 Mon Sep 17 00:00:00 2001 From: Shubh-Raj Date: Thu, 8 Jan 2026 10:00:11 +0530 Subject: [PATCH 3/3] refactor: use type guard for durationDrafter input validation - Changed parameter type from Duration to unknown - Added isDuration type guard for proper type narrowing - TypeScript signature now matches runtime validation behavior Signed-off-by: Shubh-Raj --- src/drafting/Duration/index.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/drafting/Duration/index.ts b/src/drafting/Duration/index.ts index 5482dc3..82837ca 100644 --- a/src/drafting/Duration/index.ts +++ b/src/drafting/Duration/index.ts @@ -17,19 +17,29 @@ type Duration = { unit: string; }; +/** + * Type guard to check if a value is a valid Duration + */ +function isDuration(value: unknown): value is Duration { + return ( + value != null && + typeof value === 'object' && + 'amount' in value && + 'unit' in value && + typeof (value as Duration).amount === 'number' && + typeof (value as Duration).unit === 'string' + ); +} + /** * Creates a drafter for Duration/Period types - * @param {object} value the duration or period + * @param {unknown} value the duration or period (validated at runtime) * @returns {string} the text (e.g., "2 days") */ -export default function durationDrafter(value: Duration): string { - // Validate input to avoid runtime errors - if (value == null || typeof value !== 'object') { +export default function durationDrafter(value: unknown): string { + if (!isDuration(value)) { return '0 unknown'; } - const amount = typeof value.amount === 'number' ? value.amount : 0; - const unit = typeof value.unit === 'string' && value.unit ? value.unit : 'unknown'; - - return `${amount} ${unit}`; + return `${value.amount} ${value.unit}`; }