• 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

71.56
/pasta/std/node_string_format.go
1
package std
2

3
import (
4
        "encoding/json"
5
        "fmt"
6
        "slices"
7
        "strconv"
8
        "strings"
9

10
        "github.com/asciimoth/configer/configer"
11
        "github.com/asciimoth/formular"
12
        "github.com/asciimoth/pasta/pasta"
13
)
14

15
// NodeTypeStringFormat is the class name for StringFormatClass.
16
const NodeTypeStringFormat = "pasta/StringFormat"
17

18
// StringFormatClass creates template-driven string formatting nodes.
19
type StringFormatClass struct{}
20

21
func (StringFormatClass) ClassName() string        { return NodeTypeStringFormat }
198✔
22
func (StringFormatClass) ShortDescription() string { return "Format string" }
179✔
23
func (StringFormatClass) LongDescription() string {
×
24
        return "Builds a string from text and typed placeholder parts. Placeholder parts create matching input ports."
×
25
}
×
26
func (StringFormatClass) DefaultNodeParams() pasta.NodeClassParams {
209✔
27
        return pasta.NodeClassParams{PrimaryType: TypeString, InitialPorts: []pasta.Port{rightPort(TypeString)}}
209✔
28
}
209✔
29
func (StringFormatClass) NewNode(cfg configer.Config, previous ...*pasta.NodeClassState) (pasta.Node, error) {
4✔
30
        parts := readFormatParts(cfg)
4✔
31
        if state := firstState(previous); state != nil {
6✔
32
                reconcileStringFormatState(state, parts)
2✔
33
        }
2✔
34
        return newStringFormatNode(parts), nil
4✔
35
}
36

37
type stringFormatPart struct {
38
        ID   string
39
        Kind string
40
        Text string
41
        Name string
42
        Type string
43
}
44

45
type stringFormatNode struct {
46
        pasta.BasicNode
47

48
        parts  []stringFormatPart
49
        inputs map[uint64]string
50
        value  string
51

52
        w     *pasta.Workspace
53
        id    uint64
54
        out   uint64
55
        lefts map[string]uint64
56
}
57

58
func newStringFormatNode(parts []stringFormatPart) *stringFormatNode {
4✔
59
        n := &stringFormatNode{parts: normalizeFormatParts(parts), inputs: map[uint64]string{}, lefts: map[string]uint64{}}
4✔
60
        n.recalculate(false)
4✔
61
        return n
4✔
62
}
4✔
63

64
func (n *stringFormatNode) OnInit(w *pasta.Workspace, _ pasta.Logger, id uint64, _ string, restored *pasta.NodeInitData, _, _, _, _ bool) error {
4✔
65
        n.w = w
4✔
66
        n.id = id
4✔
67
        n.lefts = map[string]uint64{}
4✔
68
        if restored != nil {
8✔
69
                if len(restored.RightPorts) > 0 {
8✔
70
                        n.out = restored.RightPorts[0]
4✔
71
                }
4✔
72
                n.refreshLefts()
4✔
73
        }
74
        if err := n.w.SetNodePrimary(n.id, TypeString); err != nil {
4✔
75
                return err
×
76
        }
×
77
        n.recalculate(false)
4✔
78
        if err := n.updatePorts(); err != nil {
4✔
79
                return err
×
80
        }
×
81
        if err := n.updateLabel(); err != nil {
4✔
82
                return err
×
83
        }
×
84
        n.sendMenuSnapshot()
4✔
85
        return nil
4✔
86
}
87

88
func (n *stringFormatNode) OnReady() error {
4✔
89
        n.requestAll()
4✔
90
        n.sendAll()
4✔
91
        return nil
4✔
92
}
4✔
93

94
func (n *stringFormatNode) PreLinkAdd(port uint64, linkType, portDirection string) error {
7✔
95
        if portDirection == "right" {
7✔
96
                if port == n.out && linkType == TypeString {
×
97
                        return nil
×
98
                }
×
NEW
99
                return pasta.LinkTypeErr(linkType)
×
100
        }
101
        if !stringFormatTypeSupported(linkType) {
7✔
NEW
102
                return pasta.LinkTypeErr(linkType)
×
103
        }
×
104
        snapshot, ok := n.w.PortSnapshot(port)
7✔
105
        if ok && len(snapshot.Links) > 0 {
7✔
106
                return pasta.ErrLinkDup
×
107
        }
×
108
        return nil
7✔
109
}
110

