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

asciimoth / pasta / 29277180437

13 Jul 2026 07:06PM UTC coverage: 77.33% (+1.8%) from 75.565%
29277180437

push

github

asciimoth
feat(std): add object type and related nodes

1882 of 2483 new or added lines in 11 files covered. (75.8%)

30 existing lines in 2 files now uncovered.

8173 of 10569 relevant lines covered (77.33%)

400.18 hits per line

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

79.69
/pasta/std/node_object_to_string.go
1
package std
2

3
import (
4
        "bytes"
5
        "encoding/json"
6

7
        "github.com/asciimoth/configer/configer"
8
        "github.com/asciimoth/formular"
9
        "github.com/asciimoth/pasta/pasta"
10
        "github.com/asciimoth/persist"
11
)
12

13
// NodeTypeObjectToString is the class name for ObjectToStringClass.
14
const NodeTypeObjectToString = "pasta/ObjectToString"
15

16
// ObjectToStringClass creates nodes that stringify pasta/object values.
17
type ObjectToStringClass struct{}
18

19
func (ObjectToStringClass) ClassName() string        { return NodeTypeObjectToString }
361✔
20
func (ObjectToStringClass) ShortDescription() string { return "Object to string" }
325✔
NEW
21
func (ObjectToStringClass) LongDescription() string {
×
NEW
22
        return "Formats one pasta/object input as compact or pretty JSON on a pasta/string output."
×
NEW
23
}
×
24
func (ObjectToStringClass) DefaultNodeParams() pasta.NodeClassParams {
384✔
25
        return pasta.NodeClassParams{PrimaryType: TypeString, InitialPorts: []pasta.Port{
384✔
26
                rightPort(TypeString),
384✔
27
                {Direction: "left", Name: "input", Types: []string{TypeObject}},
384✔
28
        }}
384✔
29
}
384✔
30
func (ObjectToStringClass) NewNode(cfg configer.Config, _ ...*pasta.NodeClassState) (pasta.Node, error) {
8✔
31
        return newObjectToStringNode(readBoolAt(cfg, "pretty", false), readBoolAt(cfg, "omit_empty", false)), nil
8✔
32
}
8✔
33

34
type objectToStringNode struct {
35
        pasta.BasicNode
36

37
        pretty    bool
38
        omitEmpty bool
39
        input     Object
40
        value     string
41

42
        w   *pasta.Workspace
43
        id  uint64
44
        in  uint64
45
        out uint64
46
}
47

48
func newObjectToStringNode(pretty, omitEmpty bool) *objectToStringNode {
8✔
49
        n := &objectToStringNode{pretty: pretty, omitEmpty: omitEmpty, input: NilObject()}
8✔
50
        n.recalculate(false)
8✔
51
        return n
8✔
52
}
8✔
53

54
func (n *objectToStringNode) OnInit(w *pasta.Workspace, _ pasta.Logger, id uint64, _ string, restored *pasta.NodeInitData, _, _, _, _ bool) error {
8✔
55
        n.w = w
8✔
56
        n.id = id
8✔
57
        if restored != nil {
16✔
58
                if len(restored.LeftPorts) > 0 {
16✔
59
                        n.in = restored.LeftPorts[0]
8✔
60
                }
8✔
61
                if len(restored.RightPorts) > 0 {
16✔
62
                        n.out = restored.RightPorts[0]
8✔
63
                }
8✔
64
        }
65
        if err := n.w.SetNodePrimaryLocked(n.id, TypeString); err != nil {
8✔
NEW
66
                return err
×
NEW
67
        }
×
68
        n.recalculate(false)
8✔
69
        if err := n.clearLabel(); err != nil {
8✔
NEW
70
                return err
×
NEW
71
        }
×
72
        n.sendMenuSnapshot(false)
8✔
73
        return nil
8✔
74
}
75

76
func (n *objectToStringNode) OnReady() error {
8✔
77
        n.requestInput()
8✔
78
        n.sendAll()
8✔
79
        return nil
8✔
80
}
8✔
81

