@reventlessdev/trait-attachments 1.0.0-alpha.1 → 1.0.0-alpha.11

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.
@@ -7,11 +7,37 @@ A host maps its own constructors onto `op` and `fact` and keeps the spec surface
7
7
  the variants, their annotations, its own refusals. Nothing here knows what an
8
8
  entity is. `Attachments_Conformance` asserts these rules through a host.
9
9
  */
10
-
11
10
  /** The stored file's reference. A `StorageRef` path today, so `string` carries it
12
11
  without a wrapper the host would have to unwrap on every arm. */
13
12
  type ref = string
14
13
 
14
+ // `@schema` so `Attachments_Scaffold`'s config can take this very type rather
15
+ // than a second spelling of it. A graft's config and the rule it selects being
16
+ // the same value is the point: a config that could say `"single"` where the
17
+ // rules say `Single` is a config that can be misspelled.
18
+
19
+ /**
20
+ How many members a host's set may hold.
21
+
22
+ Not a runtime variation: a graft fixes this once, and every command the graft
23
+ emits is shaped by the answer. It reaches `decide` as an argument rather than
24
+ sitting on `t` so that `empty` stays one value and a host that never mentions
25
+ cardinality gets the behaviour it has today.
26
+
27
+ Exactly one rule reads it — `Attach` — and that is the whole of the difference:
28
+ a bounded set replaces its member where an unbounded one appends beside it.
29
+ Everything else is already cardinality-blind. `SetPrimary` on a one-member set
30
+ resolves to a no-op through the rules it already has (the only member is already
31
+ the effective primary), which is why `Single` needs no branch for it and why the
32
+ scaffold simply declines to emit the command rather than the rules refusing it.
33
+ */
34
+ @schema
35
+ type cardinality =
36
+ /** An unbounded, ordered set: a gallery. */
37
+ | Many
38
+ /** At most one member, replaced rather than added to. */
39
+ | Single
40
+
15
41
  /** Refolded per decision, never stored — a StateChangeSlice's state is. */