111
func (n *stringFormatNode) OnLinkAdd(link, port uint64, _ string, portDirection string) error {
7✔
112
        if portDirection == "left" {
14✔
113
                n.requestLink(link, port)
7✔
114
                return nil
7✔
115
        }
7✔
116
        if port == n.out {
×
117
                n.sendToLink(link)
×
118
        }
×
119
        return nil
×
120
}
121

122
func (n *stringFormatNode) OnLinkRemoved(_ uint64, port uint64, _ string, portDirection string) error {
×
123
        if _, ok := n.inputs[port]; ok {
×
124
                delete(n.inputs, port)
×
125
                n.recalculate(true)
×
126
        }
×
127
        return nil
×
128
}
129

130
func (n *stringFormatNode) OnPortAdd(port uint64, direction string, _ []string) error {
5✔
131
        if direction == "left" {
10✔
132
                n.refreshLefts()
5✔
133
        }
5✔
134
        return nil
5✔
135
}
136

137
func (n *stringFormatNode) OnPortRemoved(port uint64, direction string) error {
×
138
        if direction == "left" {
×
139
                delete(n.inputs, port)
×
140
                n.refreshLefts()
×
141
                n.recalculate(true)
×
142
        }
×
143
        return nil
×
144
}
145

146
func (n *stringFormatNode) OnEvent(event pasta.Event, linkType string, _ []string, receiverPortDirection string) error {
20✔
147
        if receiverPortDirection == "right" {
20✔
148
                if linkType == TypeString && isValueRequest(event.Payload) {
×
149
                        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: event.SenderNode, ReceiverPort: event.SenderPort, Payload: String(n.value)})
×
150
                }
×
151
                return nil
×
152
        }
153
        value, ok := stringFormatPayloadString(linkType, event.Payload)
20✔
154
        if !ok {
20✔
155
                return nil
×
156
        }
×
157
        n.inputs[event.ReceiverPort] = value
20✔
158
        n.recalculate(true)
20✔
159
        return nil
20✔
160
}
161

162
func (n *stringFormatNode) OnFormularMsg(message any) error {
3✔
163
        msg, ok := message.(formular.FieldUpdateMessage)
3✔
164
        if !ok || msg.MenuID != pasta.NodeMenuID(n.id) ||
3✔
165
                msg.Field.BlockID != "template" ||
3✔
166
                (msg.Field.FieldID != "parts" && msg.Field.FieldID != "type") {
3✔
167
                return nil
×
168
        }
×
169
        if msg.Field.FieldID == "parts" {
6✔
170
                parts, ok := parseFormatPartValue(msg.Value)
3✔
171
                if !ok {
3✔
NEW
172
                        return nil
×
NEW
173
                }
×
174
                parts = normalizeFormatParts(parts)
3✔
175
                if stringFormatPartsEqual(parts, n.parts) {
3✔
NEW
176
                        return nil
×
NEW
177
                }
×
178
                n.parts = parts
3✔
179
                if err := n.updatePorts(); err != nil {
3✔
NEW
180
                        return err
×
NEW
181
                }
×
182
                n.recalculate(true)
3✔
183
                n.sendMenuBlock()
3✔
184
                return nil
3✔
185
        }
NEW
186
        if msg.Field.FieldID == "type" {
×
NEW
187
                if len(msg.Field.ElementPath) < 1 {
×
NEW
188
                        return nil
×
NEW
189
                }
×
190

NEW
191
                if msg.Field.ElementPath[0].ArrayFieldID != "parts" {
×
NEW
192
                        return nil
×
NEW
193
                }
×
194

NEW
195
                elementID := msg.Field.ElementPath[0].ElementID
×
NEW
196

×
NEW
197
                newType, ok := msg.Value.(string)
×
NEW
198
                if !ok || !stringFormatTypeSupported(newType) {
×
NEW
199
                        return nil
×
NEW
200
                }
×
201

NEW
202
                for i := range n.parts {
×
NEW
203
                        if n.parts[i].ID == elementID && n.parts[i].Kind == "value" {
×
NEW
204
                                n.parts[i].Type = newType
×
NEW
205
                                n.parts = normalizeFormatParts(n.parts)
×
NEW
206
                                if err := n.updatePorts(); err != nil {
×
NEW
207
                                        return err
×
NEW
208
                                }
×
NEW
209
                                n.recalculate(true)
×
NEW
210
                                n.sendMenuBlock()
×
NEW
211
                                return nil
×
212
                        }
213
                }
214
        }
UNCOV
215
        return nil
×
216
}
217