82
func (n *objectToStringNode) PreLinkAdd(port uint64, linkType, portDirection string) error {
9✔
83
        if portDirection == "left" {
17✔
84
                if port != n.in || linkType != TypeObject {
8✔
NEW
85
                        return pasta.LinkTypeErr(linkType)
×
NEW
86
                }
×
87
                snapshot, ok := n.w.PortSnapshotLocked(port)
8✔
88
                if ok && len(snapshot.Links) > 0 {
8✔
NEW
89
                        return pasta.ErrLinkDup
×
NEW
90
                }
×
91
                return nil
8✔
92
        }
93
        if port == n.out && linkType == TypeString {
2✔
94
                return nil
1✔
95
        }
1✔
NEW
96
        return pasta.LinkTypeErr(linkType)
×
97
}
98

99
func (n *objectToStringNode) OnLinkAdd(link, port uint64, _, portDirection string) error {
9✔
100
        if portDirection == "left" {
17✔
101
                n.requestLink(link, port)
8✔
102
                return nil
8✔
103
        }
8✔
104
        if port == n.out {
2✔
105
                n.sendToLink(link)
1✔
106
        }
1✔
107
        return nil
1✔
108
}
109

NEW
110
func (n *objectToStringNode) OnLinkRemoved(_ uint64, port uint64, _ string, portDirection string) error {
×
NEW
111
        if portDirection == "left" && port == n.in {
×
NEW
112
                n.input = NilObject()
×
NEW
113
                n.recalculate(true)
×
NEW
114
        }
×
NEW
115
        return nil
×
116
}
117

118
func (n *objectToStringNode) OnEvent(event pasta.Event, linkType string, _ []string, receiverPortDirection string) error {
28✔
119
        if receiverPortDirection == "right" {
29✔
120
                if event.ReceiverPort == n.out && linkType == TypeString && isValueRequest(event.Payload) {
2✔
121
                        n.w.SendEventLocked(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: event.SenderNode, ReceiverPort: event.SenderPort, Payload: String(n.value)})
1✔
122
                }
1✔
123
                return nil
1✔
124
        }
125
        if event.ReceiverPort != n.in || linkType != TypeObject {
27✔
NEW
126
                return nil
×
NEW
127
        }
×
128
        object, ok := ObjectFromPayload(event.Payload)
27✔
129
        if !ok {
27✔
NEW
130
                return nil
×
NEW
131
        }
×
132
        n.input = object
27✔
133
        n.recalculate(true)
27✔
134
        return nil
27✔
135
}
136

137
func (n *objectToStringNode) OnFormularMsg(message any) error {
2✔
138
        msg, ok := message.(formular.FieldUpdateMessage)
2✔
139
        if !ok || msg.MenuID != pasta.NodeMenuID(n.id) || msg.Field.BlockID != "state" {
2✔
NEW
140
                return nil
×
NEW
141
        }
×
142
        changed := false
2✔
143
        switch msg.Field.FieldID {
2✔
144
        case "pretty":
1✔
145
                value, ok := parseBoolAny(msg.Value)
1✔
146
                if ok && value != n.pretty {
2✔
147
                        n.pretty = value
1✔
148
                        changed = true
1✔
149
                }
1✔
150
        case "omit_empty":
1✔
151
                value, ok := parseBoolAny(msg.Value)
1✔
152
                if ok && value != n.omitEmpty {
2✔
153
                        n.omitEmpty = value
1✔
154
                        changed = true
1✔
155
                }
1✔
156
        }
157
        if !changed {
2✔
NEW
158
                return nil
×
NEW
159
        }
×
160
        n.recalculate(true)
2✔
161
        n.sendMenuBlock()
2✔
162
        return nil
2✔
163
}
164

