@reventlessdev/reventless-spec 3.0.0-alpha.123 → 3.0.0-alpha.125
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 +56 -0
- package/package.json +5 -3
- package/run-certify-trait.mjs +2 -0
- package/run-graft-trait.mjs +2 -0
- package/run-trait-manifest.mjs +2 -0
- package/schema/platform-api.graphql +41 -3
- package/src/components/Aggregate.res +22 -0
- package/src/components/AutomationSlice.res +29 -3
- package/src/components/CapabilityManifest.res +52 -24
- package/src/components/CapabilityManifest.res.mjs +35 -11
- package/src/components/InboundTranslationSlice.res +14 -0
- package/src/components/OutboundTranslationSlice.res +24 -0
- package/src/components/Plugin.res +202 -422
- package/src/components/Plugin.res.mjs +65 -3
- package/src/components/StateChangeSlice.res +22 -0
- package/src/components/TraitCertificate.res +105 -0
- package/src/components/TraitCertificate.res.mjs +65 -0
- package/src/components/TraitManifest.res +90 -0
- package/src/components/TraitManifest.res.mjs +48 -0
- package/src/generator/CertifyTrait.res +190 -0
- package/src/generator/CertifyTrait.res.mjs +154 -0
- package/src/generator/GraftTrait.res +230 -0
- package/src/generator/GraftTrait.res.mjs +193 -0
- package/src/generator/PlatformCodegen.res +44 -30
- package/src/generator/PlatformCodegen.res.mjs +36 -17
- package/src/generator/TraitManifestCli.res +138 -0
- package/src/generator/TraitManifestCli.res.mjs +105 -0
- package/src/semantic/Capabilities.res +19 -3
- package/src/semantic/Capabilities.res.mjs +18 -2
- package/src/semantic/CapabilityNeed.res +81 -0
- package/src/semantic/CapabilityNeed.res.mjs +46 -0
- package/src/semantic/Currency.res +519 -502
- package/src/semantic/Currency.res.mjs +7 -655
- package/src/semantic/Messaging.res +127 -0
- package/src/semantic/Messaging.res.mjs +57 -0
- package/src/semantic/Money.res +123 -21
- package/src/semantic/Money.res.mjs +49 -2
- package/src/types/Trait.res +62 -0
- package/src/types/Trait.res.mjs +18 -0
- package/src/types/Transition.res +71 -0
- package/src/types/Transition.res.mjs +36 -0
- package/scripts/generate-currency.mjs +0 -215
- package/scripts/iso-4217-list-one.xml +0 -1956
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Sending a message to a person, and deciding whether to try again.
|
|
3
|
+
|
|
4
|
+
The transport is provider-specific and lives with its provider. What is here is
|
|
5
|
+
provider-neutral: who a message can be addressed to, what a send can answer, the
|
|
6
|
+
retry rule — decided once, so no transport invents its own.
|
|
7
|
+
|
|
8
|
+
## One value carries the channel and the address
|
|
9
|
+
|
|
10
|
+
A `(channel, address)` pair can be built wrong: `Sms` beside an email address
|
|
11
|
+
compiles and fails at the provider. `recipient` fuses them, so the wrong pair
|
|
12
|
+
does not exist, and the channel is read back off the value that carries it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A delivery route. The selector a recipient chooses per notification kind, and
|
|
16
|
+
the granularity a platform provisions at. */
|
|
17
|
+
type channel =
|
|
18
|
+
| Email
|
|
19
|
+
| Sms
|
|
20
|
+
| Push
|
|
21
|
+
|
|
22
|
+
/** An addressed recipient: the channel and the address it needs, inseparable.
|
|
23
|
+
Each address is the branded scalar for its channel, so an unparseable one is
|
|
24
|
+
refused where it is built rather than by the provider. */
|
|
25
|
+
type recipient =
|
|
26
|
+
| ToEmail(Email.t)
|
|
27
|
+
| ToSms(Phone.t)
|
|
28
|
+
| /** The token the device registered with the push service. Opaque and
|
|
29
|
+
provider-shaped — unlike an address, nobody else has a grammar for it. */
|
|
30
|
+
ToPush({deviceToken: string})
|
|
31
|
+
|
|
32
|
+
/** The channel a recipient is addressed on. */
|
|
33
|
+
let channelOf = (recipient: recipient): channel =>
|
|
34
|
+
switch recipient {
|
|
35
|
+
| ToEmail(_) => Email
|
|
36
|
+
| ToSms(_) => Sms
|
|
37
|
+
| ToPush(_) => Push
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The channel's name, for a message a human reads and for a preference key. */
|
|
41
|
+
let channelToString = (channel: channel): string =>
|
|
42
|
+
switch channel {
|
|
43
|
+
| Email => "Email"
|
|
44
|
+
| Sms => "Sms"
|
|
45
|
+
| Push => "Push"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
What to say.
|
|
50
|
+
|
|
51
|
+
`subject` is carried for the channels that have one — an email header, a push
|
|
52
|
+
notification's title — and ignored by those that do not. Optional rather than an
|
|
53
|
+
empty string, so "this message has no subject" and "its subject is blank" stay
|
|
54
|
+
distinguishable.
|
|
55
|
+
*/
|
|
56
|
+
type message = {subject?: string, body: string}
|
|
57
|
+
|
|
58
|
+
/** The provider accepted the message. `ref` is its own id for it — what a
|
|
59
|
+
support conversation about a missing message is conducted with, and the only
|
|
60
|
+
thing a caller can record that the provider will recognise. */
|
|
61
|
+
type receipt = {ref: string}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
Why a send produced no receipt.
|
|
65
|
+
|
|
66
|
+
Three constructors because the retry decision turns on the distinction, and
|
|
67
|
+
getting it wrong is expensive in both directions: retrying a refused address
|
|
68
|
+
burns the budget on an outcome that will not change, and abandoning a transient
|
|
69
|
+
outage writes off a message that would have gone.
|
|
70
|
+
*/
|
|
71
|
+
type failure =
|
|
72
|
+
| /** The provider could not be reached, or refused the call. Retry. */
|
|
73
|
+
Unavailable(string)
|
|
74
|
+
| /** This deployment provisions nothing for this channel. Do not retry — no
|
|
75
|
+
number of attempts provisions one. */
|
|
76
|
+
UnsupportedChannel(channel)
|
|
77
|
+
| /** The provider answered and will not take this message: an address it
|
|
78
|
+
rejects, a recipient it suppresses. Do not retry. */
|
|
79
|
+
Refused(string)
|
|
80
|
+
|
|
81
|
+
/** The retry rule, stated once. Everything that sweeps a failed send derives
|
|
82
|
+
from it rather than re-reading the constructors. */
|
|
83
|
+
let retriable = (failure: failure): bool =>
|
|
84
|
+
switch failure {
|
|
85
|
+
| Unavailable(_) => true
|
|
86
|
+
| UnsupportedChannel(_)
|
|
87
|
+
| Refused(_) => false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A reason for a human, for a caller that records the outcome rather than
|
|
91
|
+
acting on it. */
|
|
92
|
+
let failureReason = (failure: failure): string =>
|
|
93
|
+
switch failure {
|
|
94
|
+
| Unavailable(reason) => reason
|
|
95
|
+
| UnsupportedChannel(channel) =>
|
|
96
|
+
`this deployment provisions no ${channel->channelToString} channel`
|
|
97
|
+
| Refused(reason) => reason
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
The port a caller reaches a messaging provider through, so swapping the
|
|
102
|
+
implementation is a change of supplier rather than of call site.
|
|
103
|
+
*/
|
|
104
|
+
type send = (~recipient: recipient, ~message: message) => promise<result<receipt, failure>>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
The capability as a slice receives it: what can be attempted, and how.
|
|
108
|
+
|
|
109
|
+
`channels` is here because a recipient cannot be offered a choice the deployment
|
|
110
|
+
cannot honour. A platform provisions email only, or email and SMS; a preference
|
|
111
|
+
centre that listed all three would collect a subscription every send then answers
|
|
112
|
+
`UnsupportedChannel` for. Published rather than inferred from a failed send,
|
|
113
|
+
because discovering a channel by failing on it costs a real message.
|
|
114
|
+
|
|
115
|
+
Empty means no channel at all — the shape `none` takes, and the one a deploy-time
|
|
116
|
+
gate exists to catch before it ships.
|
|
117
|
+
*/
|
|
118
|
+
type provider = {
|
|
119
|
+
channels: array<channel>,
|
|
120
|
+
send: send,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether this provider can attempt a recipient's channel at all. The check a
|
|
124
|
+
caller makes before spending a send, and the same rule the provider applies
|
|
125
|
+
internally, so the two cannot disagree. */
|
|
126
|
+
let supports = (provider: provider, ~recipient: recipient): bool =>
|
|
127
|
+
provider.channels->Array.includes(recipient->channelOf)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function channelOf(recipient) {
|
|
5
|
+
switch (recipient.TAG) {
|
|
6
|
+
case "ToEmail" :
|
|
7
|
+
return "Email";
|
|
8
|
+
case "ToSms" :
|
|
9
|
+
return "Sms";
|
|
10
|
+
case "ToPush" :
|
|
11
|
+
return "Push";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function channelToString(channel) {
|
|
16
|
+
switch (channel) {
|
|
17
|
+
case "Email" :
|
|
18
|
+
return "Email";
|
|
19
|
+
case "Sms" :
|
|
20
|
+
return "Sms";
|
|
21
|
+
case "Push" :
|
|
22
|
+
return "Push";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function retriable(failure) {
|
|
27
|
+
switch (failure.TAG) {
|
|
28
|
+
case "Unavailable" :
|
|
29
|
+
return true;
|
|
30
|
+
case "UnsupportedChannel" :
|
|
31
|
+
case "Refused" :
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failureReason(failure) {
|
|
37
|
+
switch (failure.TAG) {
|
|
38
|
+
case "UnsupportedChannel" :
|
|
39
|
+
return `this deployment provisions no ` + channelToString(failure._0) + ` channel`;
|
|
40
|
+
case "Unavailable" :
|
|
41
|
+
case "Refused" :
|
|
42
|
+
return failure._0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function supports(provider, recipient) {
|
|
47
|
+
return provider.channels.includes(channelOf(recipient));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
channelOf,
|
|
52
|
+
channelToString,
|
|
53
|
+
retriable,
|
|
54
|
+
failureReason,
|
|
55
|
+
supports,
|
|
56
|
+
}
|
|
57
|
+
/* No side effect */
|
package/src/semantic/Money.res
CHANGED
|
@@ -5,7 +5,7 @@ those units belong to.
|
|
|
5
5
|
## Why the currency travels with the number
|
|
6
6
|
|
|
7
7
|
A minor unit is currency-dependent — ISO 4217 gives EUR two decimal places, **JPY
|
|
8
|
-
zero** and
|
|
8
|
+
zero** and TND three — so a bare `1000` is €10.00 or ¥1000 or 1.000 TND, and
|
|
9
9
|
there is no way to tell which. It cannot be rendered, compared, summed or
|
|
10
10
|
sanity-checked without knowing. The currency is part of the number's meaning, not
|
|
11
11
|
metadata beside it.
|
|
@@ -22,6 +22,20 @@ instead — see below.
|
|
|
22
22
|
`0.1 +. 0.2` is not `0.3`, and money is summed. Minor units keep every amount an
|
|
23
23
|
exact integer, so addition is exact and equality means what it says.
|
|
24
24
|
|
|
25
|
+
This was tried the other way — an amount holding the decimal a person types —
|
|
26
|
+
and reverted. What it bought was a readable log; what it cost was the guarantee.
|
|
27
|
+
Every sum outside `add` became a float sum, including the ones a read side does
|
|
28
|
+
over a page of rows, and the failure is a figure that is a cent out and looks
|
|
29
|
+
entirely plausible. Binary floating point is not what money is counted in, and
|
|
30
|
+
holding the exact value in a `float` only works while nothing does arithmetic on
|
|
31
|
+
it — which is not a property a framework type can promise on behalf of everyone
|
|
32
|
+
downstream.
|
|
33
|
+
|
|
34
|
+
**The complaint that prompted the attempt was real and is answered elsewhere.** A
|
|
35
|
+
form generated from this schema must not ask anyone for `1050`; it asks for
|
|
36
|
+
`10.50` and converts at its own boundary, with `ofMajor`/`toMajor` below. That is
|
|
37
|
+
a presentation concern, and this type had been letting it leak.
|
|
38
|
+
|
|
25
39
|
## Why `float` for a whole number
|
|
26
40
|
|
|
27
41
|
Because ReScript's `int` is int32, and sury enforces that — an `int` amount caps
|
|
@@ -72,12 +86,15 @@ let validateAmount = (amount: float): result<float, string> =>
|
|
|
72
86
|
Ok(amount)
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
/** The amount's own schema.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
/** The amount's own schema.
|
|
90
|
+
|
|
91
|
+
The check sits on the field rather than on the pair because wholeness is a
|
|
92
|
+
property of the amount: it does not need the currency, and a rule placed
|
|
93
|
+
where it needs nothing else is a rule that cannot be read wrong. (A
|
|
94
|
+
record-level refinement is available — sury 11.0.0-rc.2 compiles one
|
|
95
|
+
correctly, where 11-alpha hoisted the result object above the field reads and
|
|
96
|
+
threw `Cannot access 'v0' before initialization` — it is simply not what this
|
|
97
|
+
check wants.) */
|
|
81
98
|
let amountSchema: S.t<float> =
|
|
82
99
|
S.float->S.refine(
|
|
83
100
|
amount =>
|
|
@@ -109,34 +126,41 @@ let make = (~amount: float, ~currency: Currency.t): t => {amount, currency}
|
|
|
109
126
|
euros" are different claims, and only the second one adds to a total. */
|
|
110
127
|
let zero = (~currency: Currency.t): t => {amount: 0.0, currency}
|
|
111
128
|
|
|
129
|
+
/** Ten to the power of the currency's decimal count — the factor between a major
|
|
130
|
+
amount and the minor units it is. Named because the two conversions below
|
|
131
|
+
both need it and neither should spell out `Math.pow` again. */
|
|
132
|
+
let scale = (currency: Currency.t): float =>
|
|
133
|
+
Math.pow(10.0, ~exp=Int.toFloat(Currency.exponent(currency)))
|
|
134
|
+
|
|
112
135
|
/**
|
|
113
136
|
Convert a major-unit decimal (`10.5`) into minor units (`1050`), using the
|
|
114
137
|
currency's own exponent.
|
|
115
138
|
|
|
116
139
|
This is the one place a decimal is allowed to become money, and it is here rather
|
|
117
140
|
than at each call site precisely so that `*. 100.0` is written once and is
|
|
118
|
-
correct for JPY and TND — where it would be `*. 1.0` and `*. 1000.0`.
|
|
141
|
+
correct for JPY and TND — where it would be `*. 1.0` and `*. 1000.0`. It is what
|
|
142
|
+
a form, a supplier feed or any other boundary that speaks in decimals converts
|
|
143
|
+
through.
|
|
119
144
|
|
|
120
|
-
Rounds half away from zero
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
Rounds half away from zero, in both directions: `Math.round` alone rounds half
|
|
146
|
+
toward positive infinity, so `-2.5` would come back `-2` and a refund would round
|
|
147
|
+
the other way from the charge it reverses. That is a *boundary conversion* and
|
|
148
|
+
not an arithmetic policy: the half-even question that FX and tax rounding turn on
|
|
149
|
+
is a separate decision, and nothing here forecloses it.
|
|
124
150
|
*/
|
|
125
151
|
let ofMajor = (~amount: float, ~currency: Currency.t): t => {
|
|
126
|
-
let
|
|
127
|
-
{amount: Math.round(
|
|
152
|
+
let scaled = amount *. scale(currency)
|
|
153
|
+
{amount: scaled < 0.0 ? -.Math.round(-.scaled) : Math.round(scaled), currency}
|
|
128
154
|
}
|
|
129
155
|
|
|
130
|
-
/** The amount as a major-unit decimal —
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let toMajor = (m: t): float =>
|
|
134
|
-
m.amount /. Math.pow(10.0, ~exp=Int.toFloat(Currency.exponent(m.currency)))
|
|
156
|
+
/** The amount as a major-unit decimal — what a form shows, and what a chart axis
|
|
157
|
+
or an average needs. Lossy by nature: the result is a float again, so it is an
|
|
158
|
+
output, not something to compute a balance in. */
|
|
159
|
+
let toMajor = (m: t): float => m.amount /. scale(m.currency)
|
|
135
160
|
|
|
136
161
|
/**
|
|
137
162
|
The amount as text: the decimal point placed by the currency's exponent, digits
|
|
138
|
-
grouped in threes, and the ISO code after it — `"1,234.50 EUR"`, `"1,000 JPY"
|
|
139
|
-
`"1.000 TND"`.
|
|
163
|
+
grouped in threes, and the ISO code after it — `"1,234.50 EUR"`, `"1,000 JPY"`.
|
|
140
164
|
|
|
141
165
|
Deliberately locale-independent, matching the rest of the framework's
|
|
142
166
|
formatters: the same value reads the same in every log line and every test. A
|
|
@@ -176,6 +200,9 @@ branded-scalar shape was that mixing currencies could not be written at all; the
|
|
|
176
200
|
answer is to *check* it rather than to delete the information that makes checking
|
|
177
201
|
possible. An aggregate that must hold one currency rejects a line item in
|
|
178
202
|
another — a decider's concern, and one it can now actually express.
|
|
203
|
+
|
|
204
|
+
The addition itself is integer addition, because both amounts are whole minor
|
|
205
|
+
units. That is the whole reason they are.
|
|
179
206
|
*/
|
|
180
207
|
let add = (a: t, b: t): result<t, string> =>
|
|
181
208
|
a.currency == b.currency
|
|
@@ -185,6 +212,81 @@ let add = (a: t, b: t): result<t, string> =>
|
|
|
185
212
|
`Converting between them needs a rate, which is not something an amount carries.`,
|
|
186
213
|
)
|
|
187
214
|
|
|
215
|
+
/** The largest whole number a `float` holds exactly, 2^53 - 1. Bound here
|
|
216
|
+
because `Float.Constants` does not carry it, and named because it is the
|
|
217
|
+
range every guarantee in this module is stated over. */
|
|
218
|
+
@val @scope("Number") external maxSafeInteger: float = "MAX_SAFE_INTEGER"
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
Multiply an amount by a count — a line item's unit price by its quantity.
|
|
222
|
+
|
|
223
|
+
`~by` is an `int` because the operand is a *count* and not a rate: three of
|
|
224
|
+
something, not 15% of something. Whole minor units times a whole count is
|
|
225
|
+
whole-number arithmetic, so the result is an exact amount and no rounding
|
|
226
|
+
question arises at all. That is the property this function exists to keep, and it
|
|
227
|
+
is why a call site should not write `amount *. Int.toFloat(quantity)` itself.
|
|
228
|
+
|
|
229
|
+
A rate is the other operation, and deliberately not this one. 15% of €10.50 is
|
|
230
|
+
€8.925, which is not an amount of euros in the first place — so applying a rate
|
|
231
|
+
is a rounding decision the domain has to state, through `toMajor`, the
|
|
232
|
+
arithmetic, and `ofMajor` back, with the rounding visible where someone chose it.
|
|
233
|
+
|
|
234
|
+
Refuses a product past the range where a `float` is an exact integer. Handing
|
|
235
|
+
back an inexact amount is the failure the wholeness check exists to prevent, and
|
|
236
|
+
it would not do to introduce it here.
|
|
237
|
+
*/
|
|
238
|
+
let times = (m: t, ~by: int): result<t, string> => {
|
|
239
|
+
let product = m.amount *. Int.toFloat(by)
|
|
240
|
+
Math.abs(product) <= maxSafeInteger
|
|
241
|
+
? Ok({amount: product, currency: m.currency})
|
|
242
|
+
: Error(
|
|
243
|
+
`cannot multiply ${format(m)} by ${Int.toString(by)}: the result is past the ` ++
|
|
244
|
+
`largest amount that stays exact, ${Float.toString(maxSafeInteger)} minor units.`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
Split an amount into `into` parts that add back up to it.
|
|
250
|
+
|
|
251
|
+
The last minor unit has to go somewhere. Dividing and rounding each part on its
|
|
252
|
+
own loses or invents one — €10.00 into three is €3.33 three times, which is
|
|
253
|
+
€9.99 — so the remainder is handed out instead: the first parts get one minor
|
|
254
|
+
unit more than the rest. €10.00 into three is €3.34, €3.33, €3.33, and those add
|
|
255
|
+
up to €10.00.
|
|
256
|
+
|
|
257
|
+
*Which* parts get the extra unit is arbitrary, but it has to be decided rather
|
|
258
|
+
than left to a rounding mode, and "the earliest" is the decision here. A caller
|
|
259
|
+
that needs it somewhere else — the largest share, the payer's — reorders what it
|
|
260
|
+
gets back. A caller that needs uneven shares wants a split by ratio, which is a
|
|
261
|
+
different function and not this one.
|
|
262
|
+
|
|
263
|
+
A negative amount splits away from zero the same way: -€10.00 into three is
|
|
264
|
+
-€3.34, -€3.33, -€3.33. A refund divides like a charge.
|
|
265
|
+
*/
|
|
266
|
+
let allocate = (m: t, ~into: int): result<array<t>, string> =>
|
|
267
|
+
if into <= 0 {
|
|
268
|
+
Error(
|
|
269
|
+
`cannot split ${format(m)} into ${Int.toString(into)} parts: ` ++
|
|
270
|
+
`a split is into at least one part.`,
|
|
271
|
+
)
|
|
272
|
+
} else {
|
|
273
|
+
let parts = Int.toFloat(into)
|
|
274
|
+
// Truncated toward zero, so the remainder carries the sign of the amount and
|
|
275
|
+
// every part stays on the same side of zero as the whole it came from.
|
|
276
|
+
let share = Math.trunc(m.amount /. parts)
|
|
277
|
+
// Taken from the share actually used rather than from the ideal one, which
|
|
278
|
+
// is what makes the parts add back up by construction.
|
|
279
|
+
let remainder = m.amount -. share *. parts
|
|
280
|
+
let step = remainder < 0.0 ? -1.0 : 1.0
|
|
281
|
+
let extra = Math.abs(remainder)
|
|
282
|
+
Ok(
|
|
283
|
+
Array.fromInitializer(~length=into, i => {
|
|
284
|
+
amount: share +. (Int.toFloat(i) < extra ? step : 0.0),
|
|
285
|
+
currency: m.currency,
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
188
290
|
/** Add a run of amounts, refusing at the first currency that does not match the
|
|
189
291
|
first amount's. `None` for an empty run — the sum of no amounts has no
|
|
190
292
|
currency to be in. */
|
|
@@ -54,10 +54,14 @@ function zero(currency) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function scale(currency) {
|
|
58
|
+
return Math.pow(10.0, Currency$Reventless.exponent(currency));
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
function ofMajor(amount, currency) {
|
|
58
|
-
let
|
|
62
|
+
let scaled = amount * Math.pow(10.0, Currency$Reventless.exponent(currency));
|
|
59
63
|
return {
|
|
60
|
-
amount: Math.round(
|
|
64
|
+
amount: scaled < 0.0 ? - Math.round(- scaled) : Math.round(scaled),
|
|
61
65
|
currency: currency
|
|
62
66
|
};
|
|
63
67
|
}
|
|
@@ -108,6 +112,46 @@ function add(a, b) {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function times(m, by) {
|
|
116
|
+
let product = m.amount * by;
|
|
117
|
+
if (Math.abs(product) <= Number.MAX_SAFE_INTEGER) {
|
|
118
|
+
return {
|
|
119
|
+
TAG: "Ok",
|
|
120
|
+
_0: {
|
|
121
|
+
amount: product,
|
|
122
|
+
currency: m.currency
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
return {
|
|
127
|
+
TAG: "Error",
|
|
128
|
+
_0: `cannot multiply ` + format(m) + ` by ` + by.toString() + `: the result is past the ` + (`largest amount that stays exact, ` + Number.MAX_SAFE_INTEGER.toString() + ` minor units.`)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function allocate(m, into) {
|
|
134
|
+
if (into <= 0) {
|
|
135
|
+
return {
|
|
136
|
+
TAG: "Error",
|
|
137
|
+
_0: `cannot split ` + format(m) + ` into ` + into.toString() + ` parts: a split is into at least one part.`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
let share = Math.trunc(m.amount / into);
|
|
141
|
+
let remainder = m.amount - share * into;
|
|
142
|
+
let step = remainder < 0.0 ? -1.0 : 1.0;
|
|
143
|
+
let extra = Math.abs(remainder);
|
|
144
|
+
return {
|
|
145
|
+
TAG: "Ok",
|
|
146
|
+
_0: Stdlib_Array.fromInitializer(into, i => ({
|
|
147
|
+
amount: share + (
|
|
148
|
+
i < extra ? step : 0.0
|
|
149
|
+
),
|
|
150
|
+
currency: m.currency
|
|
151
|
+
}))
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
111
155
|
function sum(amounts) {
|
|
112
156
|
if (amounts.length !== 0) {
|
|
113
157
|
return Stdlib_Array.reduce(amounts, {
|
|
@@ -126,10 +170,13 @@ export {
|
|
|
126
170
|
schema$1 as schema,
|
|
127
171
|
make,
|
|
128
172
|
zero,
|
|
173
|
+
scale,
|
|
129
174
|
ofMajor,
|
|
130
175
|
toMajor,
|
|
131
176
|
format,
|
|
132
177
|
add,
|
|
178
|
+
times,
|
|
179
|
+
allocate,
|
|
133
180
|
sum,
|
|
134
181
|
}
|
|
135
182
|
/* amountSchema Not a pure module */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// What a domain trait says about itself, so that a graft leaves a trace.
|
|
2
|
+
//
|
|
3
|
+
// A graft becomes ordinary host source — that is the design, and it is why every
|
|
4
|
+
// other signal a trait leaves is source-side: the package dependency, the variant
|
|
5
|
+
// spread, the `module X = Trait_Rules` alias, the conformance binding under
|
|
6
|
+
// `tests/`. None of them survives into a deployed plugin, so a running estate
|
|
7
|
+
// cannot answer "which of my components came from a trait" at all.
|
|
8
|
+
//
|
|
9
|
+
// This is the one fact that does survive, because it travels the way
|
|
10
|
+
// `capabilityNeeds` does: a value the trait exports and the host names, collected
|
|
11
|
+
// into `pluginStructure` while it is assembled and re-emitted whole on every
|
|
12
|
+
// registration.
|
|
13
|
+
//
|
|
14
|
+
// **Nothing here is typed by a human.** The trait exports its own identity, so a
|
|
15
|
+
// renamed or removed trait is a build error rather than a stale row; the version
|
|
16
|
+
// is read from the trait's own package rather than restated; and the component is
|
|
17
|
+
// filled in by the structure, which is the only party that knows which component
|
|
18
|
+
// declared it. That is the whole reason this is a value rather than an
|
|
19
|
+
// annotation — an attribute's fields would all be strings the compiler never
|
|
20
|
+
// checks, which is the failure this program exists to remove.
|
|
21
|
+
//
|
|
22
|
+
// **A declaration is a claim about origin, never about behaviour.** After a graft
|
|
23
|
+
// the developer owns the files and may edit them freely. So this says "grafted
|
|
24
|
+
// from trait X", and it does NOT say "still behaves like trait X" — the thing that
|
|
25
|
+
// answers the second question is the trait's conformance suite, which runs in the
|
|
26
|
+
// consumer's build and reports separately. A reader that paints a declared graft
|
|
27
|
+
// as verified is drawing a conclusion this field cannot support.
|
|
28
|
+
|
|
29
|
+
/** Whether the graft reports back into the host it is grafted onto.
|
|
30
|
+
|
|
31
|
+
`WritesBack` publishes commands to its host — the geocoding shape, where the
|
|
32
|
+
slice reports its answer back onto the aggregate. `Observes` reads host events
|
|
33
|
+
and writes nothing back. `SelfContained` brings its own components and grafts
|
|
34
|
+
only by reading — the notification shape, which is what broke the
|
|
35
|
+
write-back assumption the first two specimens shared. */
|
|
36
|
+
type posture =
|
|
37
|
+
| WritesBack
|
|
38
|
+
| Observes
|
|
39
|
+
| SelfContained
|
|
40
|
+
|
|
41
|
+
let postureToString = (p: posture): string =>
|
|
42
|
+
switch p {
|
|
43
|
+
| WritesBack => "WritesBack"
|
|
44
|
+
| Observes => "Observes"
|
|
45
|
+
| SelfContained => "SelfContained"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
One trait's own account of itself, exported by the trait package.
|
|
50
|
+
|
|
51
|
+
`version` is read from the trait's package at load time rather than restated in
|
|
52
|
+
source — see `PackageVersion.fromModuleUrl`, which the certification CLI already
|
|
53
|
+
resolves trait versions with. A trait whose package.json cannot be found reports
|
|
54
|
+
`"0.0.0"`, which is visibly wrong rather than quietly stale.
|
|
55
|
+
*/
|
|
56
|
+
type t = {
|
|
57
|
+
/** The package, as it is depended on: `"@reventlessdev/trait-attachments"`. */
|
|
58
|
+
trait: string,
|
|
59
|
+
/** Resolved from the trait's own package, never written by hand. */
|
|
60
|
+
version: string,
|
|
61
|
+
posture: posture,
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function postureToString(p) {
|
|
5
|
+
switch (p) {
|
|
6
|
+
case "WritesBack" :
|
|
7
|
+
return "WritesBack";
|
|
8
|
+
case "Observes" :
|
|
9
|
+
return "Observes";
|
|
10
|
+
case "SelfContained" :
|
|
11
|
+
return "SelfContained";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
postureToString,
|
|
17
|
+
}
|
|
18
|
+
/* No side effect */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The lifecycle edge a command owns, declared as a value rather than as an
|
|
2
|
+
// attribute on the constructor.
|
|
3
|
+
//
|
|
4
|
+
// `@transition([Orders.Placed] => Orders.Shipped)` says the same thing and is
|
|
5
|
+
// still the shorter spelling for a command a host declares itself. It cannot say
|
|
6
|
+
// it for a command a host did NOT declare: a variant spread splices members,
|
|
7
|
+
// while the annotation lowers to a dict on the parent union, so a spliced
|
|
8
|
+
// command arrives carrying no edge at all. Nor can the annotation be checked —
|
|
9
|
+
// the PPX extracts leaf identifiers as strings, and the states belong to another
|
|
10
|
+
// component's enum, so a misspelling survives to the plugin structure.
|
|
11
|
+
//
|
|
12
|
+
// A `command => t<'state>` switch answers both. It is exhaustive, so a spliced
|
|
13
|
+
// constructor is a compile error until the host says what it does; and `'state`
|
|
14
|
+
// is the view's own lifecycle enum, so `Customers.Active` is a constructor the
|
|
15
|
+
// compiler resolves rather than a string nobody reads.
|
|
16
|
+
//
|
|
17
|
+
// `'state` is one type across the whole switch, which is a third thing the
|
|
18
|
+
// annotation cannot do: every arm of one component's edges must name the same
|
|
19
|
+
// lifecycle, and a from-set drawn from one enum with a target from another does
|
|
20
|
+
// not compile.
|
|
21
|
+
//
|
|
22
|
+
// The type stays parameterised all the way down rather than storing names,
|
|
23
|
+
// because erasing a constructor to its own name means asserting its runtime
|
|
24
|
+
// representation — and this is the module that exists so nothing has to be
|
|
25
|
+
// asserted. The erasure happens once, at the framework's type-erasure boundary
|
|
26
|
+
// (`Plugin_Structure`), which already reads every spec member that way.
|
|
27
|
+
//
|
|
28
|
+
// The reference costs nothing at run time. A lifecycle enum's arms are
|
|
29
|
+
// payload-less, so `[Customers.Active]` compiles to `["Active"]` and the
|
|
30
|
+
// generated module imports nothing from the view — which is also why it cannot
|
|
31
|
+
// cycle: a view spec holds no reference back to the aggregate it projects.
|
|
32
|
+
//
|
|
33
|
+
// Read the same way `commandAuthorization` is: `Plugin_Structure.toCommandDef`
|
|
34
|
+
// evaluates it against a synthetic value per constructor.
|
|
35
|
+
//
|
|
36
|
+
// No `@schema`: nothing serialises a transition. It is read once, while the
|
|
37
|
+
// plugin structure is assembled, and what leaves is the pair of names the
|
|
38
|
+
// structure already carried.
|
|
39
|
+
type t<'state> =
|
|
40
|
+
/** No edge declared: legal in every state, moves the row nowhere. The honest
|
|
41
|
+
answer for a report a slice publishes, which must not be refused because
|
|
42
|
+
the row moved on while the report was in flight. */
|
|
43
|
+
| Unrestricted
|
|
44
|
+
/** Brings the row into existence, so there is no state it could come from.
|
|
45
|
+
Distinct from `Unrestricted`, which draws no edge at all. */
|
|
46
|
+
| Creates('state)
|
|
47
|
+
/** Legal in these states, and moves the row nowhere. A positive claim rather
|
|
48
|
+
than an omission. */
|
|
49
|
+
| Guards(array<'state>)
|
|
50
|
+
/** Legal in these states, and lands the row in that one. */
|
|
51
|
+
| Moves(array<'state>, 'state)
|
|
52
|
+
|
|
53
|
+
/** The from-set, or `None` for a command that names no states to come from —
|
|
54
|
+
which `Creates` and `Unrestricted` both do, for different reasons the target
|
|
55
|
+
tells apart. */
|
|
56
|
+
let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
57
|
+
switch transition {
|
|
58
|
+
| Unrestricted
|
|
59
|
+
| Creates(_) => None
|
|
60
|
+
| Guards(states)
|
|
61
|
+
| Moves(states, _) => Some(states)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The state the command's handler writes, or `None` for one that moves nothing. */
|
|
65
|
+
let targetState = (transition: t<'state>): option<'state> =>
|
|
66
|
+
switch transition {
|
|
67
|
+
| Unrestricted
|
|
68
|
+
| Guards(_) => None
|
|
69
|
+
| Creates(state)
|
|
70
|
+
| Moves(_, state) => Some(state)
|
|
71
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
|
+
|
|
5
|
+
function allowedStates(transition) {
|
|
6
|
+
if (typeof transition !== "object") {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
switch (transition.TAG) {
|
|
10
|
+
case "Creates" :
|
|
11
|
+
return;
|
|
12
|
+
case "Guards" :
|
|
13
|
+
case "Moves" :
|
|
14
|
+
return transition._0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function targetState(transition) {
|
|
19
|
+
if (typeof transition !== "object") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
switch (transition.TAG) {
|
|
23
|
+
case "Creates" :
|
|
24
|
+
return Primitive_option.some(transition._0);
|
|
25
|
+
case "Guards" :
|
|
26
|
+
return;
|
|
27
|
+
case "Moves" :
|
|
28
|
+
return Primitive_option.some(transition._1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
allowedStates,
|
|
34
|
+
targetState,
|
|
35
|
+
}
|
|
36
|
+
/* No side effect */
|