218
func (n *stringFormatNode) OnSave(cfg configer.Config) error {
2✔
219
        if err := pasta.DeleteNodeOwnedConfigKeys(cfg); err != nil {
2✔
220
                return err
×
221
        }
×
222
        return cfg.Set(configer.Path{"template"}, formatPartsConfig(n.parts))
2✔
223
}
224

225
func (n *stringFormatNode) recalculate(broadcast bool) {
31✔
226
        old := n.value
31✔
227
        var b strings.Builder
31✔
228
        for _, part := range n.parts {
169✔
229
                if part.Kind == "text" {
207✔
230
                        b.WriteString(part.Text)
69✔
231
                        continue
69✔
232
                }
233
                b.WriteString(n.inputs[n.lefts[part.ID]])
69✔
234
        }
235
        n.value = b.String()
31✔
236
        _ = n.updateLabel()
31✔
237
        if broadcast && old != n.value {
40✔
238
                n.sendAll()
9✔
239
        }
9✔
240
}
241

242
func (n *stringFormatNode) updatePorts() error {
7✔
243
        desired := stringFormatDesiredPorts(n.parts)
7✔
244
        byName := map[string]uint64{}
7✔
245
        byPart := map[string]uint64{}
7✔
246
        for part, port := range n.lefts {
13✔
247
                if port > 0 {
12✔
248
                        byPart[part] = port
6✔
249
                }
6✔
250
        }
251
        current := []uint64{}
7✔
252
        snapshot, ok := n.w.NodeSnapshot(n.id)
7✔
253
        if ok {
14✔
254
                current = append([]uint64{}, snapshot.LeftPorts...)
7✔
255
                for _, port := range current {
13✔
256
                        if ps, ok := n.w.PortSnapshot(port); ok {
12✔
257
                                byName[ps.Name] = port
6✔
258
                        }
6✔
259
                }
260
        }
261

262
        keep := map[uint64]struct{}{}
7✔
263
        ordered := make([]uint64, 0, len(desired))
7✔
264
        for _, desired := range desired {
18✔
265
                port := desired.Port
11✔
266
                if existing := byPart[desired.PartID]; existing > 0 {
17✔
267
                        keep[existing] = struct{}{}
6✔
268
                        ordered = append(ordered, existing)
6✔
269
                        _ = n.w.SetPortName(existing, port.Name)
6✔
270
                        _ = n.w.SetPortTypes(existing, port.Types)
6✔
271
                        continue
6✔
272
                }
273
                if existing := byName[port.Name]; existing > 0 {
5✔
274
                        keep[existing] = struct{}{}
×
275
                        ordered = append(ordered, existing)
×
276
                        _ = n.w.SetPortTypes(existing, port.Types)
×
277
                        continue
×
278
                }
279
                port.Node = n.id
5✔
280
                id, err := n.w.AddPort(port)
5✔
281
                if err != nil {
5✔
282
                        return err
×
283
                }
×
284
                keep[id] = struct{}{}
5✔
285
                ordered = append(ordered, id)
5✔
286
        }
287
        for _, port := range current {
13✔
288
                if _, ok := keep[port]; !ok {
6✔
289
                        n.w.RemovePort(port)
×
290
                }
×
291
        }
292
        n.refreshLefts()
7✔
293
        if len(ordered) > 0 {
12✔
294
                if err := n.w.SetNodePortOrder(n.id, "left", ordered); err != nil {
5✔
295
                        return err
×
296
                }
×
297
        }
298
        n.requestAll()
7✔
299
        return nil
7✔
300
}
301