165
func (n *objectToStringNode) OnSave(cfg configer.Config) error {
3✔
166
        if err := pasta.DeleteNodeOwnedConfigKeys(cfg); err != nil {
3✔
NEW
167
                return err
×
NEW
168
        }
×
169
        if err := cfg.Set(configer.Path{"pretty"}, n.pretty); err != nil {
3✔
NEW
170
                return err
×
NEW
171
        }
×
172
        return cfg.Set(configer.Path{"omit_empty"}, n.omitEmpty)
3✔
173
}
174

175
func (n *objectToStringNode) recalculate(broadcast bool) {
45✔
176
        old := n.value
45✔
177
        n.value = formatObjectString(n.input, n.pretty, n.omitEmpty)
45✔
178
        _ = n.clearLabel()
45✔
179
        n.sendMenuBlock()
45✔
180
        if broadcast && old != n.value {
62✔
181
                n.sendAll()
17✔
182
        }
17✔
183
}
184

185
func (n *objectToStringNode) clearLabel() error {
53✔
186
        if n.w == nil || n.id == 0 {
61✔
187
                return nil
8✔
188
        }
8✔
189
        return n.w.SetNodeLabelLocked(n.id, "")
45✔
190
}
191

192
func (n *objectToStringNode) requestInput() {
8✔
193
        snapshot, ok := n.w.PortSnapshotLocked(n.in)
8✔
194
        if !ok {
8✔
NEW
195
                return
×
NEW
196
        }
×
197
        for _, link := range snapshot.Links {
10✔
198
                n.requestLink(link, n.in)
2✔
199
        }
2✔
200
}
201

202
func (n *objectToStringNode) requestLink(link, port uint64) {
10✔
203
        snapshot, ok := n.w.LinkSnapshotLocked(link)
10✔
204
        if !ok {
10✔
NEW
205
                return
×
NEW
206
        }
×
207
        receiverNode, receiverPort := otherEndpoint(snapshot, port)
10✔
208
        n.w.SendEventLocked(pasta.Event{SenderNode: n.id, SenderPort: port, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: RequestValue{}})
10✔
209
}
210

211
func (n *objectToStringNode) sendAll() {
25✔
212
        port, ok := n.w.PortSnapshotLocked(n.out)
25✔
213
        if !ok {
25✔
NEW
214
                return
×
NEW
215
        }
×
216
        for _, link := range port.Links {
27✔
217
                n.sendToLink(link)
2✔
218
        }
2✔
219
}
220

221
func (n *objectToStringNode) sendToLink(link uint64) {
3✔
222
        snapshot, ok := n.w.LinkSnapshotLocked(link)
3✔
223
        if !ok {
3✔
NEW
224
                return
×
NEW
225
        }
×
226
        receiverNode, receiverPort := otherEndpoint(snapshot, n.out)
3✔
227
        n.w.SendEventLocked(pasta.Event{SenderNode: n.id, SenderPort: n.out, ReceiverNode: receiverNode, ReceiverPort: receiverPort, Payload: String(n.value)})
3✔
228
}
229

230
func (n *objectToStringNode) sendMenuSnapshot(force bool) {
8✔
231
        if n.w == nil || n.id == 0 {
8✔
NEW
232
                return
×
NEW
233
        }
×
234
        n.w.SendNodeMenuMsgLocked(n.id, formular.MenuSnapshotMessage{
8✔
235
                MessageBase: formular.MessageBase{Type: formular.MessageMenuSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1},
8✔
236
                Force:       force,
8✔
237
                Blocks:      []formular.Block{n.menuBlock()},
8✔
238
        })
8✔
239
}
240

241
func (n *objectToStringNode) sendMenuBlock() {
47✔
242
        if n.w == nil || n.id == 0 {
55✔
243
                return
8✔
244
        }
8✔
245
        n.w.SendNodeMenuMsgLocked(n.id, formular.BlockSnapshotMessage{
39✔
246
                MessageBase: formular.MessageBase{Type: formular.MessageBlockSnapshot, MenuID: pasta.NodeMenuID(n.id), MenuGeneration: 1, BlockGeneration: 1},
39✔
247
                Block:       n.menuBlock(),
39✔
248
        })
39✔
249
}
250

