@reventlessdev/reventless-spec 3.0.0-alpha.88 → 3.0.0-alpha.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +5 -3
- package/rescript.json +2 -1
- package/scripts/generate-currency.mjs +215 -0
- package/scripts/iso-4217-list-one.xml +1956 -0
- package/src/PackageVersion.res +7 -13
- package/src/generator/Config.res +4 -4
- package/src/generator/Discovery.res +12 -12
- package/src/generator/Generator_Node.res +12 -47
- package/src/generator/Pairing.res +12 -12
- package/src/generator/PlatformGenerator.res +12 -13
- package/src/generator/PlatformManifests.res +9 -9
- package/src/generator/PluginGenerator.res +10 -11
- package/src/semantic/Currency.res +598 -0
- package/src/semantic/Currency.res.mjs +743 -0
- package/src/semantic/DateRange.res +148 -0
- package/src/semantic/DateRange.res.mjs +74 -0
- package/src/semantic/Money.res +196 -0
- package/src/semantic/Money.res.mjs +138 -0
- package/src/semantic/Semantic.res +11 -0
- package/src/semantic/Semantic.res.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.90 (2026-08-01)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **spec:** add DateRange semantic type ([d85b6cc](https://github.com/ReventlessDev/reventless-core/commit/d85b6cc18241644905241df2abd99949dd758059))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.89 (2026-07-31)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **spec:** add Money and a closed ISO 4217 Currency ([d4852ab](https://github.com/ReventlessDev/reventless-core/commit/d4852ab63e823e39fac793c4fa5ac31470db9655))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.88 (2026-07-30)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.90",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
"jsonschema2graphql": "1.1.1",
|
|
21
21
|
"sury": "11.0.0-alpha.4",
|
|
22
22
|
"sury-ppx": "11.0.0-alpha.2",
|
|
23
|
-
"yaml": "^2.8.3"
|
|
23
|
+
"yaml": "^2.8.3",
|
|
24
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"rescript": "12.3.0",
|
|
27
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.
|
|
28
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"rescript": "12.3.0"
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"build": "rescript build",
|
|
42
43
|
"start": "rescript start",
|
|
43
44
|
"clean": "rescript clean",
|
|
45
|
+
"generate:currency": "node ./scripts/generate-currency.mjs",
|
|
44
46
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
45
47
|
"dev": "NODE_OPTIONS='--experimental-vm-modules' jest --watchAll"
|
|
46
48
|
}
|
package/rescript.json
CHANGED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generates `src/semantic/Currency.res` from the ISO 4217 table beside this
|
|
3
|
+
// script (`iso-4217-list-one.xml`, the standard's own "current currency & funds"
|
|
4
|
+
// publication).
|
|
5
|
+
//
|
|
6
|
+
// Why generated rather than hand-written: the codes and the minor-unit
|
|
7
|
+
// exponents have to agree, and the exponent is what makes `Money.format`
|
|
8
|
+
// derivable instead of a hardcoded `/100`. Taking both from one source makes
|
|
9
|
+
// them agree by construction — nobody has to remember that JPY has no decimals
|
|
10
|
+
// and TND has three.
|
|
11
|
+
//
|
|
12
|
+
// Updating to a newer ISO publication:
|
|
13
|
+
//
|
|
14
|
+
// curl -sL https://www.six-group.com/dam/download/financial-information/\
|
|
15
|
+
// data-center/iso-currrency/lists/list-one.xml \
|
|
16
|
+
// -o reventless/spec/scripts/iso-4217-list-one.xml
|
|
17
|
+
// pnpm --filter @reventlessdev/reventless-spec run generate:currency
|
|
18
|
+
//
|
|
19
|
+
// The output is committed: it is read in review, and a currency appearing or
|
|
20
|
+
// disappearing is exactly the kind of change that has to show up in a diff.
|
|
21
|
+
|
|
22
|
+
import {readFileSync, writeFileSync} from 'node:fs'
|
|
23
|
+
import {dirname, join} from 'node:path'
|
|
24
|
+
import {fileURLToPath} from 'node:url'
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const source = join(here, 'iso-4217-list-one.xml')
|
|
28
|
+
const target = join(here, '..', 'src', 'semantic', 'Currency.res')
|
|
29
|
+
|
|
30
|
+
const xml = readFileSync(source, 'utf8')
|
|
31
|
+
|
|
32
|
+
const published = xml.match(/<ISO_4217[^>]*Pblshd="([^"]+)"/)?.[1]
|
|
33
|
+
if (!published) throw new Error(`no Pblshd date in ${source} — is this the ISO 4217 list?`)
|
|
34
|
+
|
|
35
|
+
const field = (entry, tag) => entry.match(new RegExp(`<${tag}>([^<]*)</${tag}>`))?.[1]?.trim()
|
|
36
|
+
|
|
37
|
+
// One entry per country, so a currency used in several countries repeats. Keyed
|
|
38
|
+
// by code; a repeat that disagrees about the exponent is a corrupt table, not
|
|
39
|
+
// something to pick a winner from.
|
|
40
|
+
const byCode = new Map()
|
|
41
|
+
const skipped = []
|
|
42
|
+
|
|
43
|
+
for (const [, entry] of xml.matchAll(/<CcyNtry>([\s\S]*?)<\/CcyNtry>/g)) {
|
|
44
|
+
const code = field(entry, 'Ccy')
|
|
45
|
+
// Territories with no currency of their own (Antarctica) carry no <Ccy>.
|
|
46
|
+
if (!code) continue
|
|
47
|
+
const minorUnits = field(entry, 'CcyMnrUnts')
|
|
48
|
+
const name = field(entry, 'CcyNm')
|
|
49
|
+
|
|
50
|
+
// `N.A.` means the entry has no minor unit at all: the precious metals (XAU,
|
|
51
|
+
// XAG, XPD, XPT), the bond market units (XBA–XBD), XDR, XUA, XSU, the testing
|
|
52
|
+
// code XTS and the "no currency" sentinel XXX. Admitting them would make
|
|
53
|
+
// `exponent` partial, which is the one property this type exists to have — so
|
|
54
|
+
// the standard's own table draws the line rather than a curated opinion. A
|
|
55
|
+
// field holding a weight of gold is not holding money.
|
|
56
|
+
if (!/^\d+$/.test(minorUnits ?? '')) {
|
|
57
|
+
if (!skipped.some(s => s.code === code)) skipped.push({code, name, minorUnits})
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const exponent = Number(minorUnits)
|
|
62
|
+
const seen = byCode.get(code)
|
|
63
|
+
if (seen && seen.exponent !== exponent) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`${code} has two exponents in ${source}: ${seen.exponent} and ${exponent}`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
if (!seen) byCode.set(code, {code, name, exponent})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const currencies = [...byCode.values()].sort((a, b) => a.code.localeCompare(b.code))
|
|
72
|
+
if (currencies.length < 100) {
|
|
73
|
+
throw new Error(`only ${currencies.length} currencies parsed — the table did not parse`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The generated file quotes each currency's ISO name beside its constructor, so
|
|
77
|
+
// a reviewer reading a three-letter code does not have to look it up.
|
|
78
|
+
const constructors = currencies
|
|
79
|
+
.map(c => ` | /** ${c.name} */ ${c.code}`)
|
|
80
|
+
.join('\n')
|
|
81
|
+
|
|
82
|
+
const exponentArms = currencies
|
|
83
|
+
.map(c => ` | ${c.code} => ${c.exponent}`)
|
|
84
|
+
.join('\n')
|
|
85
|
+
|
|
86
|
+
const toStringArms = currencies.map(c => ` | ${c.code} => "${c.code}"`).join('\n')
|
|
87
|
+
|
|
88
|
+
// Wrapped rather than one 1,000-character line, so a code added or removed by a
|
|
89
|
+
// future ISO publication shows up as a one-line diff.
|
|
90
|
+
const all = currencies
|
|
91
|
+
.map(c => c.code)
|
|
92
|
+
.reduce((lines, code) => {
|
|
93
|
+
const last = lines[lines.length - 1]
|
|
94
|
+
if (last && `${last} ${code},`.length <= 96) lines[lines.length - 1] = `${last} ${code},`
|
|
95
|
+
else lines.push(` ${code},`)
|
|
96
|
+
return lines
|
|
97
|
+
}, [])
|
|
98
|
+
.join('\n')
|
|
99
|
+
|
|
100
|
+
const exponentCounts = [...new Set(currencies.map(c => c.exponent))]
|
|
101
|
+
.sort()
|
|
102
|
+
.map(e => `${currencies.filter(c => c.exponent === e).length}×${e}`)
|
|
103
|
+
.join(', ')
|
|
104
|
+
|
|
105
|
+
const skippedCodes = skipped.map(s => s.code).sort()
|
|
106
|
+
const group = codes => codes.filter(c => skippedCodes.includes(c)).join(', ')
|
|
107
|
+
const metals = group(['XAG', 'XAU', 'XPD', 'XPT'])
|
|
108
|
+
const bondUnits = group(['XBA', 'XBB', 'XBC', 'XBD'])
|
|
109
|
+
const rights = group(['XDR', 'XSU', 'XUA'])
|
|
110
|
+
|
|
111
|
+
const out = `// AUTO-GENERATED from ISO 4217 (published ${published}) — do not edit.
|
|
112
|
+
// Run \`pnpm --filter @reventlessdev/reventless-spec run generate:currency\`,
|
|
113
|
+
// or see \`scripts/generate-currency.mjs\` to update the source table first.
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
A currency, closed to the ${currencies.length} codes ISO 4217 defines a minor unit for.
|
|
117
|
+
|
|
118
|
+
## Why a type and not a three-letter string
|
|
119
|
+
|
|
120
|
+
A string field invites \`"eur"\` beside \`"EUR"\`, and two spellings of one
|
|
121
|
+
currency is a class of bug that reads as a data problem long after it became a
|
|
122
|
+
correctness problem — the values are present, and they simply never match. A
|
|
123
|
+
closed type makes the second spelling unwritable.
|
|
124
|
+
|
|
125
|
+
## Why generated, and why every code
|
|
126
|
+
|
|
127
|
+
The alternative was a curated handful (EUR, USD, GBP, JPY, …), which is small
|
|
128
|
+
and readable and wrong the first time an application needs a currency nobody
|
|
129
|
+
listed — a compile error in someone else's domain, fixable only by a framework
|
|
130
|
+
release. The thing being avoided by curating is ${currencies.length} constructors that are
|
|
131
|
+
machine-written and never read in full; the thing being risked is a release.
|
|
132
|
+
|
|
133
|
+
Generation also buys the property that makes this type worth having: \`exponent\`
|
|
134
|
+
comes from the *same* source as the codes, so it cannot drift from them
|
|
135
|
+
(${exponentCounts} decimal places across the set). That is what lets
|
|
136
|
+
\`Money.format\` derive its decimal placement instead of hardcoding \`/100\`, and
|
|
137
|
+
therefore what makes it correct for JPY and TND without anyone remembering that
|
|
138
|
+
those two are special.
|
|
139
|
+
|
|
140
|
+
## What is deliberately absent
|
|
141
|
+
|
|
142
|
+
The ${skipped.length} entries ISO lists with no minor unit: the precious metals
|
|
143
|
+
(${metals}), the bond market units (${bondUnits}), the accounting
|
|
144
|
+
units (${rights}), the testing code XTS, and the "no currency"
|
|
145
|
+
sentinel XXX. Each would make \`exponent\` partial, and a weight of gold is not
|
|
146
|
+
an amount of money. The standard's own table draws that line, so it is not a
|
|
147
|
+
curated opinion after all.
|
|
148
|
+
|
|
149
|
+
## The wire form
|
|
150
|
+
|
|
151
|
+
A payload-less variant, so the stored and transmitted form is the three-letter
|
|
152
|
+
code itself — \`{"amount": 1000, "currency": "EUR"}\`. Standard at the boundary,
|
|
153
|
+
a checked type in the domain.
|
|
154
|
+
*/
|
|
155
|
+
@schema
|
|
156
|
+
type t =
|
|
157
|
+
${constructors}
|
|
158
|
+
|
|
159
|
+
/** Every currency, in code order. \`fromString\` is derived from this, so a code
|
|
160
|
+
that parses and a code that exists are the same set by construction. */
|
|
161
|
+
let all: array<t> = [
|
|
162
|
+
${all}
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
/** The currency's ISO 4217 alphabetic code. */
|
|
166
|
+
let toString = (currency: t): string =>
|
|
167
|
+
switch currency {
|
|
168
|
+
${toStringArms}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
How many decimal places the currency's minor unit is: 2 for EUR, **0 for JPY**,
|
|
173
|
+
**3 for TND**, 4 for the Chilean Unidad de Fomento.
|
|
174
|
+
|
|
175
|
+
Total by construction — this is the whole reason the type is closed and the
|
|
176
|
+
table is generated. An amount is stored in integer minor units, so this is the
|
|
177
|
+
only thing that says where its decimal point goes.
|
|
178
|
+
*/
|
|
179
|
+
let exponent = (currency: t): int =>
|
|
180
|
+
switch currency {
|
|
181
|
+
${exponentArms}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let byCode: dict<t> = {
|
|
185
|
+
let d = Dict.make()
|
|
186
|
+
all->Array.forEach(c => d->Dict.set(toString(c), c))
|
|
187
|
+
d
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
Parse an ISO 4217 alphabetic code, saying why when it is not one.
|
|
192
|
+
|
|
193
|
+
Case-sensitive on purpose: \`"eur"\` is rejected rather than repaired. This type
|
|
194
|
+
exists because a silent case mismatch is expensive to find, and quietly
|
|
195
|
+
accepting the wrong spelling at the boundary would put it back — a producer
|
|
196
|
+
sending lowercase codes should learn that at its first request, not at the first
|
|
197
|
+
report that two halves of a ledger disagree.
|
|
198
|
+
*/
|
|
199
|
+
let fromString = (raw: string): result<t, string> =>
|
|
200
|
+
switch byCode->Dict.get(raw) {
|
|
201
|
+
| Some(c) => Ok(c)
|
|
202
|
+
| None =>
|
|
203
|
+
Error(
|
|
204
|
+
\`expected an ISO 4217 currency code such as "EUR", got \${raw
|
|
205
|
+
->JSON.Encode.string
|
|
206
|
+
->JSON.stringify}. Codes are upper-case and exactly three letters.\`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
`
|
|
210
|
+
|
|
211
|
+
writeFileSync(target, out)
|
|
212
|
+
console.log(
|
|
213
|
+
`Currency.res: ${currencies.length} currencies (${exponentCounts} decimals), ` +
|
|
214
|
+
`ISO 4217 published ${published}, ${skipped.length} minor-unit-less entries skipped.`,
|
|
215
|
+
)
|