302
func (n *stringFormatNode) refreshLefts() {
16✔
303
        n.lefts = map[string]uint64{}
16✔
304
        if n.w == nil || n.id == 0 {
16✔
305
                return
×
306
        }
×
307
        snapshot, ok := n.w.NodeSnapshot(n.id)
16✔
308
        if !ok {
16✔
309
                return
×
310
        }
×
311
        byName := map[string]uint64{}
16✔
312
        for _, port := range snapshot.LeftPorts {
35✔
313
                if ps, ok := n.w.PortSnapshot(port); ok {
38✔
314
                        byName[ps.Name] = port
19✔
315
                }
19✔
316
        }
317
        for _, part := range n.parts {
76✔
318
                if part.Kind == "value" {
90✔
319
                        n.lefts[part.ID] = byName[part.Name]
30✔
320
                }
30✔
321
        }
322
}
323

324
func (n *stringFormatNode) requestAll() {
11✔
325
        for _, port := range n.lefts {
24✔
326
                snapshot, ok := n.w.PortSnapshot(port)
13✔
327
                if !ok {
13✔
328
                        continue
×
329
                }
330
                for _, link := range snapshot.Links {
18✔
331
                        n.requestLink(link, port)
5✔
332
                }
5✔
333
        }
334
}
335

336
func (n *stringFormatNode) requestLink(link, port uint64) {
12✔
337
        snapshot, ok := n.w.LinkSnapshot(link)
12✔
338
        if !ok {
12✔
339
                return
×
340
        }
×
341
        receiverNode, receiverPort := otherEndpoint(snapshot, port)
12✔
342
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: port, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: RequestValue{}})
12✔
343
}
344

345
func (n *stringFormatNode) sendAll() {
13✔
346
        port, ok := n.w.PortSnapshot(n.out)
13✔
347
        if !ok {
13✔
348
                return
×
349
        }
×
350
        for _, link := range port.Links {
13✔
351
                n.sendToLink(link)
×
352
        }
×
353
}
354

355
func (n *stringFormatNode) sendToLink(link uint64) {
×
356
        snapshot, ok := n.w.LinkSnapshot(link)
×
357
        if !ok {
×
358
                return
×
359
        }
×
360
        receiverNode, receiverPort := otherEndpoint(snapshot, n.out)
×
361
        n.w.SendEvent(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: String(n.value)})
×
362
}
363

364
func (n *stringFormatNode) updateLabel() error {
35✔
365
        if n.w == nil || n.id == 0 {
39✔
366
                return nil
4✔
367
        }
4✔
368
        return n.w.SetNodeLabel(n.id, n.value)
31✔
369
}
370

371
func (n *stringFormatNode) sendMenuSnapshot() {
4✔
372
        n.w.SendNodeMenuMsg(n.id, formular.MenuSnapshotMessage{
4✔
373
                MessageBase: formular.MessageBase{Type: formular.MessageMenuSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1},
4✔
374
                Blocks:      []formular.Block{n.menuBlock()},
4✔
375
        })
4✔
376
}
4✔
377

378
func (n *stringFormatNode) sendMenuBlock() {
3✔
379
        if n.w == nil || n.id == 0 {
3✔
380
                return
×
381
        }
×
382
        n.w.SendNodeMenuMsg(n.id, formular.BlockSnapshotMessage{
3✔
383
                MessageBase: formular.MessageBase{Type: formular.MessageBlockSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1, BlockGeneration: 1},
3✔
384
                Block:       n.menuBlock(),
3✔
385
        })
3✔
386
}
387

388
func (n *stringFormatNode) menuBlock() formular.Block {
7✔
389
        return formular.Block{
7✔
390
                ID: "template", Order: 10, Generation: 1,
7✔
391
                Items: []formular.Item{{
7✔
392
                        Type:  formular.ItemField,
7✔
393
                        ID:    "parts",
7✔
394
                        Label: "Template",
7✔
395
                        Field: &formular.Field{
7✔
396
                                Kind:      formular.FieldArray,
7✔
397
                                Value:     formatPartsArrayValue(n.parts),
7✔
398
                                Templates: formatPartTemplates(),
7✔
399
                                Elements:  formatPartElements(n.parts),
7✔
400
                        },
7✔
401
                }},
7✔
402
        }
7✔
403
}
7✔
404

