@reventlessdev/reventless-spec 3.0.0-alpha.133 → 3.0.0-alpha.134

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/AnsiStyle.res +0 -1
  4. package/src/LogPrefix.res +4 -13
  5. package/src/components/AutomationSlice.res +1 -5
  6. package/src/components/CapabilityManifest.res +2 -3
  7. package/src/components/DcbDecode.res +6 -2
  8. package/src/components/DcbScopeInference.res +16 -12
  9. package/src/components/DcbTag.res +74 -46
  10. package/src/components/DcbValidation.res +114 -89
  11. package/src/components/DisplayName.res +4 -2
  12. package/src/components/ExtensionPoint.res +0 -1
  13. package/src/components/FieldDefault.res +2 -4
  14. package/src/components/InboundTranslationSlice.res +0 -2
  15. package/src/components/OutboundTranslationSlice.res +10 -8
  16. package/src/components/Owner.res +4 -4
  17. package/src/components/Plugin.res +10 -4
  18. package/src/components/ReadModel.res +0 -1
  19. package/src/components/Reference.res +2 -8
  20. package/src/components/Sensitive.res +4 -4
  21. package/src/components/StateAnnotations.res +4 -2
  22. package/src/components/StateChangeSlice.res +0 -2
  23. package/src/components/StateViewSlice.res +0 -2
  24. package/src/components/TaggedUnion.res +1 -2
  25. package/src/components/Task.res +0 -1
  26. package/src/components/TraitCertificate.res +2 -2
  27. package/src/components/TraitManifest.res +1 -1
  28. package/src/generator/CertifyTrait.res +2 -4
  29. package/src/generator/Codegen.res +77 -68
  30. package/src/generator/Discovery.res +29 -12
  31. package/src/generator/GraftTrait.res +17 -9
  32. package/src/generator/Pairing.res +39 -13
  33. package/src/generator/PlatformCodegen.res +13 -11
  34. package/src/generator/PlatformManifests.res +1 -3
  35. package/src/generator/PluginGenerator.res +0 -1
  36. package/src/generator/TraitManifestCli.res +7 -7
  37. package/src/lifecycle/CheckLifecycleModel.res +173 -145
  38. package/src/lifecycle/CheckLifecycleModel.res.mjs +17 -2
  39. package/src/semantic/Bytes.res +0 -1
  40. package/src/semantic/CalendarDate.res +2 -2
  41. package/src/semantic/Capabilities.res +4 -6
  42. package/src/semantic/Capabilities.res.mjs +9 -11
  43. package/src/semantic/CapabilityNeed.res +4 -5
  44. package/src/semantic/CaptionedImage.res +0 -1
  45. package/src/semantic/Color.res +0 -1
  46. package/src/semantic/Currency.res +189 -170
  47. package/src/semantic/DateRange.res +3 -4
  48. package/src/semantic/DateTime.res +5 -5
  49. package/src/semantic/Duration.res +2 -2
  50. package/src/semantic/Email.res +0 -1
  51. package/src/semantic/FileRef.res +2 -2
  52. package/src/semantic/GeoPoint.res +17 -21
  53. package/src/semantic/Geocoding.res +8 -9
  54. package/src/semantic/Geolocation.res +6 -9
  55. package/src/semantic/ImageRef.res +2 -2
  56. package/src/semantic/MemberRef.res +0 -1
  57. package/src/semantic/Messaging.res +141 -21
  58. package/src/semantic/Messaging.res.mjs +51 -8
  59. package/src/semantic/Money.res +23 -20
  60. package/src/semantic/Offload.res +6 -4
  61. package/src/semantic/Percent.res +3 -3
  62. package/src/semantic/Phone.res +0 -1
  63. package/src/semantic/RowImage.res +4 -3
  64. package/src/semantic/Semantic.res +8 -11
  65. package/src/semantic/StorageRef.res +17 -17
  66. package/src/semantic/Template.res +5 -7
  67. package/src/semantic/UploadableFile.res +0 -1
  68. package/src/semantic/UploadableImage.res +0 -1
  69. package/src/semantic/Url.res +3 -3
  70. package/src/types/Authorization.res +1 -2
  71. package/src/types/Handler.res +0 -1
  72. package/src/types/Identity.res +1 -3
  73. package/src/types/Lifecycle.res +7 -6
  74. package/src/types/Message.res +6 -4
  75. package/src/types/OwnerScope.res +1 -3
  76. package/src/types/Projection.res +9 -2
  77. package/src/types/StoredEvent.res +2 -4
  78. package/src/types/Transition.res +8 -4
  79. package/src/util/Util_Sury.res +1 -4