16
42
  type t = {
17
43
  attached: array<ref>,
@@ -24,11 +50,28 @@ let empty = {attached: [], primary: None, altTexts: []}
24
50
 
25
51
  /** What a host asks the set to do. */
26
52
  type op =
27
- | /** `altText` is the caption a host may supply with the file itself. */
28
- Attach({ref: ref, altText: option<string>})
53
+ /** `altText` is the caption a host may supply with the file itself. */
54
+ | Attach({ref: ref, altText: option<string>})
29
55
  | Remove({ref: ref})
56
+ /** Empty the set, whatever it holds.
57
+
58
+ The op a bounded set's remove command maps onto: with one member there is
59
+ no ref for the caller to name, and naming it would be asking them to
60
+ repeat what the row already says. Well defined at either cardinality —
61
+ an unbounded host that wants a "remove them all" command gets it here
62
+ rather than by looping its own remove. */
63
+ | Clear
30
64
  | SetPrimary({ref: ref})
31
65
  | SetAltText({ref: ref, altText: string})
66
+ /** Caption whichever member is the primary, without naming it.
67
+
68
+ The counterpart of `Clear`, and the op a bounded set's caption command
69
+ maps onto — with one member there is nothing to name. It is not
70
+ bounded-only, though: the primary is the one image a reader actually
71
+ sees, so "caption the hero" is a command an unbounded host wants too,
72
+ and resolving it here through `effectivePrimary` is what keeps the
73
+ answer the same as the member a projection puts first. */
74
+ | SetPrimaryAltText({altText: string})
32
75
 
33
76
  /** What the set decided, for the host to name in its own event. */
34
77
  type fact =
@@ -36,6 +79,25 @@ type fact =
36
79
  | Removed({ref: ref})
37
80
  | PrimarySet({ref: ref})
38
81
  | AltTextSet({ref: ref, altText: string})
82
+ /** The member a reader should now show, or `None` where the set has none.
83
+
84
+ **A conclusion, not a decision.** `PrimarySet` records that somebody
85
+ chose; this records what the set resolved to, however it got there —
86
+ which is a different fact, and the one anything outside the set can act
87
+ on. The two coincide on an explicit choice and diverge everywhere else:
88
+ the first attachment stands in with nobody choosing, and removing the
89
+ one that stood in promotes the next.
90
+
91
+ Emitted at **both** cardinalities even though a `Single` host could
92
+ derive it from its own `Attached`/`Removed` pair, for the reason
93
+ `decide` gives below for not collapsing those two: a fact that only some
94
+ grafts emit makes a subscriber ask how many members the host allows,
95
+ which is not its business.
96
+
97
+ Optional because "there is no longer one to show" is as much a change as
98
+ any other, and a subscriber that cannot be told it keeps showing a file
99
+ the set no longer holds. */
100
+ | EffectiveChanged({ref: option<ref>})
39
101
 
40
102
  /** The primary a reader should show: the one chosen, else the first attached, so
41
103
  a set never shows no file while it holds one. The read model applies the same
@@ -50,6 +112,33 @@ let effectivePrimary = t => primaryOf(~chosen=t.primary, ~attached=t.attached)
50
112
 
51
113
  let altTextOf = (t, ref) => t.altTexts->Array.find(((r, _)) => r == ref)->Option.map(((_, t)) => t)
52
114
 
115
+ /**
116
+ The set with the chosen member first, for a read model projecting it onto a row.
117
+
118
+ This is where `primaryOf`'s rule lands on a view. The view carries the set and
119
+ nothing beside it, so *being first* is how the row says which member is the
120
+ primary — and "the chosen one, else the first attached" then needs no second
121
+ field to hold the answer and no rule to keep that field in step. Attach appends,
122
+ remove filters, and both leave the head alone unless they moved it; only this
123
+ reorders.
124
+
125
+ Sound for a projection because the order is derived: the events fully determine
126
+ it, so a replay reproduces it. What it costs is that attachment order stops being
127
+ readable off the view — the log still has it, and a consumer that wanted it back
128
+ would have to be given it deliberately.
129
+
130
+ Parameterised on `~ref` rather than owning the member type, for the reason
131
+ `primaryOf` takes two arrays: the member record is the host's own `@schema` type
132
+ and the field holding its reference is named for the host's store. A `chosen` the
133
+ set does not hold leaves the order alone — a `SetPrimary` naming a member a
134
+ removal already took away must not empty the row.
135
+ */
136
+ let primaryFirst = (~chosen: ref, ~members: array<'m>, ~ref: 'm => ref): array<'m> =>
137
+ switch members->Array.find(m => ref(m) == chosen) {
138
+ | Some(primary) => Array.concat([primary], members->Array.filter(m => ref(m) != chosen))
139
+ | None => members
140
+ }
141
+
53
142
  let evolve = (t, fact) =>
54
143
  switch fact {
55
144
  | Attached({ref, altText}) =>
@@ -69,33 +158,94 @@ let evolve = (t, fact) =>
69
158
  altTexts: t.altTexts->Array.filter(((r, _)) => r != ref),
70
159
  }
71
160
  | PrimarySet({ref}) => {...t, primary: Some(ref)}
161
+ // Carries no state of its own: it is what `effectivePrimary` already answers,
162
+ // said out loud. Folding it would be storing a derivation beside the values it
163
+ // derives from, which is the one way the two could disagree.
164
+ | EffectiveChanged(_) => t
72
165
  | AltTextSet({ref, altText}) => {
73
166
  ...t,
74
167
  altTexts: t.altTexts->Array.filter(((r, _)) => r != ref)->Array.concat([(ref, altText)]),
75
168
  }
76
169
  }
77
170
 