405
func reconcileStringFormatState(state *pasta.NodeClassState, parts []stringFormatPart) {
2✔
406
        state.PrimaryType = TypeString
2✔
407
        state.RightPorts = []pasta.Port{rightPort(TypeString)}
2✔
408
        previous := map[string]pasta.Port{}
2✔
409
        for _, port := range state.LeftPorts {
4✔
410
                previous[port.Name] = port
2✔
411
        }
2✔
412
        state.LeftPorts = nil
2✔
413
        for _, desired := range stringFormatDesiredPorts(parts) {
4✔
414
                port := desired.Port
2✔
415
                if kept, ok := previous[port.Name]; ok {
4✔
416
                        kept.Direction = "left"
2✔
417
                        kept.Types = slices.Clone(port.Types)
2✔
418
                        state.LeftPorts = append(state.LeftPorts, kept)
2✔
419
                        continue
2✔
420
                }
421
                state.LeftPorts = append(state.LeftPorts, port)
×
422
        }
423
}
424

425
type stringFormatDesiredPort struct {
426
        PartID string
427
        Port   pasta.Port
428
}
429

430
func stringFormatDesiredPorts(parts []stringFormatPart) []stringFormatDesiredPort {
9✔
431
        ports := []stringFormatDesiredPort{}
9✔
432
        seen := map[string]struct{}{}
9✔
433
        for _, part := range normalizeFormatParts(parts) {
35✔
434
                if part.Kind != "value" {
39✔
435
                        continue
13✔
436
                }
437
                name := uniquePortName(part.Name, seen)
13✔
438
                ports = append(ports, stringFormatDesiredPort{
13✔
439
                        PartID: part.ID,
13✔
440
                        Port:   pasta.Port{Direction: "left", Name: name, Types: []string{part.Type}},
13✔
441
                })
13✔
442
                seen[name] = struct{}{}
13✔
443
        }
444
        return ports
9✔
445
}
446

447
func uniquePortName(name string, seen map[string]struct{}) string {
37✔
448
        base := sanitizePortName(name)
37✔
449
        if _, ok := seen[base]; !ok {
74✔
450
                return base
37✔
451
        }
37✔
452
        for i := 2; ; i++ {
×
453
                next := fmt.Sprintf("%s %d", base, i)
×
454
                if _, ok := seen[next]; !ok {
×
455
                        return next
×
456
                }
×
457
        }
458
}
459

460
func sanitizePortName(name string) string {
37✔
461
        name = strings.TrimSpace(name)
37✔
462
        var b strings.Builder
37✔
463
        space := false
37✔
464
        for i := 0; i < len(name); i++ {
191✔
465
                c := name[i]
154✔
466
                ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == ' ' || c == '\t'
154✔
467
                if !ok {
154✔
468
                        continue
×
469
                }
470
                if c == '\t' {
154✔
471
                        c = ' '
×
472
                }
×
473
                if c == ' ' {
154✔
474
                        if space {
×
475
                                continue
×
476
                        }
477
                        space = true
×
478
                } else {
154✔
479
                        space = false
154✔
480
                }
154✔
481
                b.WriteByte(c)
154✔
482
        }
483
        cleaned := strings.Trim(b.String(), " -_\t\r\n")
37✔
484
        if pasta.ValidatePortName(cleaned) == nil {
74✔
485
                return cleaned
37✔
486
        }
37✔
487
        return "Value"
×
488
}
489

490
func normalizeFormatParts(parts []stringFormatPart) []stringFormatPart {
16✔
491
        normalized := make([]stringFormatPart, 0, len(parts))
16✔
492
        seenIDs := map[string]struct{}{}
16✔
493
        seenPorts := map[string]struct{}{}
16✔
494
        for i, part := range parts {
64✔
495
                if part.Kind != "value" {
72✔
496
                        part.Kind = "text"
24✔
497
                }
24✔
498
                if part.ID == "" {
48✔
499
                        part.ID = part.Kind + "-" + strconv.Itoa(i+1)
×
500
                }
×
501
                if _, ok := seenIDs[part.ID]; ok {
48✔
502
                        part.ID = part.Kind + "-" + strconv.Itoa(i+1)
×
503
                }
×
504
                seenIDs[part.ID] = struct{}{}
48✔
505
                if part.Kind == "value" {
72✔
506
                        if !stringFormatTypeSupported(part.Type) {
24✔
507
                                part.Type = TypeString
×
508
                        }
×
509
                        part.Name = uniquePortName(part.Name, seenPorts)
24✔
510
                        seenPorts[part.Name] = struct{}{}
24✔
511
                }
512
                normalized = append(normalized, part)
48✔
513
        }
514
        return normalized
16✔
515
}
516