@@ -11,23 +11,75 @@ A `(channel, address)` pair can be built wrong: `Sms` beside an email address
11
11
  compiles and fails at the provider. `recipient` fuses them, so the wrong pair
12
12
  does not exist, and the channel is read back off the value that carries it.
13
13
  */
14
+ /**
15
+ A delivery route. The selector a recipient chooses per notification kind.
14
16
 
15
- /** A delivery route. The selector a recipient chooses per notification kind, and
16
- the granularity a platform provisions at. */
17
+ Three arms, and push stays one of them even though it is provisioned three ways.
18
+ Splitting it would offer a person a choice between notification services, which is
19
+ not a choice anyone has: an app knows its own token, and nobody prefers APNs. The
20
+ service-level discrimination lives on the address and on what a provider
21
+ publishes — see `pushService`.
22
+ */
17
23
  type channel =
18
24
  | Email
19
25
  | Sms
20
26
  | Push
21
27
 
28
+ /**
29
+ How a push service names one install.
30
+
31
+ A variant rather than a string because the shapes differ: two services issue an
32
+ opaque token, and Web Push issues a subscription whose encryption keys are part of
33
+ the address. This applies the rule the file opens with one level down — the wrong
34
+ (service, address) pair stops being constructible for the same reason the wrong
35
+ (channel, address) pair already is.
36
+ */
37
+ type pushAddress =
38
+ | Apns({deviceToken: string})
39
+ | Fcm({registrationToken: string})
40
+ /** The endpoint is where it goes; the keys are how it is sealed. Requiring them
41
+ here keeps an unsendable subscription unconstructible. */
42
+ | WebPush({endpoint: string, p256dh: string, auth: string})
43
+
44
+ /**
45
+ Which service issued an address — the granularity push is provisioned at.
46
+
47
+ Email needs no such type: one sender provisioning reaches every mailbox, because
48
+ SMTP routes off the address. Push has no routing layer, so which service can reach
49
+ a device is fixed by which credential the deployment holds, and there is nothing
50
+ in the address to route on.
51
+ */
52
+ type pushService =
53
+ /** Apple Push Notification service. Provisioned with a signing key, the team id
54
+ that issued it and the app's bundle id. */
55
+ | ApnsService
56
+ /** Firebase Cloud Messaging, Google's. Provisioned with one service-account
57
+ credential. */
58
+ | FcmService
59
+ /** Standardised rather than vendor-run: the endpoint the browser hands out is
60
+ itself the service, so one VAPID keypair provisions all of them. */
61
+ | WebPushService
62
+
22
63
  /** 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. */
64
+ Email and SMS carry the branded scalar for their channel, so an unparseable one
65
+ is refused where it is built rather than by the provider; push carries the
66
+ variant its services issue, and the same property holds by construction. */
25
67
  type recipient =
26
68
  | ToEmail(Email.t)
27
69
  | 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})
70
+ /** How one install is reached. Not a bare string: the two token services and
71
+ Web Push do not share a shape, and Web Push is not opaque. */
72
+ | ToPush(pushAddress)
73
+
74
+ /** The service that issued a push address. What a provider's published list is
75
+ checked against, so a token is never handed to the service that cannot use
76
+ it. */
77
+ let serviceOf = (address: pushAddress): pushService =>
78
+ switch address {
79
+ | Apns(_) => ApnsService
80
+ | Fcm(_) => FcmService
81
+ | WebPush(_) => WebPushService
82
+ }
31
83
 
32
84
  /** The channel a recipient is addressed on. */
33
85
  let channelOf = (recipient: recipient): channel =>
@@ -45,6 +97,15 @@ let channelToString = (channel: channel): string =>
45
97
  | Push => "Push"
46
98
  }
47
99
 
