@reventlessdev/trait-attachments 1.0.0-alpha.5 → 1.0.0-alpha.51
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/README.md +6 -4
- package/package.json +7 -7
- package/src/Attachments.res +46 -21
- package/src/Attachments_Conformance.res +59 -23
- package/src/Attachments_Conformance.res.mjs +57 -28
- package/src/Attachments_Rules.res +64 -21
- package/src/Attachments_Rules.res.mjs +22 -1
- package/src/Attachments_Scaffold.res +333 -263
- package/src/Attachments_Scaffold.res.mjs +72 -35
|
@@ -25,7 +25,6 @@ is how a scaffolder acquires a policy language nobody asked for. They are left
|
|
|
25
25
|
as `TODO(graft)` markers, and the developer writes ReScript, which is better at
|
|
26
26
|
this than any config could be.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
28
|
/**
|
|
30
29
|
The names a graft needs, and nothing else.
|
|
31
30
|
|
|
@@ -39,6 +38,9 @@ type config = {
|
|
|
39
38
|
entity: string,
|
|
40
39
|
/** Its id field: `"productId"`. */
|
|
41
40
|
entityId: string,
|
|
41
|
+
/** The host's identity module for that id: `"ProductId"`. Absent ⇒ `string`,
|
|
42
|
+
for a host that has not declared one. */
|
|
43
|
+
entityIdType?: string,
|
|
42
44
|
/**
|
|
43
45
|
What this host calls one attachment, capitalised and singular: `"Image"`,
|
|
44
46
|
`"Document"`, `"Attachment"`.
|
|
@@ -126,13 +128,13 @@ type names = {
|
|
|
126
128
|
attached: string,
|
|
127
129
|
removed: string,
|
|
128
130
|
primarySet: string,
|
|
131
|
+
effectiveChanged: string,
|
|
129
132
|
altTextSet: string,
|
|
130
133
|
notFound: string,
|
|
131
134
|
notAttached: string,
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
let cardinalityOf = (c: config): Attachments_Rules.cardinality =>
|
|
135
|
-
c.cardinality->Option.getOr(Many)
|
|
137
|
+
let cardinalityOf = (c: config): Attachments_Rules.cardinality => c.cardinality->Option.getOr(Many)
|
|
136
138
|
|
|
137
139
|
let isSingle = (c: config): bool => cardinalityOf(c) == Single
|
|
138
140
|
|
|
@@ -151,6 +153,9 @@ let namesOf = (c: config): names => {
|
|
|
151
153
|
attached: subject ++ "Attached",
|
|
152
154
|
removed: subject ++ "Removed",
|
|
153
155
|
primarySet: c.entity ++ "Primary" ++ c.noun ++ "Set",
|
|
156
|
+
// Named for what it states — the one that now stands — rather than for the
|
|
157
|
+
// command that moved it, because most of the time no command did.
|
|
158
|
+
effectiveChanged: c.entity ++ "Effective" ++ c.noun ++ "Changed",
|
|
154
159
|
altTextSet: subject ++ "AltTextSet",
|
|
155
160
|
notFound: c.entity ++ "NotFound",
|
|
156
161
|
notAttached: subject ++ "NotAttached",
|
|
@@ -159,6 +164,15 @@ let namesOf = (c: config): names => {
|
|
|
159
164
|
|
|
160
165
|
let refTypeOf = (c: config) => c.refType->Option.getOr("Reventless.UploadableImage.t")
|
|
161
166
|
|
|
167
|
+
// The entity id's type, and a fixture value of it.
|
|
168
|
+
let idTypeOf = (c: config) => c.entityIdType->Option.mapOr("string", t => t ++ ".t")
|
|
169
|
+
|
|
170
|
+
let idValueOf = (c: config, literal: string) =>
|
|
171
|
+
switch c.entityIdType {
|
|
172
|
+
| Some(t) => `${t}.make("${literal}")`
|
|
173
|
+
| None => `"${literal}"`
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
// What the VIEW holds, as against what an event's reference field holds. An
|
|
163
177
|
// event names a file; a view holds that file together with the text that goes
|
|
164
178
|
// with it, because a cell renderer is handed a field and a value and never the
|
|
@@ -302,217 +316,236 @@ let sliceSpec = (c: config): string => {
|
|
|
302
316
|
let attrs = commandAttributes(c)
|
|
303
317
|
let createdArm =
|
|
304
318
|
c.createdCarriesEntityId->Option.getOr(true)
|
|
305
|
-
? ` | ${c.created}({ ${c.entityId}:
|
|
319
|
+
? ` | ${c.created}({ ${c.entityId}: ${idTypeOf(c)}})`
|
|
306
320
|
: ` | ${c.created}`
|
|
307
321
|
// Present only where a choice exists. A bounded set has one member, so
|
|
308
322
|
// nothing consumes or emits a primary — and the trait's rules never produce
|
|
309
323
|
// the fact, so listing the event would declare one nothing can write.
|
|
310
324
|
let primaryArm = (prefix: string) =>
|
|
311
325
|
single ? [] : [` | ${n.primarySet}({ ${prefix}${c.file}: ${ref}})`]
|
|
312
|
-
let headline =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
326
|
+
let headline = single
|
|
327
|
+
? [
|
|
328
|
+
`// ${n.slice} StateChangeSlice: ${c.entity}'s single attachment — set it, remove`,
|
|
329
|
+
`// it, caption it. A graft of the Attachments trait; the set's rules are the`,
|
|
330
|
+
]
|
|
331
|
+
: [
|
|
332
|
+
`// ${n.slice} StateChangeSlice: ${c.entity}'s attachment set — attach, remove,`,
|
|
333
|
+
`// choose the primary, caption. A graft of the Attachments trait; the rules are`,
|
|
334
|
+
]
|
|
335
|
+
lines(
|
|
336
|
+
[
|
|
337
|
+
...headline,
|
|
338
|
+
`// trait's and are asserted by its conformance suite, bound in the tests.`,
|
|
339
|
+
`//`,
|
|
340
|
+
`// Emitted by the trait. Everything below is this host's own vocabulary, so it is`,
|
|
341
|
+
`// ordinary source from here on — edit it freely.`,
|
|
342
|
+
``,
|
|
343
|
+
`@@reventless.spec`,
|
|
344
|
+
``,
|
|
345
|
+
`@schema`,
|
|
346
|
+
`type consumedEvent =`,
|
|
347
|
+
createdArm,
|
|
348
|
+
` | ${n.attached}({ ${c.file}: ${ref}})`,
|
|
349
|
+
` | ${n.removed}({ ${c.file}: ${ref}})`,
|
|
350
|
+
...primaryArm(""),
|
|
351
|
+
` | ${n.altTextSet}({ ${c.file}: ${ref}, altText: string})`,
|
|
352
|
+
` | ${n.effectiveChanged}({ ${c.file}?: ${ref}})`,
|
|
353
|
+
` // TODO(graft): add the events this host's own refusal turns on — whatever`,
|
|
354
|
+
` // moves it into a state where attachments may not be changed.`,
|
|
355
|
+
``,
|
|
356
|
+
...single
|
|
357
|
+
? [
|
|
358
|
+
`// One reference field, on ${n.attachCmd}, and it accepts a new file — so`,
|
|
359
|
+
`// it is typed as the uploadable it is and a form binds an upload input to it.`,
|
|
360
|
+
`// Neither other command names a reference: with one attachment there is`,
|
|
361
|
+
`// nothing to choose between, and asking a caller to name it would be asking`,
|
|
362
|
+
`// them to repeat what the row already says.`,
|
|
363
|
+
]
|
|
364
|
+
: [
|
|
365
|
+
`// The reference fields divide into two kinds, and the division is the point.`,
|
|
366
|
+
`// The one on ${n.attachCmd} accepts a new file, so it is typed as the`,
|
|
367
|
+
`// uploadable it is and a form binds an upload input to it. The others name a`,
|
|
368
|
+
`// file the row ALREADY holds, so they are typed as selections out of`,
|
|
369
|
+
`// \`${setFieldOf(c)}\` and a form offers those instead of an uploader.`,
|
|
370
|
+
`//`,
|
|
371
|
+
`// Bound once rather than spelled three times: the collection is one answer,`,
|
|
372
|
+
`// and three copies are three chances for one to name a field that has moved.`,
|
|
373
|
+
`let ${selectionBinding} = Reventless.MemberRef.of_(~view="${c.view}", ${contentArgOf(
|
|
374
|
+
c,
|
|
375
|
+
)}~field="${setFieldOf(c)}")`,
|
|
376
|
+
``,
|
|
377
|
+
],
|
|
378
|
+
`@schema`,
|
|
379
|
+
`type command =`,
|
|
380
|
+
`${attrs}${n.attachCmd}({ ${c.entityId}: ${idTypeOf(
|
|
381
|
+
c,
|
|
382
|
+
)}, ${c.file}: ${ref}, altText?: string})`,
|
|
383
|
+
// The bounded set's remove names nothing. This is the reported defect in its
|
|
384
|
+
// purest form — the old command asked for an upload in order to delete.
|
|
385
|
+
single
|
|
386
|
+
? `${attrs}${n.removeCmd}({ ${c.entityId}: ${idTypeOf(c)}})`
|
|
387
|
+
: `${attrs}${n.removeCmd}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}: ${sel}})`,
|
|
388
|
+
...single
|
|
389
|
+
? []
|
|
390
|
+
: [`${attrs}${n.setPrimaryCmd}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}: ${sel}})`],
|
|
391
|
+
single
|
|
392
|
+
? `${attrs}${n.setAltTextCmd}({ ${c.entityId}: ${idTypeOf(c)}, altText: string})`
|
|
393
|
+
: `${attrs}${n.setAltTextCmd}({ ${c.entityId}: ${idTypeOf(
|
|
359
394
|
c,
|
|
360
|
-
)}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
` | ${n.altTextSet}({ ${c.entityId}: string, ${c.file}: ${ref}, altText: string})`,
|
|
388
|
-
``,
|
|
389
|
-
...commandTransitionBinding(c),
|
|
390
|
-
`// The graft's own record of itself. Nothing else survives into a deployed`,
|
|
391
|
-
`// plugin — the dependency and the rules alias are source-side — so without`,
|
|
392
|
-
`// this a running estate cannot say where this slice came from.`,
|
|
393
|
-
`let traits = [TraitAttachments.Attachments.declaration]`,
|
|
394
|
-
``,
|
|
395
|
-
])
|
|
395
|
+
)}, ${c.file}: ${sel}, altText: string})`,
|
|
396
|
+
``,
|
|
397
|
+
`@schema`,
|
|
398
|
+
`type error =`,
|
|
399
|
+
` | ${n.notFound}`,
|
|
400
|
+
` | ${n.notAttached}`,
|
|
401
|
+
` // TODO(graft): add this host's own refusal.`,
|
|
402
|
+
``,
|
|
403
|
+
`@schema`,
|
|
404
|
+
`type event =`,
|
|
405
|
+
` | ${n.attached}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}: ${ref}, altText?: string})`,
|
|
406
|
+
` | ${n.removed}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}: ${ref}})`,
|
|
407
|
+
...primaryArm(`${c.entityId}: ${idTypeOf(c)}, `),
|
|
408
|
+
` | ${n.altTextSet}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}: ${ref}, altText: string})`,
|
|
409
|
+
` // The member a reader should now show, or none. A conclusion rather than a`,
|
|
410
|
+
` // decision: most of the moves that change it — a first attachment, a removal`,
|
|
411
|
+
` // promoting the next — are nobody's choice, so nothing else announces them.`,
|
|
412
|
+
` | ${n.effectiveChanged}({ ${c.entityId}: ${idTypeOf(c)}, ${c.file}?: ${ref}})`,
|
|
413
|
+
``,
|
|
414
|
+
...commandTransitionBinding(c),
|
|
415
|
+
`// The graft's own record of itself. Nothing else survives into a deployed`,
|
|
416
|
+
`// plugin — the dependency and the rules alias are source-side — so without`,
|
|
417
|
+
`// this a running estate cannot say where this slice came from.`,
|
|
418
|
+
`let traits = [TraitAttachments.Attachments.declaration]`,
|
|
419
|
+
``,
|
|
420
|
+
],
|
|
421
|
+
)
|
|
396
422
|
}
|
|
397
423
|
|
|
398
424
|
// ── The slice body ───────────────────────────────────────────────────────────
|
|
399
425
|
|
|
400
|
-
let
|
|
426
|
+
let sliceDecision = (c: config): string => {
|
|
401
427
|
let n = namesOf(c)
|
|
402
428
|
let single = isSingle(c)
|
|
403
|
-
lines(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
? []
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
`
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
429
|
+
lines(
|
|
430
|
+
[
|
|
431
|
+
`@@reventless.decision`,
|
|
432
|
+
``,
|
|
433
|
+
`// The set's rules are the trait's. What is left here is this host's own refusal`,
|
|
434
|
+
`// and the mapping between its constructors and the trait's ops and facts.`,
|
|
435
|
+
`module Attachments = TraitAttachments.Attachments_Rules`,
|
|
436
|
+
``,
|
|
437
|
+
`type state = {exists: bool, attachments: Attachments.t}`,
|
|
438
|
+
``,
|
|
439
|
+
`let initialState = {exists: false, attachments: Attachments.empty}`,
|
|
440
|
+
``,
|
|
441
|
+
`let evolve = (state, event) => {`,
|
|
442
|
+
` let fold = fact => {...state, attachments: state.attachments->Attachments.evolve(fact)}`,
|
|
443
|
+
` switch event {`,
|
|
444
|
+
// A creation event that carries no id is a bare constructor, so a wildcard
|
|
445
|
+
// payload does not compile against it. The spec above already branches on
|
|
446
|
+
// this; the fold has to branch with it.
|
|
447
|
+
c.createdCarriesEntityId->Option.getOr(true)
|
|
448
|
+
? ` | ${c.created}(_) => {...state, exists: true}`
|
|
449
|
+
: ` | ${c.created} => {...state, exists: true}`,
|
|
450
|
+
` | ${n.attached}({${c.file}}) => fold(Attached({ref: ${c.file}, altText: None}))`,
|
|
451
|
+
` | ${n.removed}({${c.file}}) => fold(Removed({ref: ${c.file}}))`,
|
|
452
|
+
...single ? [] : [` | ${n.primarySet}({${c.file}}) => fold(PrimarySet({ref: ${c.file}}))`],
|
|
453
|
+
` | ${n.altTextSet}({${c.file}, altText}) => fold(AltTextSet({ref: ${c.file}, altText}))`,
|
|
454
|
+
` | ${n.effectiveChanged}({${c.file}: ?ref}) => fold(EffectiveChanged({ref: ref}))`,
|
|
455
|
+
` // TODO(graft): fold this host's own events into its own state.`,
|
|
456
|
+
` }`,
|
|
457
|
+
`}`,
|
|
458
|
+
``,
|
|
459
|
+
`let toOp = command =>`,
|
|
460
|
+
` switch command {`,
|
|
461
|
+
` | ${n.attachCmd}({${c.entityId}, ${c.file}, altText: ?altText}) => (`,
|
|
462
|
+
` ${c.entityId},`,
|
|
463
|
+
` Attachments.Attach({ref: ${c.file}, altText}),`,
|
|
464
|
+
` )`,
|
|
465
|
+
// `Clear` is what a ref-less remove maps onto: the op that empties the set,
|
|
466
|
+
// whatever it holds. Nothing here has to look the member up.
|
|
467
|
+
...single
|
|
468
|
+
? [` | ${n.removeCmd}({${c.entityId}}) => (${c.entityId}, Attachments.Clear)`]
|
|
469
|
+
: [
|
|
470
|
+
` | ${n.removeCmd}({${c.entityId}, ${c.file}}) => (`,
|
|
471
|
+
` ${c.entityId},`,
|
|
472
|
+
` Attachments.Remove({ref: ${c.file}}),`,
|
|
473
|
+
` )`,
|
|
474
|
+
` | ${n.setPrimaryCmd}({${c.entityId}, ${c.file}}) => (`,
|
|
475
|
+
` ${c.entityId},`,
|
|
476
|
+
` Attachments.SetPrimary({ref: ${c.file}}),`,
|
|
477
|
+
` )`,
|
|
478
|
+
],
|
|
479
|
+
...single
|
|
480
|
+
? [
|
|
481
|
+
` | ${n.setAltTextCmd}({${c.entityId}, altText}) => (`,
|
|
482
|
+
` ${c.entityId},`,
|
|
483
|
+
// Not punned: a single-field inline record whose field shares its name
|
|
484
|
+
// with the variable filling it is read as a record copy, and the
|
|
485
|
+
// anonymous type then escapes its constructor.
|
|
486
|
+
` Attachments.SetPrimaryAltText({altText: altText}),`,
|
|
487
|
+
` )`,
|
|
488
|
+
]
|
|
489
|
+
: [
|
|
490
|
+
` | ${n.setAltTextCmd}({${c.entityId}, ${c.file}, altText}) => (`,
|
|
491
|
+
` ${c.entityId},`,
|
|
492
|
+
` Attachments.SetAltText({ref: ${c.file}, altText}),`,
|
|
493
|
+
` )`,
|
|
494
|
+
],
|
|
495
|
+
` }`,
|
|
496
|
+
``,
|
|
497
|
+
// `Some`/`None` for a bounded set only. `fact` is the trait's type and so
|
|
498
|
+
// lists a primary at both cardinalities, but a graft with no primary command
|
|
499
|
+
// can never decide one — and an arm that fabricated some other event to keep
|
|
500
|
+
// the switch total would be writing a fact nothing happened.
|
|
501
|
+
`let toEvent = (${c.entityId}, fact) =>`,
|
|
502
|
+
` switch fact {`,
|
|
503
|
+
` | Attachments.Attached({ref, altText}) =>`,
|
|
504
|
+
` ${single
|
|
505
|
+
? "Some("
|
|
506
|
+
: ""}${n.attached}({${c.entityId}, ${c.file}: ref, altText: ?altText})${single
|
|
507
|
+
? ")"
|
|
508
|
+
: ""}`,
|
|
509
|
+
` | Attachments.Removed({ref}) => ${single
|
|
510
|
+
? `Some(${n.removed}({${c.entityId}, ${c.file}: ref}))`
|
|
511
|
+
: `${n.removed}({${c.entityId}, ${c.file}: ref})`}`,
|
|
512
|
+
...single
|
|
513
|
+
? [
|
|
514
|
+
` // Unreachable: no command of this graft chooses a primary, because a set`,
|
|
515
|
+
` // of one has nothing to choose between. It contributes no event.`,
|
|
516
|
+
` | Attachments.PrimarySet(_) => None`,
|
|
517
|
+
]
|
|
518
|
+
: [` | Attachments.PrimarySet({ref}) => ${n.primarySet}({${c.entityId}, ${c.file}: ref})`],
|
|
519
|
+
` | Attachments.AltTextSet({ref, altText}) =>`,
|
|
520
|
+
` ${single ? "Some(" : ""}${n.altTextSet}({${c.entityId}, ${c.file}: ref, altText})${single
|
|
521
|
+
? ")"
|
|
522
|
+
: ""}`,
|
|
523
|
+
` | Attachments.EffectiveChanged({ref}) =>`,
|
|
524
|
+
` ${single ? "Some(" : ""}${n.effectiveChanged}({${c.entityId}, ${c.file}: ?ref})${single
|
|
525
|
+
? ")"
|
|
526
|
+
: ""}`,
|
|
527
|
+
` }`,
|
|
528
|
+
``,
|
|
529
|
+
`let decide = (state, command) =>`,
|
|
530
|
+
` if !state.exists {`,
|
|
531
|
+
` Error(${n.notFound})`,
|
|
532
|
+
` } else {`,
|
|
533
|
+
` // TODO(graft): this host's own refusal goes here, ahead of the set's rules —`,
|
|
534
|
+
` // an \`else if\` returning the error added above. A graft with no extra refusal`,
|
|
535
|
+
` // is a complete graft, so leaving this is legitimate.`,
|
|
536
|
+
` let (${c.entityId}, op) = toOp(command)`,
|
|
537
|
+
single
|
|
538
|
+
? ` switch state.attachments->Attachments.decide(~cardinality=Single, op) {`
|
|
539
|
+
: ` switch state.attachments->Attachments.decide(op) {`,
|
|
540
|
+
` | Error(#NotAttached) => Error(${n.notAttached})`,
|
|
541
|
+
single
|
|
542
|
+
? ` | Ok(facts) => Ok(facts->Array.filterMap(toEvent(${c.entityId}, _)))`
|
|
543
|
+
: ` | Ok(facts) => Ok(facts->Array.map(toEvent(${c.entityId}, _)))`,
|
|
544
|
+
` }`,
|
|
545
|
+
` }`,
|
|
546
|
+
``,
|
|
547
|
+
],
|
|
548
|
+
)
|
|
516
549
|
}
|
|
517
550
|
|
|
518
551
|
// ── The conformance binding ──────────────────────────────────────────────────
|
|
@@ -529,64 +562,101 @@ let conformanceBinding = (c: config): string => {
|
|
|
529
562
|
let refB = c.refB->Option.getOr(`/uploads/00000000-0000-4000-8000-000000000002/b`)
|
|
530
563
|
let createdValue =
|
|
531
564
|
c.createdCarriesEntityId->Option.getOr(true)
|
|
532
|
-
? `${c.created}({ ${c.entityId}:
|
|
565
|
+
? `${c.created}({ ${c.entityId}: ${idValueOf(c, id)}})`
|
|
533
566
|
: c.created
|
|
534
|
-
lines(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
:
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
:
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
567
|
+
lines(
|
|
568
|
+
[
|
|
569
|
+
`// The Attachments trait's conformance suite, bound to \`${n.slice}\`.`,
|
|
570
|
+
`// Emitted whole: every name here is one the graft already declared.`,
|
|
571
|
+
``,
|
|
572
|
+
`module Binding = {`,
|
|
573
|
+
` type ref = string`,
|
|
574
|
+
` let refA = "${refA}"`,
|
|
575
|
+
` let refB = "${refB}"`,
|
|
576
|
+
``,
|
|
577
|
+
` module Spec = ${n.slice}`,
|
|
578
|
+
` module Decision = ${n.slice}_Decision`,
|
|
579
|
+
``,
|
|
580
|
+
` // Annotated: the slice consumes and emits same-named constructors.`,
|
|
581
|
+
` // The same facts as the slice consumes them: no ${c.entityId}, because the`,
|
|
582
|
+
` // partition already says which entity they belong to.`,
|
|
583
|
+
` module Consumed = {`,
|
|
584
|
+
` let created: array<${n.slice}.consumedEvent> = [${createdValue}]`,
|
|
585
|
+
` let attached = (ref): ${n.slice}.consumedEvent => ${n.attached}({ ${c.file}: ref})`,
|
|
586
|
+
` let removed = (ref): ${n.slice}.consumedEvent => ${n.removed}({ ${c.file}: ref})`,
|
|
587
|
+
...single
|
|
588
|
+
? []
|
|
589
|
+
: [
|
|
590
|
+
` let primarySet = (ref): ${n.slice}.consumedEvent => ${n.primarySet}({ ${c.file}: ref})`,
|
|
591
|
+
],
|
|
592
|
+
` let altTextSet = (ref, altText): ${n.slice}.consumedEvent =>`,
|
|
593
|
+
` ${n.altTextSet}({ ${c.file}: ref, altText})`,
|
|
594
|
+
` let effectiveChanged = (ref): ${n.slice}.consumedEvent =>`,
|
|
595
|
+
` ${n.effectiveChanged}({ ${c.file}: ?ref})`,
|
|
596
|
+
` }`,
|
|
597
|
+
``,
|
|
598
|
+
` let attach = ref => ${n.slice}.${n.attachCmd}({ ${c.entityId}: ${idValueOf(
|
|
599
|
+
c,
|
|
600
|
+
id,
|
|
601
|
+
)}, ${c.file}: ref})`,
|
|
602
|
+
...single
|
|
603
|
+
? [` let clear = ${n.slice}.${n.removeCmd}({ ${c.entityId}: ${idValueOf(c, id)}})`]
|
|
604
|
+
: [
|
|
605
|
+
` let remove = ref => ${n.slice}.${n.removeCmd}({ ${c.entityId}: ${idValueOf(
|
|
606
|
+
c,
|
|
607
|
+
id,
|
|
608
|
+
)}, ${c.file}: ref})`,
|
|
609
|
+
` let setPrimary = ref =>`,
|
|
610
|
+
` ${n.slice}.${n.setPrimaryCmd}({ ${c.entityId}: ${idValueOf(
|
|
611
|
+
c,
|
|
612
|
+
id,
|
|
613
|
+
)}, ${c.file}: ref})`,
|
|
614
|
+
],
|
|
615
|
+
single
|
|
616
|
+
? ` let setAltText = altText => ${n.slice}.${n.setAltTextCmd}({ ${c.entityId}: ${idValueOf(
|
|
617
|
+
c,
|
|
618
|
+
id,
|
|
619
|
+
)}, altText})`
|
|
620
|
+
: ` let setAltText = (ref, altText) =>\n ${n.slice}.${n.setAltTextCmd}({ ${c.entityId}: ${idValueOf(
|
|
621
|
+
c,
|
|
622
|
+
id,
|
|
623
|
+
)}, ${c.file}: ref, altText})`,
|
|
624
|
+
``,
|
|
625
|
+
` let attached = ref => ${n.slice}.${n.attached}({ ${c.entityId}: ${idValueOf(
|
|
626
|
+
c,
|
|
627
|
+
id,
|
|
628
|
+
)}, ${c.file}: ref})`,
|
|
629
|
+
` let removed = ref => ${n.slice}.${n.removed}({ ${c.entityId}: ${idValueOf(
|
|
630
|
+
c,
|
|
631
|
+
id,
|
|
632
|
+
)}, ${c.file}: ref})`,
|
|
633
|
+
...single
|
|
634
|
+
? []
|
|
635
|
+
: [
|
|
636
|
+
` let primarySet = ref =>`,
|
|
637
|
+
` ${n.slice}.${n.primarySet}({ ${c.entityId}: ${idValueOf(c, id)}, ${c.file}: ref})`,
|
|
638
|
+
],
|
|
639
|
+
` let altTextSet = (ref, altText) =>`,
|
|
640
|
+
` ${n.slice}.${n.altTextSet}({ ${c.entityId}: ${idValueOf(
|
|
641
|
+
c,
|
|
642
|
+
id,
|
|
643
|
+
)}, ${c.file}: ref, altText})`,
|
|
644
|
+
` let effectiveChanged = ref =>`,
|
|
645
|
+
` ${n.slice}.${n.effectiveChanged}({ ${c.entityId}: ${idValueOf(
|
|
646
|
+
c,
|
|
647
|
+
id,
|
|
648
|
+
)}, ${c.file}: ?ref})`,
|
|
649
|
+
` let notAttached = ${n.slice}.${n.notAttached}`,
|
|
650
|
+
`}`,
|
|
651
|
+
``,
|
|
652
|
+
single
|
|
653
|
+
? `module Conformance = TraitAttachments.Attachments_Conformance.MakeSingle(Binding)`
|
|
654
|
+
: `module Conformance = TraitAttachments.Attachments_Conformance.Make(Binding)`,
|
|
655
|
+
``,
|
|
656
|
+
`Conformance.register()`,
|
|
657
|
+
``,
|
|
658
|
+
],
|
|
659
|
+
)
|
|
590
660
|
}
|
|
591
661
|
|
|
592
662
|
// ── The projection patch ─────────────────────────────────────────────────────
|
|
@@ -607,7 +677,7 @@ let conformanceBinding = (c: config): string => {
|
|
|
607
677
|
let singleProjectionPatch = (c: config): patch => {
|
|
608
678
|
let n = namesOf(c)
|
|
609
679
|
{
|
|
610
|
-
into: `
|
|
680
|
+
into: `StateViewStream/${c.view}_Projection.res`,
|
|
611
681
|
at: `the projection's \`switch\`, and one field on \`${c.view}\`'s state`,
|
|
612
682
|
contents: lines([
|
|
613
683
|
`// On the view's state, one field — the reference and its text, in one value.`,
|
|
@@ -647,7 +717,7 @@ let manyProjectionPatch = (c: config): patch => {
|
|
|
647
717
|
let n = namesOf(c)
|
|
648
718
|
let set = setFieldOf(c)
|
|
649
719
|
{
|
|
650
|
-
into: `
|
|
720
|
+
into: `StateViewStream/${c.view}_Projection.res`,
|
|
651
721
|
at: `the projection's \`switch\`, and one field on \`${c.view}\`'s state`,
|
|
652
722
|
contents: lines([
|
|
653
723
|
`// On the view's state, one field — the set, primary first.`,
|
|
@@ -716,10 +786,10 @@ let emit = (~config: config, ~into: string, ~tests: string): output => {
|
|
|
716
786
|
let n = namesOf(config)
|
|
717
787
|
{
|
|
718
788
|
files: [
|
|
719
|
-
{path: `${into}/
|
|
789
|
+
{path: `${into}/StateChange/${n.slice}.res`, contents: sliceSpec(config)},
|
|
720
790
|
{
|
|
721
|
-
path: `${into}/
|
|
722
|
-
contents:
|
|
791
|
+
path: `${into}/StateChange/${n.slice}_Decision.res`,
|
|
792
|
+
contents: sliceDecision(config),
|
|
723
793
|
},
|
|
724
794
|
{
|
|
725
795
|
path: `${tests}/${n.slice}Conformance_GWT.res`,
|