517
func stringFormatTypeSupported(typ string) bool {
31✔
518
        return typ == TypeString || typ == TypeInt || typ == TypeFloat || typ == TypeBool
31✔
519
}
31✔
520

521
func stringFormatPayloadString(linkType string, payload any) (string, bool) {
20✔
522
        switch linkType {
20✔
523
        case TypeString:
11✔
524
                return parseStringAny(payload)
11✔
525
        case TypeInt:
3✔
526
                v, ok := valueFromPayload(TypeInt, payload)
3✔
527
                if !ok {
3✔
528
                        return "", false
×
529
                }
×
530
                return v.label(), true
3✔
531
        case TypeFloat:
3✔
532
                v, ok := valueFromPayload(TypeFloat, payload)
3✔
533
                if !ok {
3✔
534
                        return "", false
×
535
                }
×
536
                return v.label(), true
3✔
537
        case TypeBool:
3✔
538
                v, ok := parseBoolAny(payload)
3✔
539
                if !ok {
3✔
540
                        return "", false
×
541
                }
×
542
                return boolLabel(v), true
3✔
543
        default:
×
544
                return "", false
×
545
        }
546
}
547

548
func readFormatParts(cfg configer.Config) []stringFormatPart {
4✔
549
        if cfg == nil {
6✔
550
                return nil
2✔
551
        }
2✔
552
        raw, err := cfg.Get(configer.Path{"template"})
2✔
553
        if err != nil {
2✔
554
                return nil
×
555
        }
×
556
        parts, _ := parseFormatPartValue(raw)
2✔
557
        return parts
2✔
558
}
559

560
func parseFormatPartValue(value any) ([]stringFormatPart, bool) {
5✔
561
        switch v := value.(type) {
5✔
562
        case []formular.ArrayElementValue:
3✔
563
                parts := make([]stringFormatPart, 0, len(v))
3✔
564
                for _, element := range v {
21✔
565
                        parts = append(parts, formatPartFromValues(element.ID, element.Template, element.Values))
18✔
566
                }
18✔
567
                return parts, true
3✔
568
        case []any:
2✔
569
                parts := make([]stringFormatPart, 0, len(v))
2✔
570
                for _, item := range v {
6✔
571
                        element, ok := parseArrayElementMap(item)
4✔
572
                        if !ok {
4✔
573
                                continue
×
574
                        }
575
                        parts = append(parts, formatPartFromValues(element.ID, element.Template, element.Values))
4✔
576
                }
577
                return parts, true
2✔
578
        default:
×
579
                return nil, false
×
580
        }
581
}
582

583
func parseArrayElementMap(value any) (formular.ArrayElementValue, bool) {
4✔
584
        switch v := value.(type) {
4✔
585
        case formular.ArrayElementValue:
×
586
                return v, true
×
587
        case map[string]any:
4✔
588
                id, _ := parseStringAny(v["id"])
4✔
589
                template, _ := parseStringAny(v["template"])
4✔
590
                values, _ := v["values"].(map[string]any)
4✔
591
                return formular.ArrayElementValue{ID: id, Template: template, Values: values}, true
4✔
592
        default:
×
593
                data, err := json.Marshal(value)
×
594
                if err != nil {
×
595
                        return formular.ArrayElementValue{}, false
×
596
                }
×
597
                var element formular.ArrayElementValue
×
598
                if err := json.Unmarshal(data, &element); err != nil {
×
599
                        return formular.ArrayElementValue{}, false
×
600
                }
×
601
                return element, true
×
602
        }
603
}
604

