@reventlessdev/trait-attachments 1.0.0-alpha.3 → 1.0.0-alpha.4
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/package.json +3 -3
- package/src/Attachments.res +50 -0
- package/src/Attachments_Conformance.res +84 -1
- package/src/Attachments_Conformance.res.mjs +40 -0
- package/src/Attachments_Rules.res +121 -14
- package/src/Attachments_Rules.res.mjs +92 -36
- package/src/Attachments_Scaffold.res +342 -65
- package/src/Attachments_Scaffold.res.mjs +364 -151
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/trait-attachments",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
4
4
|
"description": "Attachments domain trait: rules, host contract, conformance suite and emitter for an ordered set of stored files on an entity, with a primary and captions",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"sury": "11.0.0-rc.2",
|
|
15
15
|
"sury-ppx": "11.0.0-rc.2",
|
|
16
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
17
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
16
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.202",
|
|
17
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.128"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"rescript": "12.3.0"
|
package/src/Attachments.res
CHANGED
|
@@ -64,3 +64,53 @@ module type Binding = {
|
|
|
64
64
|
/** The refusal for a primary or caption on a ref that is not in the set. */
|
|
65
65
|
let notAttached: Spec.error
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
One host of the **bounded** cardinality, bound.
|
|
70
|
+
|
|
71
|
+
A separate contract rather than optional members on {!Binding}, because what
|
|
72
|
+
`Attachments_Rules.Single` changes is the host's command surface and not just a
|
|
73
|
+
rule: there is no primary to choose between one member, so the graft declares no
|
|
74
|
+
such command, and there is no ref for a remove to name, so it declares none.
|
|
75
|
+
Both absences are facts a module type states better than a test does — a
|
|
76
|
+
`Single` graft that grew a `SetPrimary` command would fail to satisfy this
|
|
77
|
+
signature, which is the check, and it happens at compile time.
|
|
78
|
+
|
|
79
|
+
Everything the two contracts share is spelled identically on purpose: a reader
|
|
80
|
+
comparing them should find the difference in what is missing, not in how the
|
|
81
|
+
common half is phrased.
|
|
82
|
+
*/
|
|
83
|
+
module type SingleBinding = {
|
|
84
|
+
type ref
|
|
85
|
+
/** Two refs that differ — the second is what replaces the first. */
|
|
86
|
+
let refA: ref
|
|
87
|
+
let refB: ref
|
|
88
|
+
|
|
89
|
+
module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
|
|
90
|
+
module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
|
|
91
|
+
|
|
92
|
+
/** History that brings the entity into existence with an empty set. */
|
|
93
|
+
let created: array<Spec.consumedEvent>
|
|
94
|
+
/** The set's own facts, as the slice consumes them. No `primarySetC`: nothing
|
|
95
|
+
emits one. */
|
|
96
|
+
let attachedC: ref => Spec.consumedEvent
|
|
97
|
+
let removedC: ref => Spec.consumedEvent
|
|
98
|
+
let altTextSetC: (ref, string) => Spec.consumedEvent
|
|
99
|
+
|
|
100
|
+
let attach: ref => Spec.command
|
|
101
|
+
/** Remove whatever is held. Takes no ref — there is only one, and asking the
|
|
102
|
+
caller to name it is asking them to repeat what the row already says. */
|
|
103
|
+
let clear: Spec.command
|
|
104
|
+
/** Caption whatever is held. Ref-less for `clear`'s reason, and there is a
|
|
105
|
+
second one here: a bounded host's view carries the reference as a scalar
|
|
106
|
+
and no collection at all, so there is no field for a selection to be a
|
|
107
|
+
member *of*. */
|
|
108
|
+
let setAltText: string => Spec.command
|
|
109
|
+
|
|
110
|
+
/** The set's facts, as the slice emits them. */
|
|
111
|
+
let attached: ref => Spec.event
|
|
112
|
+
let removed: ref => Spec.event
|
|
113
|
+
let altTextSet: (ref, string) => Spec.event
|
|
114
|
+
/** The refusal for a caption on a ref that is not held. */
|
|
115
|
+
let notAttached: Spec.error
|
|
116
|
+
}
|
|
@@ -6,7 +6,12 @@ inside a Jest test file registers one `describe` block over the binding.
|
|
|
6
6
|
/** The suite's title, composed once and read twice: the suite registers it,
|
|
7
7
|
and `certify-trait` computes the same string to find the run's assertions in
|
|
8
8
|
a test report. Exported rather than inlined so neither side parses the
|
|
9
|
-
other's prose.
|
|
9
|
+
other's prose.
|
|
10
|
+
|
|
11
|
+
One title for both cardinalities: a host conforms to this trait or it does
|
|
12
|
+
not, and which of the two suites established that is not a fact a
|
|
13
|
+
certificate should have to know. `MakeSingle` registers under the same
|
|
14
|
+
name. */
|
|
10
15
|
let suiteName = (host: string) => `${host} conforms to the attachments trait`
|
|
11
16
|
|
|
12
17
|
module Make = (B: Attachments.Binding) => {
|
|
@@ -86,3 +91,81 @@ module Make = (B: Attachments.Binding) => {
|
|
|
86
91
|
)
|
|
87
92
|
})
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
The suite for a host of the bounded cardinality.
|
|
97
|
+
|
|
98
|
+
Smaller than `Make`'s, and smaller for a reason worth reading: most of what the
|
|
99
|
+
larger suite asserts is about choosing between members, and a set that holds one
|
|
100
|
+
member has nothing to choose. Those assertions are not skipped here — they are
|
|
101
|
+
unreachable, because {!Attachments.SingleBinding} does not admit the commands
|
|
102
|
+
that would reach them. The absent primary is checked by the compiler.
|
|
103
|
+
|
|
104
|
+
What is left is the one rule cardinality actually changes — a second attachment
|
|
105
|
+
replaces rather than joins — plus the ref-less remove and the captions, which a
|
|
106
|
+
single image wants exactly as much as a gallery member does.
|
|
107
|
+
*/
|
|
108
|
+
module MakeSingle = (B: Attachments.SingleBinding) => {
|
|
109
|
+
module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
|
|
110
|
+
|
|
111
|
+
let withA = Array.concat(B.created, [B.attachedC(B.refA)])
|
|
112
|
+
|
|
113
|
+
let register = () =>
|
|
114
|
+
G.describe(suiteName(B.Spec.name), () => {
|
|
115
|
+
G.test("the first attachment is appended", () =>
|
|
116
|
+
G.givenEvents(B.created)->G.whenCmd(B.attach(B.refA))->G.thenEvent(B.attached(B.refA))
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
G.test("attaching the ref already held is a no-op", () =>
|
|
120
|
+
G.givenEvents(withA)->G.whenCmd(B.attach(B.refA))->G.thenNoEvent
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// The assertion the cardinality exists for. Two facts, in order: what was
|
|
124
|
+
// there leaves before what replaces it arrives, so a reader replaying the
|
|
125
|
+
// log never sees the set hold both.
|
|
126
|
+
G.test("a second ref replaces the first rather than joining it", () =>
|
|
127
|
+
G.givenEvents(withA)
|
|
128
|
+
->G.whenCmd(B.attach(B.refB))
|
|
129
|
+
->G.thenEvents([B.removed(B.refA), B.attached(B.refB)])
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
G.test("clearing a held set removes what it holds", () =>
|
|
133
|
+
G.givenEvents(withA)->G.whenCmd(B.clear)->G.thenEvent(B.removed(B.refA))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
G.test("clearing an empty set is a no-op", () =>
|
|
137
|
+
G.givenEvents(B.created)->G.whenCmd(B.clear)->G.thenNoEvent
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
G.test("a replaced ref can be attached again", () =>
|
|
141
|
+
G.givenEvents(Array.concat(withA, [B.removedC(B.refA), B.attachedC(B.refB)]))
|
|
142
|
+
->G.whenCmd(B.attach(B.refA))
|
|
143
|
+
->G.thenEvents([B.removed(B.refB), B.attached(B.refA)])
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// The caption names no ref either, so what it lands on is whatever is
|
|
147
|
+
// held — and the assertion is that it lands on the *right* ref, which is
|
|
148
|
+
// the only thing a ref-less command can get wrong.
|
|
149
|
+
G.test("a caption lands on the ref that is held", () =>
|
|
150
|
+
G.givenEvents(withA)
|
|
151
|
+
->G.whenCmd(B.setAltText("front"))
|
|
152
|
+
->G.thenEvent(B.altTextSet(B.refA, "front"))
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
G.test("a caption follows a replacement onto the new ref", () =>
|
|
156
|
+
G.givenEvents(Array.concat(withA, [B.removedC(B.refA), B.attachedC(B.refB)]))
|
|
157
|
+
->G.whenCmd(B.setAltText("side"))
|
|
158
|
+
->G.thenEvent(B.altTextSet(B.refB, "side"))
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
G.test("captioning an empty set is refused", () =>
|
|
162
|
+
G.givenEvents(B.created)->G.whenCmd(B.setAltText("front"))->G.thenError(B.notAttached)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
G.test("repeating the caption is a no-op", () =>
|
|
166
|
+
G.givenEvents(Array.concat(withA, [B.altTextSetC(B.refA, "front")]))
|
|
167
|
+
->G.whenCmd(B.setAltText("front"))
|
|
168
|
+
->G.thenNoEvent
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
@@ -42,8 +42,48 @@ function Make(B) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function MakeSingle(B) {
|
|
46
|
+
let $$let = B.Behavior;
|
|
47
|
+
let G = Behavior_GWT$ReventlessGwt.Make(B.Spec)({
|
|
48
|
+
initialState: $$let.initialState,
|
|
49
|
+
evolve: $$let.evolve,
|
|
50
|
+
decide: $$let.decide
|
|
51
|
+
});
|
|
52
|
+
let withA = B.created.concat([B.attachedC(B.refA)]);
|
|
53
|
+
let register = () => G.describe(suiteName(B.Spec.name), () => {
|
|
54
|
+
G.test("the first attachment is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.attach(B.refA)), B.attached(B.refA)));
|
|
55
|
+
G.test("attaching the ref already held is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA), B.attach(B.refA))));
|
|
56
|
+
G.test("a second ref replaces the first rather than joining it", () => G.thenEvents(G.whenCmd(G.givenEvents(withA), B.attach(B.refB)), [
|
|
57
|
+
B.removed(B.refA),
|
|
58
|
+
B.attached(B.refB)
|
|
59
|
+
]));
|
|
60
|
+
G.test("clearing a held set removes what it holds", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.clear), B.removed(B.refA)));
|
|
61
|
+
G.test("clearing an empty set is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(B.created), B.clear)));
|
|
62
|
+
G.test("a replaced ref can be attached again", () => G.thenEvents(G.whenCmd(G.givenEvents(withA.concat([
|
|
63
|
+
B.removedC(B.refA),
|
|
64
|
+
B.attachedC(B.refB)
|
|
65
|
+
])), B.attach(B.refA)), [
|
|
66
|
+
B.removed(B.refB),
|
|
67
|
+
B.attached(B.refA)
|
|
68
|
+
]));
|
|
69
|
+
G.test("a caption lands on the ref that is held", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.setAltText("front")), B.altTextSet(B.refA, "front")));
|
|
70
|
+
G.test("a caption follows a replacement onto the new ref", () => G.thenEvent(G.whenCmd(G.givenEvents(withA.concat([
|
|
71
|
+
B.removedC(B.refA),
|
|
72
|
+
B.attachedC(B.refB)
|
|
73
|
+
])), B.setAltText("side")), B.altTextSet(B.refB, "side")));
|
|
74
|
+
G.test("captioning an empty set is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.created), B.setAltText("front")), B.notAttached));
|
|
75
|
+
G.test("repeating the caption is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA.concat([B.altTextSetC(B.refA, "front")])), B.setAltText("front"))));
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
G: G,
|
|
79
|
+
withA: withA,
|
|
80
|
+
register: register
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
45
84
|
export {
|
|
46
85
|
suiteName,
|
|
47
86
|
Make,
|
|
87
|
+
MakeSingle,
|
|
48
88
|
}
|
|
49
89
|
/* Behavior_GWT-ReventlessGwt Not a pure module */
|
|
@@ -12,6 +12,30 @@ entity is. `Attachments_Conformance` asserts these rules through a host.
|
|
|
12
12
|
without a wrapper the host would have to unwrap on every arm. */
|
|
13
13
|
type ref = string
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
How many members a host's set may hold.
|
|
17
|
+
|
|
18
|
+
Not a runtime variation: a graft fixes this once, and every command the graft
|
|
19
|
+
emits is shaped by the answer. It reaches `decide` as an argument rather than
|
|
20
|
+
sitting on `t` so that `empty` stays one value and a host that never mentions
|
|
21
|
+
cardinality gets the behaviour it has today.
|
|
22
|
+
|
|
23
|
+
Exactly one rule reads it — `Attach` — and that is the whole of the difference:
|
|
24
|
+
a bounded set replaces its member where an unbounded one appends beside it.
|
|
25
|
+
Everything else is already cardinality-blind. `SetPrimary` on a one-member set
|
|
26
|
+
resolves to a no-op through the rules it already has (the only member is already
|
|
27
|
+
the effective primary), which is why `Single` needs no branch for it and why the
|
|
28
|
+
scaffold simply declines to emit the command rather than the rules refusing it.
|
|
29
|
+
*/
|
|
30
|
+
// `@schema` so `Attachments_Scaffold`'s config can take this very type rather
|
|
31
|
+
// than a second spelling of it. A graft's config and the rule it selects being
|
|
32
|
+
// the same value is the point: a config that could say `"single"` where the
|
|
33
|
+
// rules say `Single` is a config that can be misspelled.
|
|
34
|
+
@schema
|
|
35
|
+
type cardinality =
|
|
36
|
+
| /** An unbounded, ordered set: a gallery. */ Many
|
|
37
|
+
| /** At most one member, replaced rather than added to. */ Single
|
|
38
|
+
|
|
15
39
|
/** Refolded per decision, never stored — a StateChangeSlice's state is. */
|
|
16
40
|
type t = {
|
|
17
41
|
attached: array<ref>,
|
|
@@ -27,8 +51,25 @@ type op =
|
|
|
27
51
|
| /** `altText` is the caption a host may supply with the file itself. */
|
|
28
52
|
Attach({ref: ref, altText: option<string>})
|
|
29
53
|
| Remove({ref: ref})
|
|
54
|
+
| /** Empty the set, whatever it holds.
|
|
55
|
+
|
|
56
|
+
The op a bounded set's remove command maps onto: with one member there is
|
|
57
|
+
no ref for the caller to name, and naming it would be asking them to
|
|
58
|
+
repeat what the row already says. Well defined at either cardinality —
|
|
59
|
+
an unbounded host that wants a "remove them all" command gets it here
|
|
60
|
+
rather than by looping its own remove. */
|
|
61
|
+
Clear
|
|
30
62
|
| SetPrimary({ref: ref})
|
|
31
63
|
| SetAltText({ref: ref, altText: string})
|
|
64
|
+
| /** Caption whichever member is the primary, without naming it.
|
|
65
|
+
|
|
66
|
+
The counterpart of `Clear`, and the op a bounded set's caption command
|
|
67
|
+
maps onto — with one member there is nothing to name. It is not
|
|
68
|
+
bounded-only, though: the primary is the one image a reader actually
|
|
69
|
+
sees, so "caption the hero" is a command an unbounded host wants too,
|
|
70
|
+
and resolving it here through `effectivePrimary` is what keeps the
|
|
71
|
+
answer the same as the member a projection puts first. */
|
|
72
|
+
SetPrimaryAltText({altText: string})
|
|
32
73
|
|
|
33
74
|
/** What the set decided, for the host to name in its own event. */
|
|
34
75
|
type fact =
|
|
@@ -50,6 +91,33 @@ let effectivePrimary = t => primaryOf(~chosen=t.primary, ~attached=t.attached)
|
|
|
50
91
|
|
|
51
92
|
let altTextOf = (t, ref) => t.altTexts->Array.find(((r, _)) => r == ref)->Option.map(((_, t)) => t)
|
|
52
93
|
|
|
94
|
+
/**
|
|
95
|
+
The set with the chosen member first, for a read model projecting it onto a row.
|
|
96
|
+
|
|
97
|
+
This is where `primaryOf`'s rule lands on a view. The view carries the set and
|
|
98
|
+
nothing beside it, so *being first* is how the row says which member is the
|
|
99
|
+
primary — and "the chosen one, else the first attached" then needs no second
|
|
100
|
+
field to hold the answer and no rule to keep that field in step. Attach appends,
|
|
101
|
+
remove filters, and both leave the head alone unless they moved it; only this
|
|
102
|
+
reorders.
|
|
103
|
+
|
|
104
|
+
Sound for a projection because the order is derived: the events fully determine
|
|
105
|
+
it, so a replay reproduces it. What it costs is that attachment order stops being
|
|
106
|
+
readable off the view — the log still has it, and a consumer that wanted it back
|
|
107
|
+
would have to be given it deliberately.
|
|
108
|
+
|
|
109
|
+
Parameterised on `~ref` rather than owning the member type, for the reason
|
|
110
|
+
`primaryOf` takes two arrays: the member record is the host's own `@schema` type
|
|
111
|
+
and the field holding its reference is named for the host's store. A `chosen` the
|
|
112
|
+
set does not hold leaves the order alone — a `SetPrimary` naming a member a
|
|
113
|
+
removal already took away must not empty the row.
|
|
114
|
+
*/
|
|
115
|
+
let primaryFirst = (~chosen: ref, ~members: array<'m>, ~ref: 'm => ref): array<'m> =>
|
|
116
|
+
switch members->Array.find(m => ref(m) == chosen) {
|
|
117
|
+
| Some(primary) => Array.concat([primary], members->Array.filter(m => ref(m) != chosen))
|
|
118
|
+
| None => members
|
|
119
|
+
}
|
|
120
|
+
|
|
53
121
|
let evolve = (t, fact) =>
|
|
54
122
|
switch fact {
|
|
55
123
|
| Attached({ref, altText}) =>
|
|
@@ -75,27 +143,66 @@ let evolve = (t, fact) =>
|
|
|
75
143
|
}
|
|
76
144
|
}
|
|
77
145
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
// Shared by the two captioning ops, which differ only in how they arrive at a
|
|
147
|
+
// ref. Spelled once so a named caption and the primary's caption cannot end up
|
|
148
|
+
// with different ideas of what a repeat is.
|
|
149
|
+
let setAltText = (t, ~ref, ~altText): result<array<fact>, [#NotAttached]> =>
|
|
150
|
+
if !(t.attached->Array.includes(ref)) {
|
|
151
|
+
Error(#NotAttached)
|
|
152
|
+
} else if altTextOf(t, ref) == Some(altText) {
|
|
153
|
+
Ok([])
|
|
154
|
+
} else {
|
|
155
|
+
Ok([AltTextSet({ref, altText})])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
What the set decided, as facts in the order they happened.
|
|
160
|
+
|
|
161
|
+
`Ok([])` is the no-op a retried command must produce; the one refusal the set
|
|
162
|
+
owns is a primary or a caption on a ref it does not hold.
|
|
163
|
+
|
|
164
|
+
An array rather than one optional fact, because a bounded set replacing its
|
|
165
|
+
member decides two things at once — the old one leaves and the new one arrives —
|
|
166
|
+
and an event log records both. Collapsing them into a single "replaced" fact
|
|
167
|
+
would make a host declare an event that only bounded hosts have, which is one
|
|
168
|
+
more way the two cardinalities' emitted surfaces could diverge.
|
|
169
|
+
*/
|
|
170
|
+
let decide = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
|
|
81
171
|
switch op {
|
|
82
172
|
| Attach({ref, altText}) =>
|
|
83
|
-
t.attached->Array.includes(ref)
|
|
84
|
-
|
|
173
|
+
if t.attached->Array.includes(ref) {
|
|
174
|
+
Ok([])
|
|
175
|
+
} else {
|
|
176
|
+
switch cardinality {
|
|
177
|
+
| Many => Ok([Attached({ref, altText})])
|
|
178
|
+
// The whole of what `Single` changes. The members that leave are named
|
|
179
|
+
// individually rather than through `Clear` so the facts read the same
|
|
180
|
+
// whether one was there or (through some history nothing produces) more.
|
|
181
|
+
| Single =>
|
|
182
|
+
Ok(
|
|
183
|
+
Array.concat(
|
|
184
|
+
t.attached->Array.map(r => Removed({ref: r})),
|
|
185
|
+
[Attached({ref, altText})],
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
| Remove({ref}) => t.attached->Array.includes(ref) ? Ok([Removed({ref: ref})]) : Ok([])
|
|
191
|
+
| Clear => Ok(t.attached->Array.map(r => Removed({ref: r})))
|
|
85
192
|
| SetPrimary({ref}) =>
|
|
86
193
|
if !(t.attached->Array.includes(ref)) {
|
|
87
194
|
Error(#NotAttached)
|
|
88
195
|
} else if effectivePrimary(t) == Some(ref) {
|
|
89
|
-
Ok(
|
|
196
|
+
Ok([])
|
|
90
197
|
} else {
|
|
91
|
-
Ok(
|
|
198
|
+
Ok([PrimarySet({ref: ref})])
|
|
92
199
|
}
|
|
93
|
-
| SetAltText({ref, altText}) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
200
|
+
| SetAltText({ref, altText}) => setAltText(t, ~ref, ~altText)
|
|
201
|
+
// An empty set has no primary, so there is nothing to caption — the same
|
|
202
|
+
// refusal a named ref the set does not hold gets, and for the same reason.
|
|
203
|
+
| SetPrimaryAltText({altText}) =>
|
|
204
|
+
switch effectivePrimary(t) {
|
|
205
|
+
| Some(ref) => setAltText(t, ~ref, ~altText)
|
|
206
|
+
| None => Error(#NotAttached)
|
|
100
207
|
}
|
|
101
208
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Sury from "sury";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
|
+
|
|
8
|
+
let cardinalitySchema = Sury.union([
|
|
9
|
+
Sury.literal("Many"),
|
|
10
|
+
Sury.literal("Single")
|
|
11
|
+
]);
|
|
5
12
|
|
|
6
13
|
let empty_attached = [];
|
|
7
14
|
|
|
@@ -29,6 +36,15 @@ function altTextOf(t, ref) {
|
|
|
29
36
|
return Stdlib_Option.map(t.altTexts.find(param => param[0] === ref), param => param[1]);
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
function primaryFirst(chosen, members, ref) {
|
|
40
|
+
let primary = members.find(m => ref(m) === chosen);
|
|
41
|
+
if (primary !== undefined) {
|
|
42
|
+
return [Primitive_option.valFromOption(primary)].concat(members.filter(m => ref(m) !== chosen));
|
|
43
|
+
} else {
|
|
44
|
+
return members;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
function evolve(t, fact) {
|
|
33
49
|
switch (fact.TAG) {
|
|
34
50
|
case "Attached" :
|
|
@@ -71,23 +87,72 @@ function evolve(t, fact) {
|
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
function
|
|
90
|
+
function setAltText(t, ref, altText) {
|
|
91
|
+
if (t.attached.includes(ref)) {
|
|
92
|
+
if (Primitive_object.equal(altTextOf(t, ref), altText)) {
|
|
93
|
+
return {
|
|
94
|
+
TAG: "Ok",
|
|
95
|
+
_0: []
|
|
96
|
+
};
|
|
97
|
+
} else {
|
|
98
|
+
return {
|
|
99
|
+
TAG: "Ok",
|
|
100
|
+
_0: [{
|
|
101
|
+
TAG: "AltTextSet",
|
|
102
|
+
ref: ref,
|
|
103
|
+
altText: altText
|
|
104
|
+
}]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
return {
|
|
109
|
+
TAG: "Error",
|
|
110
|
+
_0: "NotAttached"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decide(t, cardinalityOpt, op) {
|
|
116
|
+
let cardinality = cardinalityOpt !== undefined ? cardinalityOpt : "Many";
|
|
117
|
+
if (typeof op !== "object") {
|
|
118
|
+
return {
|
|
119
|
+
TAG: "Ok",
|
|
120
|
+
_0: t.attached.map(r => ({
|
|
121
|
+
TAG: "Removed",
|
|
122
|
+
ref: r
|
|
123
|
+
}))
|
|
124
|
+
};
|
|
125
|
+
}
|
|
75
126
|
switch (op.TAG) {
|
|
76
127
|
case "Attach" :
|
|
77
128
|
let ref = op.ref;
|
|
78
129
|
if (t.attached.includes(ref)) {
|
|
79
130
|
return {
|
|
80
131
|
TAG: "Ok",
|
|
81
|
-
_0:
|
|
132
|
+
_0: []
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
let altText = op.altText;
|
|
136
|
+
if (cardinality === "Many") {
|
|
137
|
+
return {
|
|
138
|
+
TAG: "Ok",
|
|
139
|
+
_0: [{
|
|
140
|
+
TAG: "Attached",
|
|
141
|
+
ref: ref,
|
|
142
|
+
altText: altText
|
|
143
|
+
}]
|
|
82
144
|
};
|
|
83
145
|
} else {
|
|
84
146
|
return {
|
|
85
147
|
TAG: "Ok",
|
|
86
|
-
_0: {
|
|
87
|
-
TAG: "
|
|
88
|
-
ref:
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
_0: t.attached.map(r => ({
|
|
149
|
+
TAG: "Removed",
|
|
150
|
+
ref: r
|
|
151
|
+
})).concat([{
|
|
152
|
+
TAG: "Attached",
|
|
153
|
+
ref: ref,
|
|
154
|
+
altText: altText
|
|
155
|
+
}])
|
|
91
156
|
};
|
|
92
157
|
}
|
|
93
158
|
case "Remove" :
|
|
@@ -95,15 +160,15 @@ function decide(t, op) {
|
|
|
95
160
|
if (t.attached.includes(ref$1)) {
|
|
96
161
|
return {
|
|
97
162
|
TAG: "Ok",
|
|
98
|
-
_0: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
163
|
+
_0: [{
|
|
164
|
+
TAG: "Removed",
|
|
165
|
+
ref: ref$1
|
|
166
|
+
}]
|
|
102
167
|
};
|
|
103
168
|
} else {
|
|
104
169
|
return {
|
|
105
170
|
TAG: "Ok",
|
|
106
|
-
_0:
|
|
171
|
+
_0: []
|
|
107
172
|
};
|
|
108
173
|
}
|
|
109
174
|
case "SetPrimary" :
|
|
@@ -112,15 +177,15 @@ function decide(t, op) {
|
|
|
112
177
|
if (Primitive_object.equal(effectivePrimary(t), ref$2)) {
|
|
113
178
|
return {
|
|
114
179
|
TAG: "Ok",
|
|
115
|
-
_0:
|
|
180
|
+
_0: []
|
|
116
181
|
};
|
|
117
182
|
} else {
|
|
118
183
|
return {
|
|
119
184
|
TAG: "Ok",
|
|
120
|
-
_0: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
185
|
+
_0: [{
|
|
186
|
+
TAG: "PrimarySet",
|
|
187
|
+
ref: ref$2
|
|
188
|
+
}]
|
|
124
189
|
};
|
|
125
190
|
}
|
|
126
191
|
} else {
|
|
@@ -130,38 +195,29 @@ function decide(t, op) {
|
|
|
130
195
|
};
|
|
131
196
|
}
|
|
132
197
|
case "SetAltText" :
|
|
133
|
-
|
|
134
|
-
|
|
198
|
+
return setAltText(t, op.ref, op.altText);
|
|
199
|
+
case "SetPrimaryAltText" :
|
|
200
|
+
let ref$3 = effectivePrimary(t);
|
|
201
|
+
if (ref$3 !== undefined) {
|
|
202
|
+
return setAltText(t, ref$3, op.altText);
|
|
203
|
+
} else {
|
|
135
204
|
return {
|
|
136
205
|
TAG: "Error",
|
|
137
206
|
_0: "NotAttached"
|
|
138
207
|
};
|
|
139
208
|
}
|
|
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
209
|
}
|
|
157
210
|
}
|
|
158
211
|
|
|
159
212
|
export {
|
|
213
|
+
cardinalitySchema,
|
|
160
214
|
empty,
|
|
161
215
|
primaryOf,
|
|
162
216
|
effectivePrimary,
|
|
163
217
|
altTextOf,
|
|
218
|
+
primaryFirst,
|
|
164
219
|
evolve,
|
|
220
|
+
setAltText,
|
|
165
221
|
decide,
|
|
166
222
|
}
|
|
167
|
-
/*
|
|
223
|
+
/* cardinalitySchema Not a pure module */
|