• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

asciimoth / pasta / 26626415405

29 May 2026 08:18AM UTC coverage: 77.855% (-0.3%) from 78.117%
26626415405

push

github

asciimoth
fix(std): fix formular menu handling by some nodes

20 of 72 new or added lines in 12 files covered. (27.78%)

1 existing line in 1 file now uncovered.

5467 of 7022 relevant lines covered (77.86%)

323.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.11
/pasta/std/node_string.go
1
package std
2

3
import (
4
        "slices"
5
        "strings"
6

7
        "github.com/asciimoth/formular"
8
        "github.com/asciimoth/pasta/pasta"
9
)
10

11
type stringNode struct {
12
        pasta.BasicNode
13

14
        op       string
15
        variadic bool
16
        inputs   map[uint64]string
17
        value    any
18

19
        w     *pasta.Workspace
20
        id    uint64
21
        out   uint64
22
        lefts []uint64
23
}
24

25
func newStringNode(op string, variadic bool) *stringNode {
10✔
26
        n := &stringNode{op: op, variadic: variadic, inputs: map[uint64]string{}}
10✔
27
        n.recalculate(false)
10✔
28
        return n
10✔
29
}
10✔
30

31
func (n *stringNode) OnInit(w *pasta.Workspace, _ pasta.Logger, id uint64, _ string, restored *pasta.NodeInitData, _, _, _, _ bool) error {
10✔
32
        n.w = w
10✔
33
        n.id = id
10✔
34
        if restored != nil {
20✔
35
                if len(restored.RightPorts) > 0 {
20✔
36
                        n.out = restored.RightPorts[0]
10✔
37
                }
10✔
38
                n.lefts = append([]uint64{}, restored.LeftPorts...)
10✔
39
        }
40
        if err := n.w.SetNodePrimary(n.id, n.outputType()); err != nil {
10✔
41
                return err
×
42
        }
×
43
        n.recalculate(false)
10✔
44
        if err := n.updateLabel(); err != nil {
10✔
45
                return err
×
46
        }
×
47
        n.sendMenuSnapshot()
10✔
48
        if n.variadic {
13✔
49
                n.scheduleBalanceInputs()
3✔
50
        }
3✔
51
        return nil
10✔
52
}
53

54
func (n *stringNode) OnReady() error {
10✔
55
        n.requestAll()
10✔
56
        n.sendAll()
10✔
57
        return nil
10✔
58
}
10✔
59

60
func (n *stringNode) PreLinkAdd(port uint64, linkType, portDirection string) error {
21✔
61
        switch portDirection {
21✔
62
        case "left":
15✔
63
                if linkType != TypeString {
15✔
NEW
64
                        return pasta.LinkTypeErr(linkType)
×
65
                }
×
66
                snapshot, ok := n.w.PortSnapshot(port)
15✔
67
                if ok && len(snapshot.Links) > 0 {
15✔
68
                        return pasta.ErrLinkDup
×
69
                }
×
70
                return nil
15✔
71
        case "right":
6✔
72
                if linkType != n.outputType() {
6✔
NEW
73
                        return pasta.LinkTypeErr(linkType)
×
74
                }
×
75
                return nil
6✔
76
        default:
×
NEW
77
                return pasta.LinkTypeErr(linkType)
×
78
        }
79
}
80

81
func (n *stringNode) OnLinkAdd(link, port uint64, _, portDirection string) error {
21✔
82
        if portDirection == "left" {
36✔
83
                n.requestLink(link, port)
15✔
84
                if n.variadic {
22✔
85
                        n.scheduleBalanceInputs()
7✔
86
                }
7✔
87
                return nil
15✔
88
        }
89
        if port == n.out {
12✔
90
                n.sendToLink(link)
6✔
91
        }
6✔
92
        return nil
6✔
93
}
94

95
func (n *stringNode) OnLinkRemoved(_ uint64, port uint64, _ string, portDirection string) error {
×
96
        if portDirection == "left" {
×
97
                delete(n.inputs, port)
×
98
                n.recalculate(true)
×
99
                if n.variadic {
×
100
                        n.scheduleBalanceInputs()
×
101
                }
×
102
        }
103
        return nil
×
104
}
105

106
func (n *stringNode) OnPortAdd(port uint64, direction string, _ []string) error {
7✔
107
        if direction == "left" {
14✔
108
                n.lefts = append(n.lefts, port)
7✔
109
                n.sortInputs()
7✔
110
        }
7✔
111
        return nil
7✔
112
}
113

114
func (n *stringNode) OnPortRemoved(port uint64, direction string) error {
1✔
115
        if direction == "left" {
2✔
116
                delete(n.inputs, port)
1✔
117
                n.refreshLefts()
1✔
118
                n.recalculate(true)
1✔
119
        }
1✔
120
        return nil
1✔
121
}
122