251
func (n *objectToStringNode) menuBlock() formular.Block {
47✔
252
        return formular.Block{
47✔
253
                ID: "state", Order: 10, Generation: 1,
47✔
254
                Items: []formular.Item{
47✔
255
                        {Type: formular.ItemField, ID: "pretty", Label: "Pretty print", Field: &formular.Field{Kind: formular.FieldCheckbox, Value: n.pretty}},
47✔
256
                        {Type: formular.ItemField, ID: "omit_empty", Label: "Omit empty/nil", Field: &formular.Field{Kind: formular.FieldCheckbox, Value: n.omitEmpty}},
47✔
257
                        {Type: formular.ItemField, ID: "value", Label: "Result", Field: &formular.Field{Kind: formular.FieldText, Value: n.value, Readonly: true, Multiline: true, Placeholder: `{"name":"Ada"}`}},
47✔
258
                },
47✔
259
        }
47✔
260
}
47✔
261

262
func formatObjectString(object Object, pretty, omitEmpty bool) string {
45✔
263
        value := object.Value()
45✔
264
        if omitEmpty {
46✔
265
                if omitted, keep := omitEmptyObjectValue(value); keep {
2✔
266
                        value = omitted
1✔
267
                } else {
1✔
NEW
268
                        value = persist.Nil()
×
NEW
269
                }
×
270
        }
271
        data, err := value.ToJSON()
45✔
272
        if err != nil {
45✔
NEW
273
                return "null"
×
NEW
274
        }
×
275
        if !pretty {
88✔
276
                var out bytes.Buffer
43✔
277
                if err := json.Compact(&out, data); err == nil {
86✔
278
                        return out.String()
43✔
279
                }
43✔
NEW
280
                return string(data)
×
281
        }
282
        var out bytes.Buffer
2✔
283
        if err := json.Indent(&out, data, "", "  "); err != nil {
2✔
NEW
284
                return string(data)
×
NEW
285
        }
×
286
        return out.String()
2✔
287
}
288

289
func omitEmptyObjectValue(value persist.Value) (persist.Value, bool) {
10✔
290
        switch value.Kind() {
10✔
291
        case persist.KindNil:
2✔
292
                return persist.Nil(), false
2✔
293
        case persist.KindMap:
4✔
294
                m, _ := value.Map()
4✔
295
                out := persist.NewMap()
4✔
296
                m.Range(func(k persist.Key, v persist.Value) bool {
9✔
297
                        if kept, ok := omitEmptyObjectValue(v); ok {
8✔
298
                                out = out.Assoc(k, kept)
3✔
299
                        }
3✔
300
                        return true
5✔
301
                })
302
                if out.Len() == 0 {
6✔
303
                        return persist.Nil(), false
2✔
304
                }
2✔
305
                return persist.MapValue(out), true
2✔
306
        case persist.KindVector:
1✔
307
                v, _ := value.Vector()
1✔
308
                out := make([]persist.Value, 0, v.Len())
1✔
309
                v.Range(func(_ int, item persist.Value) bool {
5✔
310
                        if kept, ok := omitEmptyObjectValue(item); ok {
6✔
311
                                out = append(out, kept)
2✔
312
                        }
2✔
313
                        return true
4✔
314
                })
315
                if len(out) == 0 {
1✔
NEW
316
                        return persist.Nil(), false
×
NEW
317
                }
×
318
                return persist.VectorValue(persist.NewVector(out...)), true
1✔
319
        default:
3✔
320
                return value, true
3✔
321
        }
322
}
323

324
func readBoolAt(cfg configer.Config, key string, fallback bool) bool {
16✔
325
        if cfg == nil {
26✔
326
                return fallback
10✔
327
        }
10✔
328
        raw, err := cfg.Get(configer.Path{key})
6✔
329
        if err != nil {
6✔
NEW
330
                return fallback
×
NEW
331
        }
×
332
        if value, ok := parseBoolAny(raw); ok {
12✔
333
                return value
6✔
334
        }
6✔
NEW
335
        return fallback
×
336
}
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