605
func formatPartFromValues(id, template string, values map[string]any) stringFormatPart {
22✔
606
        if template == "value" {
33✔
607
                name, _ := parseStringAny(values["name"])
11✔
608
                typ, _ := parseStringAny(values["type"])
11✔
609
                return stringFormatPart{ID: id, Kind: "value", Name: name, Type: typ}
11✔
610
        }
11✔
611
        text, _ := parseStringAny(values["text"])
11✔
612
        return stringFormatPart{ID: id, Kind: "text", Text: text}
11✔
613
}
614

615
func formatPartsConfig(parts []stringFormatPart) []any {
2✔
616
        values := formatPartsArrayValue(parts)
2✔
617
        out := make([]any, 0, len(values))
2✔
618
        for _, value := range values {
6✔
619
                out = append(out, map[string]any{"id": value.ID, "template": value.Template, "values": value.Values})
4✔
620
        }
4✔
621
        return out
2✔
622
}
623

624
func formatPartsArrayValue(parts []stringFormatPart) []formular.ArrayElementValue {
9✔
625
        values := make([]formular.ArrayElementValue, 0, len(parts))
9✔
626
        for _, part := range parts {
35✔
627
                element := formular.ArrayElementValue{ID: part.ID, Template: part.Kind, Values: map[string]any{}}
26✔
628
                if part.Kind == "value" {
39✔
629
                        element.Values["name"] = part.Name
13✔
630
                        element.Values["type"] = part.Type
13✔
631
                } else {
26✔
632
                        element.Values["text"] = part.Text
13✔
633
                }
13✔
634
                values = append(values, element)
26✔
635
        }
636
        return values
9✔
637
}
638

639
func formatPartTemplates() []formular.ArrayTemplate {
7✔
640
        return []formular.ArrayTemplate{
7✔
641
                {
7✔
642
                        Name:  "text",
7✔
643
                        Label: "Text",
7✔
644
                        Items: []formular.Item{{Type: formular.ItemField, ID: "text", Label: "Text", Field: &formular.Field{Kind: formular.FieldText, Multiline: true}}},
7✔
645
                },
7✔
646
                {
7✔
647
                        Name:  "value",
7✔
648
                        Label: "Value",
7✔
649
                        Items: []formular.Item{
7✔
650
                                {Type: formular.ItemField, ID: "name", Label: "Name", Field: &formular.Field{Kind: formular.FieldText, Value: "Value"}},
7✔
651
                                {Type: formular.ItemField, ID: "type", Label: "Type", Field: &formular.Field{Kind: formular.FieldRadio, Value: TypeString, AllowedValues: []any{TypeString, TypeInt, TypeFloat, TypeBool}}},
7✔
652
                        },
7✔
653
                },
7✔
654
        }
7✔
655
}
7✔
656

657
func formatPartElements(parts []stringFormatPart) []formular.ArrayElement {
7✔
658
        elements := make([]formular.ArrayElement, 0, len(parts))
7✔
659
        for _, part := range parts {
29✔
660
                element := formular.ArrayElement{ID: part.ID, Template: part.Kind}
22✔
661
                if part.Kind == "value" {
33✔
662
                        element.Items = []formular.Item{
11✔
663
                                {Type: formular.ItemField, ID: "name", Label: "Name", Field: &formular.Field{Kind: formular.FieldText, Value: part.Name}},
11✔
664
                                {Type: formular.ItemField, ID: "type", Label: "Type", Field: &formular.Field{Kind: formular.FieldRadio, Value: part.Type, AllowedValues: []any{TypeString, TypeInt, TypeFloat, TypeBool}}},
11✔
665
                        }
11✔
666
                } else {
22✔
667
                        element.Items = []formular.Item{{Type: formular.ItemField, ID: "text", Label: "Text", Field: &formular.Field{Kind: formular.FieldText, Value: part.Text, Multiline: true}}}
11✔
668
                }
11✔
669
                elements = append(elements, element)
22✔
670
        }
671
        return elements
7✔
672
}
673

674
func stringFormatPartsEqual(a, b []stringFormatPart) bool {
3✔
675
        if len(a) != len(b) {
5✔
676
                return false
2✔
677
        }
2✔
678
        for i := range a {
3✔
679
                if a[i] != b[i] {
3✔
680
                        return false
1✔
681
                }
1✔
682
        }
683
        return true
×
684
}
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