100
+ /** The service's name as its own documentation spells it, because that is the
101
+ word a support conversation about a missing notification is conducted with. */
102
+ let pushServiceToString = (service: pushService): string =>
103
+ switch service {
104
+ | ApnsService => "APNs"
105
+ | FcmService => "FCM"
106
+ | WebPushService => "Web Push"
107
+ }
108
+
48
109
  /**
49
110
  The `From:` header a deployment's email sender presents as: the bare address, or
50
111
  a display name in front of it.
@@ -87,20 +148,28 @@ type receipt = {ref: string}
87
148
  /**
88
149
  Why a send produced no receipt.
89
150
 
90
- Three constructors because the retry decision turns on the distinction, and
91
- getting it wrong is expensive in both directions: retrying a refused address
92
- burns the budget on an outcome that will not change, and abandoning a transient
93
- outage writes off a message that would have gone.
151
+ The retry decision turns on the distinction, and getting it wrong is expensive in
152
+ both directions: retrying a refused address burns the budget on an outcome that
153
+ will not change, and abandoning a transient outage writes off a message that would
154
+ have gone.
155
+
156
+ Push needs its own refusal because it is provisioned per service. Once a
157
+ deployment can provision *some* push, `UnsupportedChannel(Push)` would have to
158
+ mean both "no push at all" and "not that service", and the two read the same to
159
+ someone debugging a half-provisioned deployment.
94
160
  */
95
161
  type failure =
96
- | /** The provider could not be reached, or refused the call. Retry. */
97
- Unavailable(string)
98
- | /** This deployment provisions nothing for this channel. Do not retry — no
162
+ /** The provider could not be reached, or refused the call. Retry. */
163
+ | Unavailable(string)
164
+ /** This deployment provisions nothing for this channel. Do not retry — no
99
165
  number of attempts provisions one. */
100
- UnsupportedChannel(channel)
101
- | /** The provider answered and will not take this message: an address it
166
+ | UnsupportedChannel(channel)
167
+ /** This deployment provisions push, but not the service that issued this
168
+ address. Do not retry — the device is reachable only through its own. */
169
+ | UnsupportedPushService(pushService)
170
+ /** The provider answered and will not take this message: an address it
102
171
  rejects, a recipient it suppresses. Do not retry. */
103
- Refused(string)
172
+ | Refused(string)
104
173
 
105
174
  /** The retry rule, stated once. Everything that sweeps a failed send derives
106
175
  from it rather than re-reading the constructors. */
@@ -108,6 +177,7 @@ let retriable = (failure: failure): bool =>
108
177
  switch failure {
109
178
  | Unavailable(_) => true
110
179
  | UnsupportedChannel(_)
180
+ | UnsupportedPushService(_)
111
181
  | Refused(_) => false
112
182
  }
113
183
 
@@ -118,6 +188,8 @@ let failureReason = (failure: failure): string =>
118
188
  | Unavailable(reason) => reason
119
189
  | UnsupportedChannel(channel) =>
120
190
  `this deployment provisions no ${channel->channelToString} channel`
191
+ | UnsupportedPushService(service) =>
192
+ `this deployment provisions push, but not ${service->pushServiceToString}`
121
193
  | Refused(reason) => reason
122
194
  }
123
195
 
@@ -138,14 +210,62 @@ because discovering a channel by failing on it costs a real message.
138
210
 
139
211
  Empty means no channel at all — the shape `none` takes, and the one a deploy-time
140
212
  gate exists to catch before it ships.
213
+
214
+ `pushServices` is the second answer, at the granularity push is actually
215
+ provisioned at. Both are published because the two granularities differ: a
216
+ preference centre reads `channels` because a person picks a channel, and `supports`
217
+ reads `pushServices` because a token is only reachable through its own service.
218
+ Build one through `makeProvider`, which derives the first from the second.
141
219
  */
142
220
  type provider = {
143
221
  channels: array<channel>,
222
+ pushServices: array<pushService>,
144
223
  send: send,
145
224
  }
146
225
 