123
func (n *stringNode) OnEvent(event pasta.Event, linkType string, _ []string, receiverPortDirection string) error {
47✔
124
        if receiverPortDirection == "right" {
53✔
125
                if linkType == n.outputType() && isValueRequest(event.Payload) {
12✔
126
                        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: event.SenderNode, ReceiverPort: event.SenderPort, Payload: n.payload()})
6✔
127
                }
6✔
128
                return nil
6✔
129
        }
130
        if linkType != TypeString {
41✔
131
                return nil
×
132
        }
×
133
        value, ok := parseStringAny(event.Payload)
41✔
134
        if !ok {
41✔
135
                return nil
×
136
        }
×
137
        n.inputs[event.ReceiverPort] = value
41✔
138
        n.recalculate(true)
41✔
139
        return nil
41✔
140
}
141

142
func (n *stringNode) outputType() string {
84✔
143
        switch n.op {
84✔
144
        case "length":
6✔
145
                return TypeInt
6✔
146
        case "contains":
8✔
147
                return TypeBool
8✔
148
        default:
70✔
149
                return TypeString
70✔
150
        }
151
}
152

153
func (n *stringNode) recalculate(broadcast bool) {
62✔
154
        old := n.value
62✔
155
        switch n.op {
62✔
156
        case "concat":
26✔
157
                var b strings.Builder
26✔
158
                for i := range n.lefts {
85✔
159
                        b.WriteString(n.input(i))
59✔
160
                }
59✔
161
                n.value = b.String()
26✔
162
        case "length":
5✔
163
                n.value = len(n.input(0))
5✔
164
        case "contains":
7✔
165
                n.value = strings.Contains(n.input(0), n.input(1))
7✔
166
        case "upper":
10✔
167
                n.value = strings.ToUpper(n.input(0))
10✔
168
        case "lower":
5✔
169
                n.value = strings.ToLower(n.input(0))
5✔
170
        case "trimSpace":
9✔
171
                n.value = strings.TrimSpace(n.input(0))
9✔
172
        default:
×
173
                n.value = ""
×
174
        }
175
        _ = n.updateLabel()
62✔
176
        n.sendMenuBlock()
62✔
177
        if broadcast && old != n.value {
81✔
178
                n.sendAll()
19✔
179
        }
19✔
180
}
181

182
func (n *stringNode) input(index int) string {
102✔
183
        if index >= len(n.lefts) {
110✔
184
                return ""
8✔
185
        }
8✔
186
        return n.inputs[n.lefts[index]]
94✔
187
}
188

189
func (n *stringNode) payload() any {
18✔
190
        switch v := n.value.(type) {
18✔
191
        case int:
×
192
                return Int(v)
×
193
        case bool:
×
194
                return v
×
195
        case string:
18✔
196
                return String(v)
18✔
197
        default:
×
198
                return String("")
×
199
        }
200
}
201

202
func (n *stringNode) label() string {
62✔
203
        switch v := n.value.(type) {
62✔
204
        case int:
5✔
205
                return intValue(v).label()
5✔
206
        case bool:
7✔
207
                return boolLabel(v)
7✔
208
        case string:
50✔
209
                return v
50✔
210
        default:
×
211
                return ""
×
212
        }
213
}
214

215
func (n *stringNode) menuValue() any {
62✔
216
        switch v := n.value.(type) {
62✔
217
        case int:
5✔
218
                return v
5✔
219
        case bool:
7✔
220
                return v
7✔
221
        case string:
50✔
222
                return v
50✔
223
        default:
×
224
                return ""
×
225
        }
226
}
227

228
func (n *stringNode) requestAll() {
10✔
229
        for _, port := range n.lefts {
23✔
230
                snapshot, ok := n.w.PortSnapshot(port)
13✔
231
                if !ok {
13✔
232
                        continue
×
233
                }
234
                for _, link := range snapshot.Links {
15✔
235
                        n.requestLink(link, port)
2✔
236
                }
2✔
237
        }
238
}
239

240
func (n *stringNode) requestLink(link, port uint64) {
17✔
241
        snapshot, ok := n.w.LinkSnapshot(link)
17✔
242
        if !ok {
17✔
243
                return
×
244
        }
×
245
        receiverNode, receiverPort := otherEndpoint(snapshot, port)
17✔
246
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: port, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: RequestValue{}})
17✔
247
}
248

249
func (n *stringNode) sendAll() {
29✔
250
        port, ok := n.w.PortSnapshot(n.out)
29✔
251
        if !ok {
29✔
252
                return
×
253
        }
×
254
        for _, link := range port.Links {
35✔
255
                n.sendToLink(link)
6✔
256
        }
6✔
257
}
258

259
func (n *stringNode) sendToLink(link uint64) {
12✔
260
        snapshot, ok := n.w.LinkSnapshot(link)
12✔
261
        if !ok {
12✔
262
                return
×
263
        }
×
264
        receiverNode, receiverPort := otherEndpoint(snapshot, n.out)
12✔
265
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: n.payload()})
12✔
266
}
267