78
- /** `Ok(None)` is the no-op a retried command must produce; the one refusal the set
79
- owns is a primary or a caption on a ref it does not hold. */
80
- let decide = (t, op): result<option<fact>, [#NotAttached]> =>
171
+ // Shared by the two captioning ops, which differ only in how they arrive at a
172
+ // ref. Spelled once so a named caption and the primary's caption cannot end up
173
+ // with different ideas of what a repeat is.
174
+ let setAltText = (t, ~ref, ~altText): result<array<fact>, [#NotAttached]> =>
175
+ if !(t.attached->Array.includes(ref)) {
176
+ Error(#NotAttached)
177
+ } else if altTextOf(t, ref) == Some(altText) {
178
+ Ok([])
179
+ } else {
180
+ Ok([AltTextSet({ref, altText})])
181
+ }
182
+
183
+ /**
184
+ What the set decided, as facts in the order they happened.
185
+
186
+ `Ok([])` is the no-op a retried command must produce; the one refusal the set
187
+ owns is a primary or a caption on a ref it does not hold.
188
+
189
+ An array rather than one optional fact, because a bounded set replacing its
190
+ member decides two things at once — the old one leaves and the new one arrives —
191
+ and an event log records both. Collapsing them into a single "replaced" fact
192
+ would make a host declare an event that only bounded hosts have, which is one
193
+ more way the two cardinalities' emitted surfaces could diverge.
194
+ */
195
+ let decideFacts = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
81
196
  switch op {
82
197
  | Attach({ref, altText}) =>
83
- t.attached->Array.includes(ref) ? Ok(None) : Ok(Some(Attached({ref, altText})))
84
- | Remove({ref}) => t.attached->Array.includes(ref) ? Ok(Some(Removed({ref: ref}))) : Ok(None)
198
+ if t.attached->Array.includes(ref) {
199
+ Ok([])
200
+ } else {
201
+ switch cardinality {
202
+ | Many => Ok([Attached({ref, altText})])
203
+ // The whole of what `Single` changes. The members that leave are named
204
+ // individually rather than through `Clear` so the facts read the same
205
+ // whether one was there or (through some history nothing produces) more.
206
+ | Single =>
207
+ Ok(Array.concat(t.attached->Array.map(r => Removed({ref: r})), [Attached({ref, altText})]))
208
+ }
209
+ }
210
+ | Remove({ref}) => t.attached->Array.includes(ref) ? Ok([Removed({ref: ref})]) : Ok([])
211
+ | Clear => Ok(t.attached->Array.map(r => Removed({ref: r})))
85
212
  | SetPrimary({ref}) =>
86
213
  if !(t.attached->Array.includes(ref)) {
87
214
  Error(#NotAttached)
88
215
  } else if effectivePrimary(t) == Some(ref) {
89
- Ok(None)
216
+ Ok([])
90
217
  } else {
91
- Ok(Some(PrimarySet({ref: ref})))
218
+ Ok([PrimarySet({ref: ref})])
92
219
  }
93
- | SetAltText({ref, altText}) =>
94
- if !(t.attached->Array.includes(ref)) {
95
- Error(#NotAttached)
96
- } else if altTextOf(t, ref) == Some(altText) {
97
- Ok(None)
98
- } else {
99
- Ok(Some(AltTextSet({ref, altText})))
220
+ | SetAltText({ref, altText}) => setAltText(t, ~ref, ~altText)
221
+ // An empty set has no primary, so there is nothing to caption — the same
222
+ // refusal a named ref the set does not hold gets, and for the same reason.
223
+ | SetPrimaryAltText({altText}) =>
224
+ switch effectivePrimary(t) {
225
+ | Some(ref) => setAltText(t, ~ref, ~altText)
226
+ | None => Error(#NotAttached)
100
227
  }
101
228
  }
229
+
230
+ /**
231
+ The set's own facts, followed by `EffectiveChanged` when those facts moved which
232
+ member a reader should show.
233
+
234
+ Appended rather than woven into each arm because the question is the same one
235
+ after every op: fold what was decided, ask `effectivePrimary` again, and say so
236
+ if the answer differs. An arm that had to remember to announce would eventually
237
+ be an arm that forgot — and the ops where it moves without anybody choosing (a
238
+ first attachment, a removal that promotes the next) are exactly the ones where
239
+ forgetting is easiest.
240
+ */
241
+ let decide = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
242
+ switch decideFacts(t, ~cardinality, op) {
243
+ | Error(_) as e => e
244
+ | Ok(facts) =>
245
+ let after = facts->Array.reduce(t, evolve)
246
+ Ok(
247
+ effectivePrimary(t) == effectivePrimary(after)
248
+ ? facts
249
+ : facts->Array.concat([EffectiveChanged({ref: effectivePrimary(after)})]),
250
+ )
251
+ }
@@ -1,7 +1,15 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
3
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
+
9
+ let cardinalitySchema = Sury.union([
10
+ Sury.literal("Many"),
11
+ Sury.literal("Single")
12
+ ]);
5
13
 
6
14
  let empty_attached = [];
7
15
 
@@ -29,6 +37,15 @@ function altTextOf(t, ref) {
29
37
  return Stdlib_Option.map(t.altTexts.find(param => param[0] === ref), param => param[1]);
30
38
  }
31
39
 
40
+ function primaryFirst(chosen, members, ref) {
41
+ let primary = members.find(m => ref(m) === chosen);
42
+ if (primary !== undefined) {
43
+ return [Primitive_option.valFromOption(primary)].concat(members.filter(m => ref(m) !== chosen));
44
+ } else {
45
+ return members;
46
+ }
47
+ }
48
+
32
49
  function evolve(t, fact) {
33
50
  switch (fact.TAG) {
34
51
  case "Attached" :
@@ -68,26 +85,77 @@ function evolve(t, fact) {
68
85
  fact.altText
69
86
  ]])
70
87
  };
88
+ case "EffectiveChanged" :
89
+ return t;
90
+ }
91
+ }
92
+
93
+ function setAltText(t, ref, altText) {
94
+ if (t.attached.includes(ref)) {
95
+ if (Primitive_object.equal(altTextOf(t, ref), altText)) {
96
+ return {
97
+ TAG: "Ok",
98
+ _0: []
99
+ };
100
+ } else {
101
+ return {
102
+ TAG: "Ok",
103
+ _0: [{
104
+ TAG: "AltTextSet",
105
+ ref: ref,
106
+ altText: altText
107
+ }]
108
+ };
109
+ }
110
+ } else {
111
+ return {
112
+ TAG: "Error",
113
+ _0: "NotAttached"
114
+ };
71
115
  }
72
116
  }
73
117
 
74
- function decide(t, op) {
118
+ function decideFacts(t, cardinalityOpt, op) {
119
+ let cardinality = cardinalityOpt !== undefined ? cardinalityOpt : "Many";
120
+ if (typeof op !== "object") {
121
+ return {
122
+ TAG: "Ok",
123
+ _0: t.attached.map(r => ({
124
+ TAG: "Removed",
125
+ ref: r
126
+ }))
127
+ };
128
+ }
75
129
  switch (op.TAG) {
76
130
  case "Attach" :
77
131
  let ref = op.ref;
78
132
  if (t.attached.includes(ref)) {
79
133
  return {
80
134
  TAG: "Ok",
81
- _0: undefined
135
+ _0: []
136
+ };
137
+ }
138
+ let altText = op.altText;
139
+ if (cardinality === "Many") {
140
+ return {
141
+ TAG: "Ok",
142
+ _0: [{
143
+ TAG: "Attached",
144
+ ref: ref,
145
+ altText: altText
146
+ }]
82
147
  };
83
148
  } else {
84
149
  return {
85
150
  TAG: "Ok",
86
- _0: {
87
- TAG: "Attached",
88
- ref: ref,
89
- altText: op.altText
90
- }
151
+ _0: t.attached.map(r => ({
152
+ TAG: "Removed",
153
+ ref: r
154
+ })).concat([{
155
+ TAG: "Attached",
156
+ ref: ref,
157
+ altText: altText
158
+ }])
91
159
  };
92
160
  }
93
161
  case "Remove" :
@@ -95,15 +163,15 @@ function decide(t, op) {
95
163
  if (t.attached.includes(ref$1)) {
96
164
  return {
97
165
  TAG: "Ok",
98
- _0: {
99
- TAG: "Removed",
100
- ref: ref$1
101
- }
166
+ _0: [{
167
+ TAG: "Removed",
168
+ ref: ref$1
169
+ }]
102
170
  };
103
171
  } else {
104
172
  return {
105
173
  TAG: "Ok",
106
- _0: undefined
174
+ _0: []
107
175
  };
108
176
  }
109
177
  case "SetPrimary" :
@@ -112,15 +180,15 @@ function decide(t, op) {
112
180
  if (Primitive_object.equal(effectivePrimary(t), ref$2)) {
113
181
  return {
114
182
  TAG: "Ok",
115
- _0: undefined
183
+ _0: []
116
184
  };
117
185
  } else {
118
186
  return {
119
187
  TAG: "Ok",
120
- _0: {
121
- TAG: "PrimarySet",
122
- ref: ref$2
123
- }
188
+ _0: [{
189
+ TAG: "PrimarySet",
190
+ ref: ref$2
191
+ }]
124
192
  };
125
193
  }
126
194
  } else {
@@ -130,38 +198,47 @@ function decide(t, op) {
130
198
  };
131
199
  }
132
200
  case "SetAltText" :
133
- let ref$3 = op.ref;
134
- if (!t.attached.includes(ref$3)) {
201
+ return setAltText(t, op.ref, op.altText);
202
+ case "SetPrimaryAltText" :
203
+ let ref$3 = effectivePrimary(t);
204
+ if (ref$3 !== undefined) {
205
+ return setAltText(t, ref$3, op.altText);
206
+ } else {
135
207
  return {
136
208
  TAG: "Error",
137
209
  _0: "NotAttached"
138
210
  };
139
211
  }
140
- let altText = op.altText;
141
- if (Primitive_object.equal(altTextOf(t, ref$3), altText)) {
142
- return {
143
- TAG: "Ok",
144
- _0: undefined
145
- };
146
- } else {
147
- return {
148
- TAG: "Ok",
149
- _0: {
150
- TAG: "AltTextSet",
151
- ref: ref$3,
152
- altText: altText
153
- }
154
- };
155
- }
156
212
  }
157
213
  }
158
214
 
215
+ function decide(t, cardinalityOpt, op) {
216
+ let cardinality = cardinalityOpt !== undefined ? cardinalityOpt : "Many";
217
+ let e = decideFacts(t, cardinality, op);
218
+ if (e.TAG !== "Ok") {
219
+ return e;
220
+ }
221
+ let facts = e._0;
222
+ let after = Stdlib_Array.reduce(facts, t, evolve);
223
+ return {
224
+ TAG: "Ok",
225
+ _0: Primitive_object.equal(effectivePrimary(t), effectivePrimary(after)) ? facts : facts.concat([{
226
+ TAG: "EffectiveChanged",
227
+ ref: effectivePrimary(after)
228
+ }])
229
+ };
230
+ }
231
+
159
232
  export {
233
+ cardinalitySchema,
160
234
  empty,
161
235
  primaryOf,
162
236
  effectivePrimary,
163
237
  altTextOf,
238
+ primaryFirst,
164
239
  evolve,
240
+ setAltText,
241
+ decideFacts,
165
242
  decide,
166
243
  }
167
- /* No side effect */
244
+ /* cardinalitySchema Not a pure module */