147
- /** Whether this provider can attempt a recipient's channel at all. The check a
148
- caller makes before spending a send, and the same rule the provider applies
149
- internally, so the two cannot disagree. */
226
+ /**
227
+ Build a provider whose two published answers cannot disagree.
228
+
229
+ `channels` is derived rather than given: `Push` appears exactly when a push service
230
+ is behind it, so no transport can publish a channel with nothing to reach it on.
231
+ The alternative is that invariant stated in a comment beside two independently
232
+ written fields, which is where invariants go to die.
233
+
234
+ `emailAndSms` carries the channels the channel itself settles reachability for. A
235
+ `Push` passed there is dropped — `pushServices` is the only thing that provisions
236
+ push — and the switch doing the dropping is exhaustive, so a fourth channel arrives
237
+ here as a compile error rather than as a silent omission.
238
+ */
239
+ let makeProvider = (
240
+ ~emailAndSms: array<channel>,
241
+ ~pushServices: array<pushService>,
242
+ ~send: send,
243
+ ): provider => {
244
+ channels: emailAndSms
245
+ ->Array.filter(channel =>
246
+ switch channel {
247
+ | Email
248
+ | Sms => true
249
+ | Push => false
250
+ }
251
+ )
252
+ ->Array.concat(pushServices->Array.length == 0 ? [] : [Push]),
253
+ pushServices,
254
+ send,
255
+ }
256
+
257
+ /**
258
+ Whether this provider can attempt this recipient at all. The check a caller makes
259
+ before spending a send, and the same rule the provider applies internally, so the
260
+ two cannot disagree.
261
+
262
+ Push is checked against the issuing service, not against the channel. Answering off
263
+ `channels` would return `true` for an APNs token on a deployment that provisioned
264
+ FCM only — and inferring a channel by failing on it is exactly what publishing the
265
+ list exists to avoid, since a failed send costs a real message.
266
+ */
150
267
  let supports = (provider: provider, ~recipient: recipient): bool =>
151
- provider.channels->Array.includes(recipient->channelOf)
268
+ switch recipient {
269
+ | ToPush(address) => provider.pushServices->Array.includes(address->serviceOf)
270
+ | recipient => provider.channels->Array.includes(recipient->channelOf)
271
+ }
@@ -1,6 +1,17 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
 