268
func (n *stringNode) scheduleBalanceInputs() {
10✔
269
        n.w.SendInbox(pasta.InboxMessage{ReceiverNode: n.id, Payload: "balanceInputs"})
10✔
270
}
10✔
271

272
func (n *stringNode) OnInbox(message pasta.InboxMessage) error {
10✔
273
        if message.Payload == "balanceInputs" && n.variadic {
20✔
274
                n.balanceInputs()
10✔
275
        }
10✔
276
        return nil
10✔
277
}
278

279
func (n *stringNode) balanceInputs() {
10✔
280
        for len(n.freeInputPorts()) == 0 {
17✔
281
                _, _ = n.w.AddPort(stringInputPortForNode(n.id, len(n.lefts)+1))
7✔
282
                n.refreshLefts()
7✔
283
        }
7✔
284
        for {
21✔
285
                free := n.freeInputPorts()
11✔
286
                if len(free) <= 1 {
21✔
287
                        break
10✔
288
                }
289
                trailing := n.trailingFreePort()
1✔
290
                if trailing == 0 {
1✔
291
                        break
×
292
                }
293
                n.w.RemovePort(trailing)
1✔
294
                n.refreshLefts()
1✔
295
        }
296
        n.sortInputs()
10✔
297
}
298

299
func (n *stringNode) refreshLefts() {
9✔
300
        snapshot, ok := n.w.NodeSnapshot(n.id)
9✔
301
        if !ok {
9✔
302
                n.lefts = nil
×
303
                return
×
304
        }
×
305
        n.lefts = append([]uint64{}, snapshot.LeftPorts...)
9✔
306
        n.sortInputs()
9✔
307
}
308

309
func (n *stringNode) freeInputPorts() []uint64 {
28✔
310
        free := []uint64{}
28✔
311
        for _, port := range n.lefts {
87✔
312
                snapshot, ok := n.w.PortSnapshot(port)
59✔
313
                if ok && len(snapshot.Links) == 0 {
82✔
314
                        free = append(free, port)
23✔
315
                }
23✔
316
        }
317
        return free
28✔
318
}
319

320
func (n *stringNode) trailingFreePort() uint64 {
1✔
321
        for i := len(n.lefts) - 1; i >= 0; i-- {
2✔
322
                port := n.lefts[i]
1✔
323
                snapshot, ok := n.w.PortSnapshot(port)
1✔
324
                if ok && len(snapshot.Links) == 0 {
2✔
325
                        return port
1✔
326
                }
1✔
327
                if ok && len(snapshot.Links) > 0 {
×
328
                        return 0
×
329
                }
×
330
        }
331
        return 0
×
332
}
333

334
func (n *stringNode) sortInputs() {
26✔
335
        slices.SortFunc(n.lefts, func(a, b uint64) int {
62✔
336
                return inputIndex(n.w, a) - inputIndex(n.w, b)
36✔
337
        })
36✔
338
        _ = n.w.SetNodePortOrder(n.id, "left", n.lefts)
26✔
339
}
340

341
func (n *stringNode) updateLabel() error {
72✔
342
        if n.w == nil || n.id == 0 {
82✔
343
                return nil
10✔
344
        }
10✔
345
        return n.w.SetNodeLabel(n.id, n.label())
62✔
346
}
347

348
func (n *stringNode) sendMenuSnapshot() {
10✔
349
        n.w.SendNodeMenuMsg(n.id, formular.MenuSnapshotMessage{
10✔
350
                MessageBase: formular.MessageBase{Type: formular.MessageMenuSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1},
10✔
351
                Blocks:      []formular.Block{n.menuBlock()},
10✔
352
        })
10✔
353
}
10✔
354

355
func (n *stringNode) sendMenuBlock() {
62✔
356
        if n.w == nil || n.id == 0 {
72✔
357
                return
10✔
358
        }
10✔
359
        n.w.SendNodeMenuMsg(n.id, formular.BlockSnapshotMessage{
52✔
360
                MessageBase: formular.MessageBase{Type: formular.MessageBlockSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1, BlockGeneration: 1},
52✔
361
                Block:       n.menuBlock(),
52✔
362
        })
52✔
363
}
364

365
func (n *stringNode) menuBlock() formular.Block {
62✔
366
        return formular.Block{
62✔
367
                ID: "state", Order: 10, Generation: 1,
62✔
368
                Items: []formular.Item{{
62✔
369
                        Type:  formular.ItemField,
62✔
370
                        ID:    "value",
62✔
371
                        Label: "Value",
62✔
372
                        Field: &formular.Field{Kind: menuFieldKind(n.outputType()), Value: n.menuValue(), Readonly: true},
62✔
373
                }},
62✔
374
        }
62✔
375
}
62✔
376

377
func stringInputPortForNode(node uint64, index int) pasta.Port {
7✔
378
        port := stringInputPort(index)
7✔
379
        port.Node = node
7✔
380
        return port
7✔
381
}
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc