@reventlessdev/trait-attachments 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +5 -3
- package/package.json +7 -7
- package/src/Attachments.res +83 -8
- package/src/Attachments_Conformance.res +131 -12
- package/src/Attachments_Conformance.res.mjs +79 -10
- package/src/Attachments_Rules.res +167 -17
- package/src/Attachments_Rules.res.mjs +113 -36
- package/src/Attachments_Scaffold.res +537 -190
- package/src/Attachments_Scaffold.res.mjs +412 -162
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Attachments_Rules$TraitAttachments from "./Attachments_Rules.res.mjs";
|
|
6
7
|
|
|
7
8
|
let configSchema = Sury.$schema(s => ({
|
|
8
9
|
entity: s.m(Sury.string),
|
|
9
10
|
entityId: s.m(Sury.string),
|
|
11
|
+
entityIdType: s.m(Sury.$option(Sury.string)),
|
|
10
12
|
noun: s.m(Sury.string),
|
|
11
13
|
file: s.m(Sury.string),
|
|
14
|
+
cardinality: s.m(Sury.$option(Attachments_Rules$TraitAttachments.cardinalitySchema)),
|
|
12
15
|
created: s.m(Sury.string),
|
|
13
|
-
createdCarriesEntityId: s.m(Sury.$option(Sury.
|
|
16
|
+
createdCarriesEntityId: s.m(Sury.$option(Sury.boolean)),
|
|
14
17
|
view: s.m(Sury.string),
|
|
18
|
+
lifecycleType: s.m(Sury.$option(Sury.string)),
|
|
15
19
|
refType: s.m(Sury.$option(Sury.string)),
|
|
16
20
|
authorize: s.m(Sury.$option(Sury.string)),
|
|
17
21
|
transition: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
@@ -19,17 +23,28 @@ let configSchema = Sury.$schema(s => ({
|
|
|
19
23
|
refB: s.m(Sury.$option(Sury.string))
|
|
20
24
|
}));
|
|
21
25
|
|
|
26
|
+
function cardinalityOf(c) {
|
|
27
|
+
return Stdlib_Option.getOr(c.cardinality, "Many");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isSingle(c) {
|
|
31
|
+
return Stdlib_Option.getOr(c.cardinality, "Many") === "Single";
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
function namesOf(c) {
|
|
23
35
|
let subject = c.entity + c.noun;
|
|
24
36
|
return {
|
|
25
37
|
slice: subject + "s",
|
|
26
|
-
attachCmd:
|
|
38
|
+
attachCmd: (
|
|
39
|
+
Stdlib_Option.getOr(c.cardinality, "Many") === "Single" ? "Set" : "Attach"
|
|
40
|
+
) + subject,
|
|
27
41
|
removeCmd: "Remove" + subject,
|
|
28
42
|
setPrimaryCmd: "SetPrimary" + subject,
|
|
29
43
|
setAltTextCmd: "Set" + subject + "AltText",
|
|
30
44
|
attached: subject + "Attached",
|
|
31
45
|
removed: subject + "Removed",
|
|
32
46
|
primarySet: c.entity + "Primary" + c.noun + "Set",
|
|
47
|
+
effectiveChanged: c.entity + "Effective" + c.noun + "Changed",
|
|
33
48
|
altTextSet: subject + "AltTextSet",
|
|
34
49
|
notFound: c.entity + "NotFound",
|
|
35
50
|
notAttached: subject + "NotAttached"
|
|
@@ -40,6 +55,48 @@ function refTypeOf(c) {
|
|
|
40
55
|
return Stdlib_Option.getOr(c.refType, "Reventless.UploadableImage.t");
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
function idTypeOf(c) {
|
|
59
|
+
return Stdlib_Option.mapOr(c.entityIdType, "string", t => t + ".t");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function idValueOf(c, literal) {
|
|
63
|
+
let t = c.entityIdType;
|
|
64
|
+
if (t !== undefined) {
|
|
65
|
+
return t + `.make("` + literal + `")`;
|
|
66
|
+
} else {
|
|
67
|
+
return `"` + literal + `"`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function viewTypeOf(c) {
|
|
72
|
+
if (Stdlib_Option.getOr(c.refType, "Reventless.UploadableImage.t").includes("Image")) {
|
|
73
|
+
return "Reventless.CaptionedImage.t";
|
|
74
|
+
} else {
|
|
75
|
+
return Stdlib_Option.getOr(c.refType, "Reventless.UploadableImage.t");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setFieldOf(c) {
|
|
80
|
+
return c.file + "s";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let selectionBinding = "selected";
|
|
84
|
+
|
|
85
|
+
function selectionTypeOf(_c) {
|
|
86
|
+
return `@s.matches(` + selectionBinding + `) string`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function contentArgOf(c) {
|
|
90
|
+
let ref = Stdlib_Option.getOr(c.refType, "Reventless.UploadableImage.t");
|
|
91
|
+
if (ref.includes("Image")) {
|
|
92
|
+
return `~content=Reventless.Semantic.Id.imageRef, `;
|
|
93
|
+
} else if (ref.includes("File")) {
|
|
94
|
+
return `~content=Reventless.Semantic.Id.fileRef, `;
|
|
95
|
+
} else {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
43
100
|
function commandAttributes(c) {
|
|
44
101
|
let a = c.authorize;
|
|
45
102
|
if (a !== undefined) {
|
|
@@ -49,18 +106,32 @@ function commandAttributes(c) {
|
|
|
49
106
|
}
|
|
50
107
|
}
|
|
51
108
|
|
|
52
|
-
function
|
|
109
|
+
function commandNames(c) {
|
|
53
110
|
let n = namesOf(c);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
111
|
+
if (Stdlib_Option.getOr(c.cardinality, "Many") === "Single") {
|
|
112
|
+
return [
|
|
113
|
+
n.attachCmd,
|
|
114
|
+
n.removeCmd,
|
|
115
|
+
n.setAltTextCmd
|
|
116
|
+
];
|
|
117
|
+
} else {
|
|
118
|
+
return [
|
|
119
|
+
n.attachCmd,
|
|
120
|
+
n.removeCmd,
|
|
121
|
+
n.setPrimaryCmd,
|
|
122
|
+
n.setAltTextCmd
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function commandTransitionBinding(c) {
|
|
128
|
+
let arms = commandNames(c).map(cmd => ` | ` + cmd + `(_)`).join("\n");
|
|
129
|
+
let t = c.lifecycleType;
|
|
130
|
+
let lifecycleType = t !== undefined ? c.view + `.` + t : c.view + `.<lifecycle> // TODO(graft): the enum's name`;
|
|
60
131
|
let states = c.transition;
|
|
61
132
|
if (states !== undefined) {
|
|
62
133
|
return [
|
|
63
|
-
`type lifecycleState = ` +
|
|
134
|
+
`type lifecycleState = ` + lifecycleType,
|
|
64
135
|
`let commandTransition = (command: command): Reventless.Transition.t<lifecycleState> => {`,
|
|
65
136
|
` open Reventless.Transition`,
|
|
66
137
|
` switch command {`,
|
|
@@ -96,13 +167,28 @@ function lines(ls) {
|
|
|
96
167
|
function sliceSpec(c) {
|
|
97
168
|
let n = namesOf(c);
|
|
98
169
|
let ref = Stdlib_Option.getOr(c.refType, "Reventless.UploadableImage.t");
|
|
170
|
+
let sel = selectionTypeOf(c);
|
|
171
|
+
let single = Stdlib_Option.getOr(c.cardinality, "Many") === "Single";
|
|
99
172
|
let attrs = commandAttributes(c);
|
|
100
|
-
let createdArm = Stdlib_Option.getOr(c.createdCarriesEntityId, true) ? ` | ` + c.created + `({ ` + c.entityId + `:
|
|
173
|
+
let createdArm = Stdlib_Option.getOr(c.createdCarriesEntityId, true) ? ` | ` + c.created + `({ ` + c.entityId + `: ` + idTypeOf(c) + `})` : ` | ` + c.created;
|
|
174
|
+
let primaryArm = prefix => {
|
|
175
|
+
if (single) {
|
|
176
|
+
return [];
|
|
177
|
+
} else {
|
|
178
|
+
return [` | ` + n.primarySet + `({ ` + prefix + c.file + `: ` + ref + `})`];
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
let headline = single ? [
|
|
182
|
+
`// ` + n.slice + ` StateChangeSlice: ` + c.entity + `'s single attachment — set it, remove`,
|
|
183
|
+
`// it, caption it. A graft of the Attachments trait; the set's rules are the`
|
|
184
|
+
] : [
|
|
185
|
+
`// ` + n.slice + ` StateChangeSlice: ` + c.entity + `'s attachment set — attach, remove,`,
|
|
186
|
+
`// choose the primary, caption. A graft of the Attachments trait; the rules are`
|
|
187
|
+
];
|
|
101
188
|
return Belt_Array.concatMany([
|
|
189
|
+
headline,
|
|
102
190
|
[
|
|
103
|
-
`//
|
|
104
|
-
`// choose the primary, caption. A graft of the Attachments trait; the set's rules`,
|
|
105
|
-
`// are the trait's and are asserted by its conformance suite, bound in the tests.`,
|
|
191
|
+
`// trait's and are asserted by its conformance suite, bound in the tests.`,
|
|
106
192
|
`//`,
|
|
107
193
|
`// Emitted by the trait. Everything below is this host's own vocabulary, so it is`,
|
|
108
194
|
`// ordinary source from here on — edit it freely.`,
|
|
@@ -112,19 +198,44 @@ function sliceSpec(c) {
|
|
|
112
198
|
`@schema`,
|
|
113
199
|
`type consumedEvent =`,
|
|
114
200
|
createdArm,
|
|
115
|
-
` | ` + n.attached + `({ ` + c.file + `:
|
|
116
|
-
` | ` + n.removed + `({ ` + c.file + `:
|
|
117
|
-
|
|
118
|
-
|
|
201
|
+
` | ` + n.attached + `({ ` + c.file + `: ` + ref + `})`,
|
|
202
|
+
` | ` + n.removed + `({ ` + c.file + `: ` + ref + `})`
|
|
203
|
+
],
|
|
204
|
+
primaryArm(""),
|
|
205
|
+
[
|
|
206
|
+
` | ` + n.altTextSet + `({ ` + c.file + `: ` + ref + `, altText: string})`,
|
|
207
|
+
` | ` + n.effectiveChanged + `({ ` + c.file + `?: ` + ref + `})`,
|
|
119
208
|
` // TODO(graft): add the events this host's own refusal turns on — whatever`,
|
|
120
209
|
` // moves it into a state where attachments may not be changed.`,
|
|
121
|
-
|
|
210
|
+
``
|
|
211
|
+
],
|
|
212
|
+
single ? [
|
|
213
|
+
`// One reference field, on ` + n.attachCmd + `, and it accepts a new file — so`,
|
|
214
|
+
`// it is typed as the uploadable it is and a form binds an upload input to it.`,
|
|
215
|
+
`// Neither other command names a reference: with one attachment there is`,
|
|
216
|
+
`// nothing to choose between, and asking a caller to name it would be asking`,
|
|
217
|
+
`// them to repeat what the row already says.`
|
|
218
|
+
] : [
|
|
219
|
+
`// The reference fields divide into two kinds, and the division is the point.`,
|
|
220
|
+
`// The one on ` + n.attachCmd + ` accepts a new file, so it is typed as the`,
|
|
221
|
+
`// uploadable it is and a form binds an upload input to it. The others name a`,
|
|
222
|
+
`// file the row ALREADY holds, so they are typed as selections out of`,
|
|
223
|
+
`// \`` + (c.file + "s") + `\` and a form offers those instead of an uploader.`,
|
|
224
|
+
`//`,
|
|
225
|
+
`// Bound once rather than spelled three times: the collection is one answer,`,
|
|
226
|
+
`// and three copies are three chances for one to name a field that has moved.`,
|
|
227
|
+
`let ` + selectionBinding + ` = Reventless.MemberRef.of_(~view="` + c.view + `", ` + contentArgOf(c) + `~field="` + (c.file + "s") + `")`,
|
|
228
|
+
``
|
|
229
|
+
],
|
|
230
|
+
[
|
|
122
231
|
`@schema`,
|
|
123
232
|
`type command =`,
|
|
124
|
-
attrs + n.attachCmd + `({ ` + c.entityId + `:
|
|
125
|
-
attrs + n.removeCmd + `({ ` + c.entityId + `:
|
|
126
|
-
|
|
127
|
-
|
|
233
|
+
attrs + n.attachCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + ref + `, altText?: string})`,
|
|
234
|
+
single ? attrs + n.removeCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `})` : attrs + n.removeCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + sel + `})`
|
|
235
|
+
],
|
|
236
|
+
single ? [] : [attrs + n.setPrimaryCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + sel + `})`],
|
|
237
|
+
[
|
|
238
|
+
single ? attrs + n.setAltTextCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, altText: string})` : attrs + n.setAltTextCmd + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + sel + `, altText: string})`,
|
|
128
239
|
``,
|
|
129
240
|
`@schema`,
|
|
130
241
|
`type error =`,
|
|
@@ -134,10 +245,16 @@ function sliceSpec(c) {
|
|
|
134
245
|
``,
|
|
135
246
|
`@schema`,
|
|
136
247
|
`type event =`,
|
|
137
|
-
` | ` + n.attached + `({ ` + c.entityId + `:
|
|
138
|
-
` | ` + n.removed + `({ ` + c.entityId + `:
|
|
139
|
-
|
|
140
|
-
|
|
248
|
+
` | ` + n.attached + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + ref + `, altText?: string})`,
|
|
249
|
+
` | ` + n.removed + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + ref + `})`
|
|
250
|
+
],
|
|
251
|
+
primaryArm(c.entityId + `: ` + idTypeOf(c) + `, `),
|
|
252
|
+
[
|
|
253
|
+
` | ` + n.altTextSet + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `: ` + ref + `, altText: string})`,
|
|
254
|
+
` // The member a reader should now show, or none. A conclusion rather than a`,
|
|
255
|
+
` // decision: most of the moves that change it — a first attachment, a removal`,
|
|
256
|
+
` // promoting the next — are nobody's choice, so nothing else announces them.`,
|
|
257
|
+
` | ` + n.effectiveChanged + `({ ` + c.entityId + `: ` + idTypeOf(c) + `, ` + c.file + `?: ` + ref + `})`,
|
|
141
258
|
``
|
|
142
259
|
],
|
|
143
260
|
commandTransitionBinding(c),
|
|
@@ -153,180 +270,301 @@ function sliceSpec(c) {
|
|
|
153
270
|
|
|
154
271
|
function sliceBehavior(c) {
|
|
155
272
|
let n = namesOf(c);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
` }
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
273
|
+
let single = Stdlib_Option.getOr(c.cardinality, "Many") === "Single";
|
|
274
|
+
return Belt_Array.concatMany([
|
|
275
|
+
[
|
|
276
|
+
`@@reventless.behavior`,
|
|
277
|
+
``,
|
|
278
|
+
`// The set's rules are the trait's. What is left here is this host's own refusal`,
|
|
279
|
+
`// and the mapping between its constructors and the trait's ops and facts.`,
|
|
280
|
+
`module Attachments = TraitAttachments.Attachments_Rules`,
|
|
281
|
+
``,
|
|
282
|
+
`type state = {exists: bool, attachments: Attachments.t}`,
|
|
283
|
+
``,
|
|
284
|
+
`let initialState = {exists: false, attachments: Attachments.empty}`,
|
|
285
|
+
``,
|
|
286
|
+
`let evolve = (state, event) => {`,
|
|
287
|
+
` let fold = fact => {...state, attachments: state.attachments->Attachments.evolve(fact)}`,
|
|
288
|
+
` switch event {`,
|
|
289
|
+
Stdlib_Option.getOr(c.createdCarriesEntityId, true) ? ` | ` + c.created + `(_) => {...state, exists: true}` : ` | ` + c.created + ` => {...state, exists: true}`,
|
|
290
|
+
` | ` + n.attached + `({` + c.file + `}) => fold(Attached({ref: ` + c.file + `, altText: None}))`,
|
|
291
|
+
` | ` + n.removed + `({` + c.file + `}) => fold(Removed({ref: ` + c.file + `}))`
|
|
292
|
+
],
|
|
293
|
+
single ? [] : [` | ` + n.primarySet + `({` + c.file + `}) => fold(PrimarySet({ref: ` + c.file + `}))`],
|
|
294
|
+
[
|
|
295
|
+
` | ` + n.altTextSet + `({` + c.file + `, altText}) => fold(AltTextSet({ref: ` + c.file + `, altText}))`,
|
|
296
|
+
` | ` + n.effectiveChanged + `({` + c.file + `: ?ref}) => fold(EffectiveChanged({ref: ref}))`,
|
|
297
|
+
` // TODO(graft): fold this host's own events into its own state.`,
|
|
298
|
+
` }`,
|
|
299
|
+
`}`,
|
|
300
|
+
``,
|
|
301
|
+
`let toOp = command =>`,
|
|
302
|
+
` switch command {`,
|
|
303
|
+
` | ` + n.attachCmd + `({` + c.entityId + `, ` + c.file + `, altText: ?altText}) => (`,
|
|
304
|
+
` ` + c.entityId + `,`,
|
|
305
|
+
` Attachments.Attach({ref: ` + c.file + `, altText}),`,
|
|
306
|
+
` )`
|
|
307
|
+
],
|
|
308
|
+
single ? [` | ` + n.removeCmd + `({` + c.entityId + `}) => (` + c.entityId + `, Attachments.Clear)`] : [
|
|
309
|
+
` | ` + n.removeCmd + `({` + c.entityId + `, ` + c.file + `}) => (`,
|
|
310
|
+
` ` + c.entityId + `,`,
|
|
311
|
+
` Attachments.Remove({ref: ` + c.file + `}),`,
|
|
312
|
+
` )`,
|
|
313
|
+
` | ` + n.setPrimaryCmd + `({` + c.entityId + `, ` + c.file + `}) => (`,
|
|
314
|
+
` ` + c.entityId + `,`,
|
|
315
|
+
` Attachments.SetPrimary({ref: ` + c.file + `}),`,
|
|
316
|
+
` )`
|
|
317
|
+
],
|
|
318
|
+
single ? [
|
|
319
|
+
` | ` + n.setAltTextCmd + `({` + c.entityId + `, altText}) => (`,
|
|
320
|
+
` ` + c.entityId + `,`,
|
|
321
|
+
` Attachments.SetPrimaryAltText({altText: altText}),`,
|
|
322
|
+
` )`
|
|
323
|
+
] : [
|
|
324
|
+
` | ` + n.setAltTextCmd + `({` + c.entityId + `, ` + c.file + `, altText}) => (`,
|
|
325
|
+
` ` + c.entityId + `,`,
|
|
326
|
+
` Attachments.SetAltText({ref: ` + c.file + `, altText}),`,
|
|
327
|
+
` )`
|
|
328
|
+
],
|
|
329
|
+
[
|
|
330
|
+
` }`,
|
|
331
|
+
``,
|
|
332
|
+
`let toEvent = (` + c.entityId + `, fact) =>`,
|
|
333
|
+
` switch fact {`,
|
|
334
|
+
` | Attachments.Attached({ref, altText}) =>`,
|
|
335
|
+
` ` + (
|
|
336
|
+
single ? "Some(" : ""
|
|
337
|
+
) + n.attached + `({` + c.entityId + `, ` + c.file + `: ref, altText: ?altText})` + (
|
|
338
|
+
single ? ")" : ""
|
|
339
|
+
),
|
|
340
|
+
` | Attachments.Removed({ref}) => ` + (
|
|
341
|
+
single ? `Some(` + n.removed + `({` + c.entityId + `, ` + c.file + `: ref}))` : n.removed + `({` + c.entityId + `, ` + c.file + `: ref})`
|
|
342
|
+
)
|
|
343
|
+
],
|
|
344
|
+
single ? [
|
|
345
|
+
` // Unreachable: no command of this graft chooses a primary, because a set`,
|
|
346
|
+
` // of one has nothing to choose between. It contributes no event.`,
|
|
347
|
+
` | Attachments.PrimarySet(_) => None`
|
|
348
|
+
] : [` | Attachments.PrimarySet({ref}) => ` + n.primarySet + `({` + c.entityId + `, ` + c.file + `: ref})`],
|
|
349
|
+
[
|
|
350
|
+
` | Attachments.AltTextSet({ref, altText}) =>`,
|
|
351
|
+
` ` + (
|
|
352
|
+
single ? "Some(" : ""
|
|
353
|
+
) + n.altTextSet + `({` + c.entityId + `, ` + c.file + `: ref, altText})` + (
|
|
354
|
+
single ? ")" : ""
|
|
355
|
+
),
|
|
356
|
+
` | Attachments.EffectiveChanged({ref}) =>`,
|
|
357
|
+
` ` + (
|
|
358
|
+
single ? "Some(" : ""
|
|
359
|
+
) + n.effectiveChanged + `({` + c.entityId + `, ` + c.file + `: ?ref})` + (
|
|
360
|
+
single ? ")" : ""
|
|
361
|
+
),
|
|
362
|
+
` }`,
|
|
363
|
+
``,
|
|
364
|
+
`let decide = (state, command) =>`,
|
|
365
|
+
` if !state.exists {`,
|
|
366
|
+
` Error(` + n.notFound + `)`,
|
|
367
|
+
` } else {`,
|
|
368
|
+
` // TODO(graft): this host's own refusal goes here, ahead of the set's rules —`,
|
|
369
|
+
` // an \`else if\` returning the error added above. A graft with no extra refusal`,
|
|
370
|
+
` // is a complete graft, so leaving this is legitimate.`,
|
|
371
|
+
` let (` + c.entityId + `, op) = toOp(command)`,
|
|
372
|
+
single ? ` switch state.attachments->Attachments.decide(~cardinality=Single, op) {` : ` switch state.attachments->Attachments.decide(op) {`,
|
|
373
|
+
` | Error(#NotAttached) => Error(` + n.notAttached + `)`,
|
|
374
|
+
single ? ` | Ok(facts) => Ok(facts->Array.filterMap(toEvent(` + c.entityId + `, _)))` : ` | Ok(facts) => Ok(facts->Array.map(toEvent(` + c.entityId + `, _)))`,
|
|
375
|
+
` }`,
|
|
376
|
+
` }`,
|
|
377
|
+
``
|
|
378
|
+
]
|
|
379
|
+
]).join("\n");
|
|
225
380
|
}
|
|
226
381
|
|
|
227
382
|
function conformanceBinding(c) {
|
|
228
383
|
let n = namesOf(c);
|
|
384
|
+
let single = Stdlib_Option.getOr(c.cardinality, "Many") === "Single";
|
|
229
385
|
let id = "e1";
|
|
230
386
|
let refA = Stdlib_Option.getOr(c.refA, `/uploads/00000000-0000-4000-8000-000000000001/a`);
|
|
231
387
|
let refB = Stdlib_Option.getOr(c.refB, `/uploads/00000000-0000-4000-8000-000000000002/b`);
|
|
232
|
-
let createdValue = Stdlib_Option.getOr(c.createdCarriesEntityId, true) ? c.created + `({ ` + c.entityId + `:
|
|
233
|
-
return [
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
` let
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
`}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
388
|
+
let createdValue = Stdlib_Option.getOr(c.createdCarriesEntityId, true) ? c.created + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `})` : c.created;
|
|
389
|
+
return Belt_Array.concatMany([
|
|
390
|
+
[
|
|
391
|
+
`// The Attachments trait's conformance suite, bound to \`` + n.slice + `\`.`,
|
|
392
|
+
`// Emitted whole: every name here is one the graft already declared.`,
|
|
393
|
+
``,
|
|
394
|
+
`module Binding = {`,
|
|
395
|
+
` type ref = string`,
|
|
396
|
+
` let refA = "` + refA + `"`,
|
|
397
|
+
` let refB = "` + refB + `"`,
|
|
398
|
+
``,
|
|
399
|
+
` module Spec = ` + n.slice,
|
|
400
|
+
` module Behavior = ` + n.slice + `_Behavior`,
|
|
401
|
+
``,
|
|
402
|
+
` // Annotated: the slice consumes and emits same-named constructors.`,
|
|
403
|
+
` // The same facts as the slice consumes them: no ` + c.entityId + `, because the`,
|
|
404
|
+
` // partition already says which entity they belong to.`,
|
|
405
|
+
` module Consumed = {`,
|
|
406
|
+
` let created: array<` + n.slice + `.consumedEvent> = [` + createdValue + `]`,
|
|
407
|
+
` let attached = (ref): ` + n.slice + `.consumedEvent => ` + n.attached + `({ ` + c.file + `: ref})`,
|
|
408
|
+
` let removed = (ref): ` + n.slice + `.consumedEvent => ` + n.removed + `({ ` + c.file + `: ref})`
|
|
409
|
+
],
|
|
410
|
+
single ? [] : [` let primarySet = (ref): ` + n.slice + `.consumedEvent => ` + n.primarySet + `({ ` + c.file + `: ref})`],
|
|
411
|
+
[
|
|
412
|
+
` let altTextSet = (ref, altText): ` + n.slice + `.consumedEvent =>`,
|
|
413
|
+
` ` + n.altTextSet + `({ ` + c.file + `: ref, altText})`,
|
|
414
|
+
` let effectiveChanged = (ref): ` + n.slice + `.consumedEvent =>`,
|
|
415
|
+
` ` + n.effectiveChanged + `({ ` + c.file + `: ?ref})`,
|
|
416
|
+
` }`,
|
|
417
|
+
``,
|
|
418
|
+
` let attach = ref => ` + n.slice + `.` + n.attachCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`
|
|
419
|
+
],
|
|
420
|
+
single ? [` let clear = ` + n.slice + `.` + n.removeCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `})`] : [
|
|
421
|
+
` let remove = ref => ` + n.slice + `.` + n.removeCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`,
|
|
422
|
+
` let setPrimary = ref =>`,
|
|
423
|
+
` ` + n.slice + `.` + n.setPrimaryCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`
|
|
424
|
+
],
|
|
425
|
+
[
|
|
426
|
+
single ? ` let setAltText = altText => ` + n.slice + `.` + n.setAltTextCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, altText})` : ` let setAltText = (ref, altText) =>\n ` + n.slice + `.` + n.setAltTextCmd + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref, altText})`,
|
|
427
|
+
``,
|
|
428
|
+
` let attached = ref => ` + n.slice + `.` + n.attached + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`,
|
|
429
|
+
` let removed = ref => ` + n.slice + `.` + n.removed + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`
|
|
430
|
+
],
|
|
431
|
+
single ? [] : [
|
|
432
|
+
` let primarySet = ref =>`,
|
|
433
|
+
` ` + n.slice + `.` + n.primarySet + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref})`
|
|
434
|
+
],
|
|
435
|
+
[
|
|
436
|
+
` let altTextSet = (ref, altText) =>`,
|
|
437
|
+
` ` + n.slice + `.` + n.altTextSet + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ref, altText})`,
|
|
438
|
+
` let effectiveChanged = ref =>`,
|
|
439
|
+
` ` + n.slice + `.` + n.effectiveChanged + `({ ` + c.entityId + `: ` + idValueOf(c, id) + `, ` + c.file + `: ?ref})`,
|
|
440
|
+
` let notAttached = ` + n.slice + `.` + n.notAttached,
|
|
441
|
+
`}`,
|
|
442
|
+
``,
|
|
443
|
+
single ? `module Conformance = TraitAttachments.Attachments_Conformance.MakeSingle(Binding)` : `module Conformance = TraitAttachments.Attachments_Conformance.Make(Binding)`,
|
|
444
|
+
``,
|
|
445
|
+
`Conformance.register()`,
|
|
446
|
+
``
|
|
447
|
+
]
|
|
448
|
+
]).join("\n");
|
|
274
449
|
}
|
|
275
450
|
|
|
276
|
-
function
|
|
451
|
+
function singleProjectionPatch(c) {
|
|
277
452
|
let n = namesOf(c);
|
|
278
453
|
return {
|
|
279
|
-
into: `
|
|
280
|
-
at: `the projection's \`switch\`, and
|
|
454
|
+
into: `StateViewStream/` + c.view + `_Projection.res`,
|
|
455
|
+
at: `the projection's \`switch\`, and one field on \`` + c.view + `\`'s state`,
|
|
281
456
|
contents: [
|
|
282
|
-
`// On the view's state,
|
|
283
|
-
`//
|
|
284
|
-
`//
|
|
457
|
+
`// On the view's state, one field — the reference and its text, in one value.`,
|
|
458
|
+
`// No collection: this entity holds one attachment, so the field a card, a`,
|
|
459
|
+
`// gallery tile and a list cell read IS the whole of what it has.`,
|
|
285
460
|
`//`,
|
|
286
|
-
`// ` + c.file +
|
|
287
|
-
`// ` + c.file + `?: string,`,
|
|
461
|
+
`// ` + c.file + `?: ` + viewTypeOf(c) + `,`,
|
|
288
462
|
``,
|
|
289
463
|
`| ` + n.attached + `({` + c.entityId + `, ` + c.file + `, altText: ?altText}) =>`,
|
|
290
464
|
` Update(` + c.entityId + `, state => {`,
|
|
291
|
-
`
|
|
292
|
-
`
|
|
465
|
+
` ...state,`,
|
|
466
|
+
` ` + c.file + `: {ref: ` + c.file + `, altText: ?altText},`,
|
|
293
467
|
` })`,
|
|
294
468
|
`| ` + n.removed + `({` + c.entityId + `, ` + c.file + `}) =>`,
|
|
469
|
+
` Update(` + c.entityId + `, state =>`,
|
|
470
|
+
` // Guarded on the reference: a removal that names something this row no`,
|
|
471
|
+
` // longer holds — the first half of a replacement, arriving late — must not`,
|
|
472
|
+
` // blank the one it does.`,
|
|
473
|
+
` heldRef(state) == Some(` + c.file + `) ? {...state, ` + c.file + `: ?None} : state`,
|
|
474
|
+
` )`,
|
|
475
|
+
`| ` + n.altTextSet + `({` + c.entityId + `, ` + c.file + `, altText}) =>`,
|
|
476
|
+
` Update(` + c.entityId + `, state =>`,
|
|
477
|
+
` switch state.` + c.file + ` {`,
|
|
478
|
+
` | Some(held) if held.ref == ` + c.file + ` => {...state, ` + c.file + `: {...held, altText}}`,
|
|
479
|
+
` | _ => state`,
|
|
480
|
+
` }`,
|
|
481
|
+
` )`,
|
|
482
|
+
``,
|
|
483
|
+
`// The reference this row holds, if it holds one. Named because both guards`,
|
|
484
|
+
`// above ask the same question of a value that is no longer the reference itself.`,
|
|
485
|
+
`let heldRef = (state: ` + c.view + `.state) => state.` + c.file + `->Option.map(held => held.ref)`
|
|
486
|
+
].join("\n")
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function manyProjectionPatch(c) {
|
|
491
|
+
let n = namesOf(c);
|
|
492
|
+
let set = c.file + "s";
|
|
493
|
+
return {
|
|
494
|
+
into: `StateViewStream/` + c.view + `_Projection.res`,
|
|
495
|
+
at: `the projection's \`switch\`, and one field on \`` + c.view + `\`'s state`,
|
|
496
|
+
contents: [
|
|
497
|
+
`// On the view's state, one field — the set, primary first.`,
|
|
498
|
+
`//`,
|
|
499
|
+
`// The primary is the FIRST member rather than a scalar beside the set. That is`,
|
|
500
|
+
`// what a card, a gallery tile and a list cell read, so there is no second field`,
|
|
501
|
+
`// to keep in step with the set and no arm that can forget to. The text rides`,
|
|
502
|
+
`// inside each member for the same reason: a cell renderer is handed a field and`,
|
|
503
|
+
`// a value and never the row, so a caption in a sibling field is one no cell can`,
|
|
504
|
+
`// draw.`,
|
|
505
|
+
`//`,
|
|
506
|
+
`// What it costs, stated plainly: attachment order stops being readable off the`,
|
|
507
|
+
`// view. The log still has it.`,
|
|
508
|
+
`//`,
|
|
509
|
+
`// ` + set + `: array<` + viewTypeOf(c) + `>,`,
|
|
510
|
+
``,
|
|
511
|
+
`// Appended, so the first attached is the primary until one is chosen.`,
|
|
512
|
+
`| ` + n.attached + `({` + c.entityId + `, ` + c.file + `, altText: ?altText}) =>`,
|
|
513
|
+
` Update(` + c.entityId + `, state =>`,
|
|
514
|
+
` state.` + set + `->Array.some(m => m.ref == ` + c.file + `)`,
|
|
515
|
+
` ? state`,
|
|
516
|
+
` : {`,
|
|
517
|
+
` ...state,`,
|
|
518
|
+
` ` + set + `: state.` + set + `->Array.concat([{ref: ` + c.file + `, altText: ?altText}]),`,
|
|
519
|
+
` }`,
|
|
520
|
+
` )`,
|
|
521
|
+
`// Removing the head promotes the next member with no arm to say so.`,
|
|
522
|
+
`| ` + n.removed + `({` + c.entityId + `, ` + c.file + `}) =>`,
|
|
295
523
|
` Update(` + c.entityId + `, state => {`,
|
|
296
|
-
`
|
|
297
|
-
`
|
|
524
|
+
` ...state,`,
|
|
525
|
+
` ` + set + `: state.` + set + `->Array.filter(m => m.ref != ` + c.file + `),`,
|
|
298
526
|
` })`,
|
|
299
527
|
`| ` + n.primarySet + `({` + c.entityId + `, ` + c.file + `}) =>`,
|
|
300
|
-
` Update(` + c.entityId + `, state =>
|
|
528
|
+
` Update(` + c.entityId + `, state => primaryFirst(state, ` + c.file + `))`,
|
|
301
529
|
`| ` + n.altTextSet + `({` + c.entityId + `, ` + c.file + `, altText}) =>`,
|
|
302
530
|
` Update(` + c.entityId + `, state => {`,
|
|
303
531
|
` ...state,`,
|
|
304
|
-
` ` +
|
|
305
|
-
` m.` + c.file + ` == ` + c.file + ` ? {...m, altText} : m`,
|
|
306
|
-
` ),`,
|
|
532
|
+
` ` + set + `: state.` + set + `->Array.map(m => m.ref == ` + c.file + ` ? {...m, altText} : m),`,
|
|
307
533
|
` })`,
|
|
308
534
|
``,
|
|
309
|
-
`//
|
|
310
|
-
`// the
|
|
311
|
-
|
|
312
|
-
`
|
|
535
|
+
`// Choosing the primary is moving it to the front — the trait's rule, applied`,
|
|
536
|
+
`// over the view's rows. The only arm that reorders; the rest leave the head`,
|
|
537
|
+
`// where they found it.`,
|
|
538
|
+
`let primaryFirst = (state: ` + c.view + `.state, chosen) => {`,
|
|
539
|
+
` ...state,`,
|
|
540
|
+
` ` + set + `: TraitAttachments.Attachments_Rules.primaryFirst(`,
|
|
313
541
|
` ~chosen,`,
|
|
314
|
-
` ~
|
|
315
|
-
`
|
|
542
|
+
` ~members=state.` + set + `,`,
|
|
543
|
+
` ~ref=m => m.ref,`,
|
|
544
|
+
` ),`,
|
|
545
|
+
`}`
|
|
316
546
|
].join("\n")
|
|
317
547
|
};
|
|
318
548
|
}
|
|
319
549
|
|
|
550
|
+
function projectionPatch(c) {
|
|
551
|
+
if (Stdlib_Option.getOr(c.cardinality, "Many") === "Single") {
|
|
552
|
+
return singleProjectionPatch(c);
|
|
553
|
+
} else {
|
|
554
|
+
return manyProjectionPatch(c);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
320
558
|
function emit(config, into, tests) {
|
|
321
559
|
let n = namesOf(config);
|
|
322
560
|
return {
|
|
323
561
|
files: [
|
|
324
562
|
{
|
|
325
|
-
path: into + `/
|
|
563
|
+
path: into + `/StateChange/` + n.slice + `.res`,
|
|
326
564
|
contents: sliceSpec(config)
|
|
327
565
|
},
|
|
328
566
|
{
|
|
329
|
-
path: into + `/
|
|
567
|
+
path: into + `/StateChange/` + n.slice + `_Behavior.res`,
|
|
330
568
|
contents: sliceBehavior(config)
|
|
331
569
|
},
|
|
332
570
|
{
|
|
@@ -340,14 +578,26 @@ function emit(config, into, tests) {
|
|
|
340
578
|
|
|
341
579
|
export {
|
|
342
580
|
configSchema,
|
|
581
|
+
cardinalityOf,
|
|
582
|
+
isSingle,
|
|
343
583
|
namesOf,
|
|
344
584
|
refTypeOf,
|
|
585
|
+
idTypeOf,
|
|
586
|
+
idValueOf,
|
|
587
|
+
viewTypeOf,
|
|
588
|
+
setFieldOf,
|
|
589
|
+
selectionBinding,
|
|
590
|
+
selectionTypeOf,
|
|
591
|
+
contentArgOf,
|
|
345
592
|
commandAttributes,
|
|
593
|
+
commandNames,
|
|
346
594
|
commandTransitionBinding,
|
|
347
595
|
lines,
|
|
348
596
|
sliceSpec,
|
|
349
597
|
sliceBehavior,
|
|
350
598
|
conformanceBinding,
|
|
599
|
+
singleProjectionPatch,
|
|
600
|
+
manyProjectionPatch,
|
|
351
601
|
projectionPatch,
|
|
352
602
|
emit,
|
|
353
603
|
}
|