4
+ function serviceOf(address) {
5
+ switch (address.TAG) {
6
+ case "Apns" :
7
+ return "ApnsService";
8
+ case "Fcm" :
9
+ return "FcmService";
10
+ case "WebPush" :
11
+ return "WebPushService";
12
+ }
13
+ }
14
+
4
15
  function channelOf(recipient) {
5
16
  switch (recipient.TAG) {
6
17
  case "ToEmail" :
@@ -23,6 +34,17 @@ function channelToString(channel) {
23
34
  }
24
35
  }
25
36
 
37
+ function pushServiceToString(service) {
38
+ switch (service) {
39
+ case "ApnsService" :
40
+ return "APNs";
41
+ case "FcmService" :
42
+ return "FCM";
43
+ case "WebPushService" :
44
+ return "Web Push";
45
+ }
46
+ }
47
+
26
48
  function fromHeader(displayName, address) {
27
49
  if (displayName === undefined) {
28
50
  return address;
@@ -32,35 +54,56 @@ function fromHeader(displayName, address) {
32
54
  }
33
55
 
34
56
  function retriable(failure) {
35
- switch (failure.TAG) {
36
- case "Unavailable" :
37
- return true;
38
- case "UnsupportedChannel" :
39
- case "Refused" :
40
- return false;
41
- }
57
+ return failure.TAG === "Unavailable";
42
58
  }
43
59
 
44
60
  function failureReason(failure) {
45
61
  switch (failure.TAG) {
46
62
  case "UnsupportedChannel" :
47
63
  return `this deployment provisions no ` + channelToString(failure._0) + ` channel`;
64
+ case "UnsupportedPushService" :
65
+ return `this deployment provisions push, but not ` + pushServiceToString(failure._0);
48
66
  case "Unavailable" :
49
67
  case "Refused" :
50
68
  return failure._0;
51
69
  }
52
70
  }
53
71
 
72
+ function makeProvider(emailAndSms, pushServices, send) {
73
+ return {
74
+ channels: emailAndSms.filter(channel => {
75
+ switch (channel) {
76
+ case "Email" :
77
+ case "Sms" :
78
+ return true;
79
+ case "Push" :
80
+ return false;
81
+ }
82
+ }).concat(pushServices.length === 0 ? [] : ["Push"]),
83
+ pushServices: pushServices,
84
+ send: send
85
+ };
86
+ }
87
+
54
88
  function supports(provider, recipient) {
55
- return provider.channels.includes(channelOf(recipient));
89
+ switch (recipient.TAG) {
90
+ case "ToEmail" :
91
+ case "ToSms" :
92
+ return provider.channels.includes(channelOf(recipient));
93
+ case "ToPush" :
94
+ return provider.pushServices.includes(serviceOf(recipient._0));
95
+ }
56
96
  }
57
97
 
58
98
  export {
99
+ serviceOf,
59
100
  channelOf,
60
101
  channelToString,
102
+ pushServiceToString,
61
103
  fromHeader,
62
104
  retriable,
63
105
  failureReason,
106
+ makeProvider,
64
107
  supports,
65
108
  }
66
109
  /* No side effect */
@@ -66,7 +66,6 @@ rewrites the wire shape, so stored events no longer decode and projections must
66
66
  be rebuilt. Retyping a field in a log that has to survive needs an upcaster
67
67
  first; a log that can be discarded can take it today.
68
68
  */
69
-
70
69
  /**
71
70
  Validate a minor-unit amount, saying why when it is not one.
72
71
 
@@ -79,8 +78,9 @@ let validateAmount = (amount: float): result<float, string> =>
79
78
  } else if amount !== Math.trunc(amount) {
80
79
  Error(
81
80
  `an amount is a whole number of a currency's minor units, got ` ++
82
- `${Float.toString(amount)}. There is no such thing as a fraction of the ` ++
83
- `smallest unit — a major amount converts with Money.ofMajor.`,
81
+ `${Float.toString(
82
+ amount,
83
+ )}. There is no such thing as a fraction of the ` ++ `smallest unit — a major amount converts with Money.ofMajor.`,
84
84
  )
85
85
  } else {
86
86
  Ok(amount)
@@ -96,14 +96,12 @@ let validateAmount = (amount: float): result<float, string> =>
96
96
  threw `Cannot access 'v0' before initialization` — it is simply not what this
97
97
  check wants.) */
98
98
  let amountSchema: S.t<float> =
99
- S.float->S.refine(
100
- amount =>
101
- switch validateAmount(amount) {
102
- | Ok(_) => true
103
- | Error(_) => false
104
- },
105
- ~error="expected a monetary amount",
106
- )
99
+ S.float->S.refine(amount =>
100
+ switch validateAmount(amount) {
101
+ | Ok(_) => true
102
+ | Error(_) => false
103
+ }
104
+ , ~error="expected a monetary amount")
107
105
 
108
106
  @schema
109
107
  type t = {
@@ -117,7 +115,8 @@ type t = {
117
115
 
118
116
  Shadows the schema sury-ppx derived from the type above: the derived one is
119
117
  the shape, and this adds the marker the shape cannot carry. */
120
- let schema: S.t<t> = schema->Semantic.mark(~id=Semantic.Id.money)
118
+ let schema: S.t<t> =
119
+ schema->Semantic.mark(~id=Semantic.Id.money)
121
120
 
122
121
  /** An amount already counted in minor units. */
123
122
  let make = (~amount: float, ~currency: Currency.t): t => {amount, currency}
@@ -208,8 +207,9 @@ let add = (a: t, b: t): result<t, string> =>
208
207
  a.currency == b.currency
209
208
  ? Ok({amount: a.amount +. b.amount, currency: a.currency})
210
209
  : Error(
211
- `cannot add ${format(b)} to ${format(a)}: they are different currencies. ` ++
212
- `Converting between them needs a rate, which is not something an amount carries.`,
210
+ `cannot add ${format(b)} to ${format(
211
+ a,
212
+ )}: they are different currencies. ` ++ `Converting between them needs a rate, which is not something an amount carries.`,
213
213
  )
214
214
 
215
215
  /** The largest whole number a `float` holds exactly, 2^53 - 1. Bound here
@@ -266,8 +266,9 @@ A negative amount splits away from zero the same way: -€10.00 into three is
266
266
  let allocate = (m: t, ~into: int): result<array<t>, string> =>
267
267
  if into <= 0 {
268
268
  Error(
269
- `cannot split ${format(m)} into ${Int.toString(into)} parts: ` ++
270
- `a split is into at least one part.`,
269
+ `cannot split ${format(m)} into ${Int.toString(
270
+ into,
271
+ )} parts: ` ++ `a split is into at least one part.`,
271
272
  )
272
273
  } else {
273
274
  let parts = Int.toFloat(into)
@@ -293,8 +294,10 @@ let allocate = (m: t, ~into: int): result<array<t>, string> =>
293
294
  let sum = (amounts: array<t>): option<result<t, string>> =>
294
295
  switch amounts {
295
296
  | [] => None
296
- | _ => Some(amounts->Array.reduce(Ok(zero(~currency=(amounts->Array.getUnsafe(0)).currency)), (
297
- acc,
298
- m,
299
- ) => acc->Result.flatMap(total => add(total, m))))
297
+ | _ =>
298
+ Some(
299
+ amounts->Array.reduce(Ok(zero(~currency=(amounts->Array.getUnsafe(0)).currency)), (acc, m) =>
300
+ acc->Result.flatMap(total => add(total, m))
301
+ ),
302
+ )
300
303
  }
@@ -45,7 +45,6 @@ structure: @s.matches(Offload.optionSchema(~store="pluginStructures", pluginStru
45
45
  option<Offload.payload<pluginStructure>>
46
46
  ```
47
47
  */
48
-
49
48
  /** The reference an offloaded value carries: which store holds it, the
50
49
  content-addressed key, the content hash (== the key's basis), and the byte
51
50
  length. `hash` is redundant with `key` today (`key` is `sha256/<hash>`) but
@@ -75,7 +74,7 @@ Parameterised by the inner value's schema because the `Inline` arm round-trips
75
74
  through it. The `Offloaded` arm round-trips through `offloadedRefSchema` under the
76
75
  sentinel key. See the module doc for why this is untagged.
77
76
  */
78
- // Both arms are *declared* (`S.object` / `S.shape`) rather than hand-written as
77
+ let // Both arms are *declared* (`S.object` / `S.shape`) rather than hand-written as
79
78
  // `S.transform` pairs, and that is load-bearing, not a style choice. A transform
80
79
  // arm is opaque: sury cannot see what it accepts, so it must offer every value to
81
80
  // the arm's own serializer and let it reject. Two things follow, both of which bit
@@ -89,7 +88,7 @@ sentinel key. See the module doc for why this is untagged.
89
88
  // A declared arm has neither problem: sury derives both directions from the shape,
90
89
  // discriminates on it, and never runs user code to find out. One transform arm is
91
90
  // enough to bring both failures back, so keep every arm declarative.
92
- let schema = (inner: S.t<'a>): S.t<payload<'a>> => {
91
+ schema = (inner: S.t<'a>): S.t<payload<'a>> => {
93
92
  // Offloaded arm: an object carrying the reserved sentinel key, tried first so an
94
93
  // offloaded value is never mistaken for an inline one.
95
94
  let offloadedArm = S.object(s => Offloaded(s.field(sentinelKey, offloadedRefSchema)))
@@ -115,7 +114,10 @@ let forStore = (
115
114
  ~threshold: option<int>=?,
116
115
  inner: S.t<'a>,
117
116
  ): S.t<payload<'a>> =>
118
- schema(inner)->Semantic.mark(~id=Semantic.Id.offload, ~payload=StoredIn({plugin, store, threshold}))
117
+ schema(inner)->Semantic.mark(
118
+ ~id=Semantic.Id.offload,
119
+ ~payload=StoredIn({plugin, store, threshold}),
120
+ )
119
121
 
120
122
  /**
121
123
  The codec wrapped for an **optional** field, plus the `StoredIn` marker. This is
@@ -13,7 +13,6 @@ fractions are allowed — `99.95` is a percentage.
13
13
  }
14
14
  ```
15
15
  */
16
-
17
16
  /** The percentage's representation. Transparent `float`: the marker refines an
18
17
  existing numeric field, so nothing stored changes. */
19
18
  type t = float
@@ -27,8 +26,9 @@ let fromFloat = (raw: float): result<t, string> =>
27
26
  Error(`a percentage must be a finite number, got ${Float.toString(raw)}`)
28
27
  } else if raw < 0.0 || raw > 100.0 {
29
28
  Error(
30
- `a percentage runs from 0 to 100, got ${Float.toString(raw)}. ` ++
31
- `This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`,
29
+ `a percentage runs from 0 to 100, got ${Float.toString(
30
+ raw,
31
+ )}. ` ++ `This scale is 0–100, not 0–1 — a fraction multiplies by 100 first.`,
32
32
  )
33
33
  } else {
34
34
  Ok(raw)
@@ -30,7 +30,6 @@ which is the E.164 limit. No spaces, no punctuation, no leading zero after the
30
30
  })
31
31
  ```
32
32
  */
33
-
34
33
  /** The number's representation. Transparent `string`; see `Email.t`. */
35
34
  type t = string
36
35
 
@@ -23,11 +23,12 @@ Declaration order rather than a name rule: a view with two image fields has
23
23
  already said which one comes first, and guessing from names would let a field
24
24
  called `thumbnail` outrank the one the author put at the top.
25
25
  */
26
-
27
26
  /** Where the ref string sits, relative to the field that carries it. */
28
27
  type shape =
29
- | /** The field's own value is the ref. */ Scalar
30
- | /** The ref is one member of the field's record. */ Member(string)
28
+ /** The field's own value is the ref. */
29
+ | Scalar
30
+ /** The ref is one member of the field's record. */
31
+ | Member(string)
31
32
 
32
33
  type source = {
33
34
  field: string,
@@ -9,7 +9,6 @@ semantic is a new value rather than a new branch.
9
9
  The payload is a typed variant because the vocabulary is framework-owned and
10
10
  closed — which also keeps `Reference.getTarget` total.
11
11
  */
12
-
13
12
  /** Which entity a reference field points to. */
14
13
  type referenceTarget = {entity: string, plugin: option<string>}
15
14
 
@@ -122,18 +121,16 @@ let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
122
121
 
123
122
  /** A schema that validates with `check` and carries the semantic `id`, derived
124
123
  from the constructor so no second grammar can drift from it. */
125
- // sury's refiner takes a fixed message, so `check`'s per-value reason is lost
124
+ let // sury's refiner takes a fixed message, so `check`'s per-value reason is lost
126
125
  // here; call the scalar's own `fromString`/`fromFloat` to report which rule broke.
127
- let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.t<'a> =>
126
+ refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.t<'a> =>
128
127
  base
129
- ->S.refine(
130
- value =>
131
- switch check(value) {
132
- | Ok(_) => true
133
- | Error(_) => false
134
- },
135
- ~error=`expected a valid ${id}`,
136
- )
128
+ ->S.refine(value =>
129
+ switch check(value) {
130
+ | Ok(_) => true
131
+ | Error(_) => false
132
+ }
133
+ , ~error=`expected a valid ${id}`)
137
134
  ->mark(~id)
138
135
 
139
136
  /** A value as it should read back to whoever typed it — rejection messages quote
@@ -50,7 +50,6 @@ wire format, or any stored value changing.
50
50
  })
51
51
  ```
52
52
  */
53
-
54
53
  /** The ref's representation. Transparent `string` on purpose: the marker refines
55
54
  an existing `string` field rather than replacing it, so the field's runtime
56
55
  representation — and therefore every stored event — is unchanged. A sealed
@@ -61,8 +60,7 @@ type t = string
61
60
  external unsafe: string => t = "%identity"
62
61
  external toString: t => string = "%identity"
63
62
 
64
- let segmentIsSafe = (segment: string) =>
65
- segment !== "" && segment !== "." && segment !== ".."
63
+ let segmentIsSafe = (segment: string) => segment !== "" && segment !== "." && segment !== ".."
66
64
 
67
65
  /**
68
66
  Validate a raw string as a storage ref, saying why when it is not one.
@@ -74,7 +72,9 @@ schema validation cannot drift apart.
74
72
  let fromString = (raw: string): result<t, string> =>
75
73
  if !String.startsWith(raw, "/") {
76
74
  Error(
77
- `expected an origin-relative storage ref starting with "/", got ${raw->JSON.Encode.string->JSON.stringify}. External URLs and data: URIs are not storage refs.`,
75
+ `expected an origin-relative storage ref starting with "/", got ${raw
76
+ ->JSON.Encode.string
77
+ ->JSON.stringify}. External URLs and data: URIs are not storage refs.`,
78
78
  )
79
79
  } else if String.startsWith(raw, "//") {
80
80
  Error(`protocol-relative refs are not storage refs: ${raw}`)
@@ -94,7 +94,7 @@ The refinement a ref-holding field validates against, carrying no semantic yet.
94
94
  The `Uploadable*` types re-label this rather than restating the grammar, so a
95
95
  re-labelling cannot validate differently from what it re-labels.
96
96
  */
97
- // The empty string is admitted as the "no object" sentinel. The fields this
97
+ let // The empty string is admitted as the "no object" sentinel. The fields this
98
98
  // marks are non-optional today, and a producer with nothing to reference —
99
99
  // a supplier feed carrying no image, say — already writes `""` to mean
100
100
  // absence. Rejecting it here would break a legitimate existing value and
@@ -107,16 +107,14 @@ re-labelling cannot validate differently from what it re-labels.
107
107
  // sury's refiner is a predicate with a fixed message, so the per-value reason
108
108
  // `fromString` returns is not threaded through; call `fromString` directly
109
109
  // when the caller needs to report which rule the value broke.
110
- let refinement: S.t<t> =
111
- S.string->S.refine(
112
- value =>
113
- value === "" ||
114
- switch fromString(value) {
115
- | Ok(_) => true
116
- | Error(_) => false
117
- },
118
- ~error=`expected an origin-relative storage ref: "/" followed by a prefix and an object path, or "" for no object`,
119
- )
110
+ refinement: S.t<t> =
111
+ S.string->S.refine(value =>
112
+ value === "" ||
113
+ switch fromString(value) {
114
+ | Ok(_) => true
115
+ | Error(_) => false
116
+ }
117
+ , ~error=`expected an origin-relative storage ref: "/" followed by a prefix and an object path, or "" for no object`)
120
118
 
121
119
  /**
122
120
  The sury schema for a field holding a ref into a named store.
@@ -139,8 +137,10 @@ let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
139
137
 
140
138
  /** How many refs one field holds. */
141
139
  type arity =
142
- | /** A `string` field: one ref. */ Single
143
- | /** An `array<string>` field: zero or more refs. */ Multiple
140
+ /** A `string` field: one ref. */
141
+ | Single
142
+ /** An `array<string>` field: zero or more refs. */
143
+ | Multiple
144
144
 
145
145
  /**
146
146
  The store a *field* declares, looking through an array wrapper, with the arity
@@ -6,7 +6,6 @@ Grammar: `{{ path }}`, `{{ path | formatter }}`, `{{# if path }}…{{/ if }}` an
6
6
  evaluated, an unresolved path renders a visible placeholder rather than throwing,
7
7
  and a field the schema marks `@sensitive` is withheld.
8
8
  */
9
-
10
9
  /** A path into the payload, in **wire** field names. `text` is what was written,
11
10
  so a placeholder can name it. */
12
11
  type path = {text: string, relative: bool, segments: array<string>}
@@ -54,8 +53,7 @@ let parsePath = (raw: string, ~insideEach: bool): result<path, string> => {
54
53
  segments->Array.every(segment => segmentGrammar->RegExp.test(segment))
55
54
  ? Ok({text, relative, segments})
56
55
  : Error(
57
- `"${text}" is not a path — segments are letters, digits and ` ++
58
- `underscores, separated by dots`,
56
+ `"${text}" is not a path — segments are letters, digits and ` ++ `underscores, separated by dots`,
59
57
  )
60
58
  }
61
59
  }
@@ -268,10 +266,10 @@ let isWithheld = (schema: option<S.t<unknown>>): bool =>
268
266
  | None => false
269
267
  | Some(field) =>
270
268
  Sensitive.isFieldSensitive(field) ||
271
- switch Semantic.getFrom(field) {
272
- | Some({id}) => Sensitive.impliedBySemantic(id)
273
- | None => false
274
- }
269
+ switch Semantic.getFrom(field) {
270
+ | Some({id}) => Sensitive.impliedBySemantic(id)
271
+ | None => false
272
+ }
275
273
  }
276
274
 
277
275
  let parseSafely = (json: JSON.t, schema: S.t<'a>): option<'a> =>
@@ -14,7 +14,6 @@ representation, no media-type restriction on what may be uploaded. See
14
14
  })
15
15
  ```
16
16
  */
17
-
18
17
  type t = string
19
18
 
20
19
  external unsafe: string => t = "%identity"
@@ -31,7 +31,6 @@ does, and only because the store is derived from it.
31
31
  })
32
32
  ```
33
33
  */
34
-
35
34
  /** Transparent `string`, for `StorageRef.t`'s reason: the marker refines an
36
35
  existing field rather than replacing it, so nothing stored changes. */
37
36
  type t = string