+1,
-0
1@@ -1 +1,2 @@
2 examples/load_document
3+docs/
M
array.go
+153,
-0
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "encoding/json"
6 "fmt"
7 "runtime"
8 "unsafe"
9@@ -170,3 +171,155 @@ func (it *ArrayIter) Next() (*Output, error) {
10 func (a *Array) Branch() unsafe.Pointer {
11 return unsafe.Pointer(a.branch)
12 }
13+
14+// MarshalJSONWithTxn implements JSON marshaling for Array.
15+// Returns a JSON array with all elements marshaled to JSON.
16+func (a *Array) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
17+ if a.branch == nil {
18+ return nil, ErrNilBranch
19+ }
20+ if txn == nil || txn.ptr == nil {
21+ return nil, ErrNilTransaction
22+ }
23+
24+ // Collect all elements
25+ var elements []interface{}
26+
27+ iter, err := a.Iter(txn)
28+ if err != nil {
29+ return nil, err
30+ }
31+ defer iter.Destroy()
32+
33+ for {
34+ out, err := iter.Next()
35+ if err != nil {
36+ return nil, err
37+ }
38+ if out == nil {
39+ break
40+ }
41+
42+ // Convert value to JSON-compatible type
43+ val, err := a.valueToJSON(out, txn)
44+ if err != nil {
45+ out.Destroy()
46+ return nil, err
47+ }
48+
49+ elements = append(elements, val)
50+ out.Destroy()
51+ }
52+
53+ return json.Marshal(elements)
54+}
55+
56+// valueToJSON converts an Output value to a JSON-compatible Go value.
57+// For primitive types, uses Output methods. For shared types, recursively marshals.
58+func (a *Array) valueToJSON(out *Output, txn *Transaction) (interface{}, error) {
59+ if out == nil || out.ptr == nil {
60+ return nil, nil
61+ }
62+
63+ switch out.Tag() {
64+ // Primitive JSON types - use Output methods
65+ case TagJSONNull:
66+ return nil, nil
67+ case TagJSONBool:
68+ if v, ok := out.Bool(); ok {
69+ return v, nil
70+ }
71+ return nil, fmt.Errorf("failed to read boolean")
72+ case TagJSONNum:
73+ if v, ok := out.Float(); ok {
74+ return v, nil
75+ }
76+ return nil, fmt.Errorf("failed to read float")
77+ case TagJSONInt:
78+ if v, ok := out.Int(); ok {
79+ return v, nil
80+ }
81+ return nil, fmt.Errorf("failed to read int")
82+ case TagJSONStr:
83+ if v, ok := out.String(); ok {
84+ return v, nil
85+ }
86+ return nil, fmt.Errorf("failed to read string")
87+ case TagJSONBuf:
88+ if v, ok := out.Binary(); ok {
89+ return v, nil
90+ }
91+ return nil, fmt.Errorf("failed to read binary")
92+
93+ // Shared types - recursively marshal
94+ case TagMap:
95+ branch := C.youtput_read_ymap(out.ptr)
96+ if branch == nil {
97+ return nil, fmt.Errorf("failed to read ymap from output")
98+ }
99+ m := &Map{branch: branch}
100+ // Create a temporary map with just the branch pointer
101+ // We need to get data from it
102+ data := make(map[string]interface{})
103+ iter, err := m.Iter(txn)
104+ if err != nil {
105+ return nil, err
106+ }
107+ defer iter.Destroy()
108+ for {
109+ entry, err := iter.Next()
110+ if err != nil {
111+ return nil, err
112+ }
113+ if entry == nil {
114+ break
115+ }
116+ val, err := m.valueToJSON(entry.Value, txn)
117+ if err != nil {
118+ entry.Value.Destroy()
119+ return nil, fmt.Errorf("key %q: %w", entry.Key, err)
120+ }
121+ data[entry.Key] = val
122+ entry.Value.Destroy()
123+ }
124+ return data, nil
125+ case TagArray:
126+ branch := C.youtput_read_yarray(out.ptr)
127+ if branch == nil {
128+ return nil, fmt.Errorf("failed to read yarray from output")
129+ }
130+ arr := &Array{branch: branch}
131+ var elements []interface{}
132+ iter, err := arr.Iter(txn)
133+ if err != nil {
134+ return nil, err
135+ }
136+ defer iter.Destroy()
137+ for {
138+ el, err := iter.Next()
139+ if err != nil {
140+ return nil, err
141+ }
142+ if el == nil {
143+ break
144+ }
145+ val, err := arr.valueToJSON(el, txn)
146+ if err != nil {
147+ el.Destroy()
148+ return nil, err
149+ }
150+ elements = append(elements, val)
151+ el.Destroy()
152+ }
153+ return elements, nil
154+ case TagText, TagXmlElem, TagXmlText, TagXmlFrag:
155+ // XML types not supported in array context
156+ return nil, fmt.Errorf("XML types not supported in array JSON marshaling: %v", out.Tag())
157+ case TagDoc:
158+ return nil, fmt.Errorf("nested documents not supported in JSON marshaling")
159+ case TagWeakLink, TagUndefined, TagJSONUndef:
160+ return nil, nil
161+ default:
162+ return nil, fmt.Errorf("unsupported type tag: %v", out.Tag())
163+ }
164+}
+219,
-1
1@@ -7,6 +7,7 @@ package ygo
2 import "C"
3 import (
4 "encoding"
5+ "encoding/json"
6 "fmt"
7 "runtime"
8 "unsafe"
9@@ -18,6 +19,56 @@ var (
10 _ encoding.BinaryUnmarshaler = (*Doc)(nil)
11 )
12
13+// MarshalOption configures Document JSON marshaling.
14+type MarshalOption func(*marshalConfig)
15+
16+type marshalConfig struct {
17+ roots []rootSpec
18+}
19+
20+type rootSpec struct {
21+ name string
22+ typ RootType
23+}
24+
25+// RootType identifies the type of shared root to marshal.
26+type RootType int
27+
28+const (
29+ RootTypeMap RootType = iota
30+ RootTypeArray
31+ RootTypeXmlFragment
32+ RootTypeText
33+)
34+
35+// WithMap includes the named Map in the marshaled output.
36+func WithMap(name string) MarshalOption {
37+ return func(cfg *marshalConfig) {
38+ cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeMap})
39+ }
40+}
41+
42+// WithArray includes the named Array in the marshaled output.
43+func WithArray(name string) MarshalOption {
44+ return func(cfg *marshalConfig) {
45+ cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeArray})
46+ }
47+}
48+
49+// WithXmlFragment includes the named XmlFragment in the marshaled output.
50+func WithXmlFragment(name string) MarshalOption {
51+ return func(cfg *marshalConfig) {
52+ cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeXmlFragment})
53+ }
54+}
55+
56+// WithText includes the named Text in the marshaled output.
57+func WithText(name string) MarshalOption {
58+ return func(cfg *marshalConfig) {
59+ cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeText})
60+ }
61+}
62+
63 // Doc represents a Yjs document - the core unit of collaborative resources.
64 // All shared collections live within a document scope.
65 type Doc struct {
66@@ -55,7 +106,10 @@ func NewDocWithOptions(opts DocOptions) (*Doc, error) {
67 return d, nil
68 }
69
70-// Clone creates a shallow clone (reference counted) of the document.
71+// Clone creates a shallow, reference-counted clone of the document.
72+// Both the original and clone share the same underlying data - changes to one
73+// are immediately visible to the other. Use DeepClone() if you need an
74+// independent copy with no shared state.
75 func (d *Doc) Clone() (*Doc, error) {
76 if d.ptr == nil {
77 return nil, ErrNilDocument
78@@ -68,6 +122,30 @@ func (d *Doc) Clone() (*Doc, error) {
79 return cloned, nil
80 }
81
82+// DeepClone creates an independent copy of the document by serializing its
83+// current state and deserializing into a new document. The returned document
84+// has no shared state with the original - changes to one do not affect the
85+// other. This is useful for creating checkpoints, backups, or isolated copies
86+// that can be modified independently.
87+func (d *Doc) DeepClone() (*Doc, error) {
88+ data, err := d.MarshalBinary()
89+ if err != nil {
90+ return nil, fmt.Errorf("failed to marshal document: %w", err)
91+ }
92+
93+ clone, err := NewDoc()
94+ if err != nil {
95+ return nil, fmt.Errorf("failed to create new document: %w", err)
96+ }
97+
98+ if err := clone.UnmarshalBinary(data); err != nil {
99+ clone.Destroy()
100+ return nil, fmt.Errorf("failed to unmarshal document: %w", err)
101+ }
102+
103+ return clone, nil
104+}
105+
106 // MarshalBinary implements encoding.BinaryMarshaler.
107 // Returns the document state as V1-encoded binary data.
108 // This enables idiomatic usage with Go's encoding packages.
109@@ -168,3 +246,143 @@ func (d *Doc) Destroy() {
110 runtime.SetFinalizer(d, nil)
111 }
112 }
113+
114+// MarshalJSON implements json.Marshaler for Document.
115+// Returns the specified top-level shared types as JSON.
116+// If no options are provided, returns an empty JSON object "{}".
117+//
118+// Example:
119+//
120+// doc.MarshalJSON(ygo.WithMap("users"), ygo.WithArray("items"))
121+// // Returns: {"users": {...}, "items": [...]}
122+func (d *Doc) MarshalJSON(opts ...MarshalOption) ([]byte, error) {
123+ if d.ptr == nil {
124+ return nil, ErrNilDocument
125+ }
126+
127+ cfg := &marshalConfig{}
128+ for _, opt := range opts {
129+ opt(cfg)
130+ }
131+
132+ if len(cfg.roots) == 0 {
133+ return []byte("{}"), nil
134+ }
135+
136+ roots := make(map[string]struct {
137+ typ RootType
138+ m *Map
139+ a *Array
140+ f *XmlFragment
141+ t *Text
142+ }, len(cfg.roots))
143+
144+ for _, spec := range cfg.roots {
145+ switch spec.typ {
146+ case RootTypeMap:
147+ m, err := d.GetMap(spec.name)
148+ if err != nil {
149+ return nil, fmt.Errorf("map %q: %w", spec.name, err)
150+ }
151+ roots[spec.name] = struct {
152+ typ RootType
153+ m *Map
154+ a *Array
155+ f *XmlFragment
156+ t *Text
157+ }{typ: spec.typ, m: m}
158+
159+ case RootTypeArray:
160+ a, err := d.GetArray(spec.name)
161+ if err != nil {
162+ return nil, fmt.Errorf("array %q: %w", spec.name, err)
163+ }
164+ roots[spec.name] = struct {
165+ typ RootType
166+ m *Map
167+ a *Array
168+ f *XmlFragment
169+ t *Text
170+ }{typ: spec.typ, a: a}
171+
172+ case RootTypeXmlFragment:
173+ f, err := d.GetXmlFragment(spec.name)
174+ if err != nil {
175+ return nil, fmt.Errorf("xml fragment %q: %w", spec.name, err)
176+ }
177+ roots[spec.name] = struct {
178+ typ RootType
179+ m *Map
180+ a *Array
181+ f *XmlFragment
182+ t *Text
183+ }{typ: spec.typ, f: f}
184+
185+ case RootTypeText:
186+ t, err := d.GetText(spec.name)
187+ if err != nil {
188+ return nil, fmt.Errorf("text %q: %w", spec.name, err)
189+ }
190+ roots[spec.name] = struct {
191+ typ RootType
192+ m *Map
193+ a *Array
194+ f *XmlFragment
195+ t *Text
196+ }{typ: spec.typ, t: t}
197+ }
198+ }
199+
200+ defer func() {
201+ for _, r := range roots {
202+ if r.m != nil {
203+ r.m.Destroy()
204+ }
205+ if r.a != nil {
206+ r.a.Destroy()
207+ }
208+ if r.f != nil {
209+ r.f.Destroy()
210+ }
211+ if r.t != nil {
212+ r.t.Destroy()
213+ }
214+ }
215+ }()
216+
217+ result := make(map[string]interface{})
218+ err := d.WithReadTransaction(func(txn *Transaction) error {
219+ for name, r := range roots {
220+ var jsonBytes []byte
221+ var err error
222+
223+ switch r.typ {
224+ case RootTypeMap:
225+ jsonBytes, err = r.m.MarshalJSONWithTxn(txn)
226+ case RootTypeArray:
227+ jsonBytes, err = r.a.MarshalJSONWithTxn(txn)
228+ case RootTypeXmlFragment:
229+ jsonBytes, err = r.f.MarshalJSONWithTxn(txn)
230+ case RootTypeText:
231+ jsonBytes, err = r.t.MarshalJSONWithTxn(txn)
232+ }
233+
234+ if err != nil {
235+ return fmt.Errorf("%s: %w", name, err)
236+ }
237+
238+ var val interface{}
239+ if err := json.Unmarshal(jsonBytes, &val); err != nil {
240+ return fmt.Errorf("%s: %w", name, err)
241+ }
242+ result[name] = val
243+ }
244+ return nil
245+ })
246+
247+ if err != nil {
248+ return nil, err
249+ }
250+
251+ return json.Marshal(result)
252+}
+255,
-0
1@@ -1,6 +1,7 @@
2 package ygo_test
3
4 import (
5+ "encoding/json"
6 "github.com/BTBurke/ygo"
7 "testing"
8 )
9@@ -62,6 +63,65 @@ func TestDocClone(t *testing.T) {
10 }
11 }
12
13+func TestDocDeepClone(t *testing.T) {
14+ doc, err := ygo.NewDoc()
15+ if err != nil {
16+ t.Fatalf("failed to create doc: %v", err)
17+ }
18+ defer doc.Destroy()
19+
20+ txt, err := doc.GetText("content")
21+ if err != nil {
22+ t.Fatalf("failed to get text: %v", err)
23+ }
24+ defer txt.Destroy()
25+
26+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
27+ return txt.Push(txn, "Hello")
28+ })
29+ if err != nil {
30+ t.Fatalf("transaction failed: %v", err)
31+ }
32+
33+ clone, err := doc.DeepClone()
34+ if err != nil {
35+ t.Fatalf("DeepClone failed: %v", err)
36+ }
37+ defer clone.Destroy()
38+
39+ // Verify clone has same content - get text BEFORE transaction
40+ txt2, err := clone.GetText("content")
41+ if err != nil {
42+ t.Fatalf("failed to get text from clone: %v", err)
43+ }
44+ defer txt2.Destroy()
45+
46+ var cloneContent string
47+ err = clone.WithReadTransaction(func(txn *ygo.Transaction) error {
48+ cloneContent, err = txt2.String(txn)
49+ return err
50+ })
51+ if err != nil {
52+ t.Fatalf("failed to read clone: %v", err)
53+ }
54+ if cloneContent != "Hello" {
55+ t.Errorf("expected 'Hello', got '%s'", cloneContent)
56+ }
57+
58+ // Verify independence - change original doesn't affect clone
59+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
60+ return txt.Push(txn, " World")
61+ })
62+ if err != nil {
63+ t.Fatalf("failed to modify original: %v", err)
64+ }
65+
66+ // Clone should still have old content
67+ if cloneContent != "Hello" {
68+ t.Error("clone was affected by change to original - not independent")
69+ }
70+}
71+
72 func TestDocDestroy(t *testing.T) {
73 doc, err := ygo.NewDoc()
74 if err != nil {
75@@ -189,3 +249,198 @@ func TestMarshalEmptyDocument(t *testing.T) {
76 t.Error("marshal of empty document should return some data (metadata)")
77 }
78 }
79+
80+func TestDocumentMarshalJSONEmpty(t *testing.T) {
81+ doc, err := ygo.NewDoc()
82+ if err != nil {
83+ t.Fatalf("failed to create doc: %v", err)
84+ }
85+ defer doc.Destroy()
86+
87+ jsonBytes, err := doc.MarshalJSON()
88+ if err != nil {
89+ t.Fatalf("marshal failed: %v", err)
90+ }
91+
92+ if string(jsonBytes) != "{}" {
93+ t.Errorf("expected {}, got %s", string(jsonBytes))
94+ }
95+}
96+
97+func TestDocumentMarshalJSONNonexistent(t *testing.T) {
98+ doc, err := ygo.NewDoc()
99+ if err != nil {
100+ t.Fatalf("failed to create doc: %v", err)
101+ }
102+ defer doc.Destroy()
103+
104+ jsonBytes, err := doc.MarshalJSON(ygo.WithMap("nonexistent"))
105+ if err != nil {
106+ t.Fatalf("marshal should succeed for nonexistent roots (ygo creates them): %v", err)
107+ }
108+
109+ var result map[string]interface{}
110+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
111+ t.Fatalf("failed to unmarshal: %v", err)
112+ }
113+
114+ if _, ok := result["nonexistent"]; !ok {
115+ t.Error("expected nonexistent map to be created and included")
116+ }
117+}
118+
119+func TestDocumentMarshalJSONWithOptions(t *testing.T) {
120+ doc, err := ygo.NewDoc()
121+ if err != nil {
122+ t.Fatalf("failed to create doc: %v", err)
123+ }
124+ defer doc.Destroy()
125+
126+ m, err := doc.GetMap("users")
127+ if err != nil {
128+ t.Fatalf("failed to get map: %v", err)
129+ }
130+ defer m.Destroy()
131+
132+ arr, err := doc.GetArray("items")
133+ if err != nil {
134+ t.Fatalf("failed to get array: %v", err)
135+ }
136+ defer arr.Destroy()
137+
138+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
139+ m.Insert(txn, "name", ygo.String("Alice"))
140+ m.Insert(txn, "age", ygo.Int(30))
141+ arr.Push(txn, ygo.String("item1"))
142+ arr.Push(txn, ygo.String("item2"))
143+ return nil
144+ })
145+ if err != nil {
146+ t.Fatalf("transaction failed: %v", err)
147+ }
148+
149+ jsonBytes, err := doc.MarshalJSON(
150+ ygo.WithMap("users"),
151+ ygo.WithArray("items"),
152+ )
153+ if err != nil {
154+ t.Fatalf("marshal failed: %v", err)
155+ }
156+
157+ var result map[string]interface{}
158+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
159+ t.Fatalf("failed to unmarshal: %v", err)
160+ }
161+
162+ users, ok := result["users"].(map[string]interface{})
163+ if !ok {
164+ t.Fatal("expected users to be a map")
165+ }
166+ if users["name"] != "Alice" {
167+ t.Errorf("expected users.name=Alice, got %v", users["name"])
168+ }
169+ if users["age"] != float64(30) {
170+ t.Errorf("expected users.age=30, got %v", users["age"])
171+ }
172+
173+ items, ok := result["items"].([]interface{})
174+ if !ok {
175+ t.Fatal("expected items to be an array")
176+ }
177+ if len(items) != 2 {
178+ t.Errorf("expected 2 items, got %d", len(items))
179+ }
180+ if items[0] != "item1" {
181+ t.Errorf("expected items[0]=item1, got %v", items[0])
182+ }
183+ if items[1] != "item2" {
184+ t.Errorf("expected items[1]=item2, got %v", items[1])
185+ }
186+}
187+
188+func TestDocumentMarshalJSONWithXmlFragment(t *testing.T) {
189+ doc, err := ygo.NewDoc()
190+ if err != nil {
191+ t.Fatalf("failed to create doc: %v", err)
192+ }
193+ defer doc.Destroy()
194+
195+ f, err := doc.GetXmlFragment("content")
196+ if err != nil {
197+ t.Fatalf("failed to get xml fragment: %v", err)
198+ }
199+ defer f.Destroy()
200+
201+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
202+ elem, err := f.InsertElement(txn, 0, "note")
203+ if err != nil {
204+ return err
205+ }
206+ defer elem.Destroy()
207+
208+ txt, err := elem.InsertText(txn, 0)
209+ if err != nil {
210+ return err
211+ }
212+ defer txt.Destroy()
213+
214+ txt.Insert(txn, 0, "Hello World")
215+ return nil
216+ })
217+ if err != nil {
218+ t.Fatalf("transaction failed: %v", err)
219+ }
220+
221+ jsonBytes, err := doc.MarshalJSON(ygo.WithXmlFragment("content"))
222+ if err != nil {
223+ t.Fatalf("marshal failed: %v", err)
224+ }
225+
226+ var result map[string]interface{}
227+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
228+ t.Fatalf("failed to unmarshal: %v", err)
229+ }
230+
231+ content, ok := result["content"].([]interface{})
232+ if !ok {
233+ t.Fatal("expected content to be an array")
234+ }
235+ if len(content) == 0 {
236+ t.Fatal("expected at least one element in content array")
237+ }
238+}
239+
240+func TestDocumentMarshalJSONWithText(t *testing.T) {
241+ doc, err := ygo.NewDoc()
242+ if err != nil {
243+ t.Fatalf("failed to create doc: %v", err)
244+ }
245+ defer doc.Destroy()
246+
247+ txt, err := doc.GetText("schema")
248+ if err != nil {
249+ t.Fatalf("failed to get text: %v", err)
250+ }
251+ defer txt.Destroy()
252+
253+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
254+ return txt.Push(txn, "some-schema-value")
255+ })
256+ if err != nil {
257+ t.Fatalf("transaction failed: %v", err)
258+ }
259+
260+ jsonBytes, err := doc.MarshalJSON(ygo.WithText("schema"))
261+ if err != nil {
262+ t.Fatalf("marshal failed: %v", err)
263+ }
264+
265+ var result map[string]interface{}
266+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
267+ t.Fatalf("failed to unmarshal: %v", err)
268+ }
269+
270+ if result["schema"] != "some-schema-value" {
271+ t.Errorf("expected schema=some-schema-value, got %v", result["schema"])
272+ }
273+}
+0,
-0
M
map.go
+207,
-0
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "encoding/json"
6 "fmt"
7 "runtime"
8 "unsafe"
9@@ -122,3 +123,209 @@ func (m *Map) Clear(txn *Transaction) error {
10 func (m *Map) Branch() unsafe.Pointer {
11 return unsafe.Pointer(m.branch)
12 }
13+
14+// MapEntry represents a key-value pair from a map.
15+type MapEntry struct {
16+ Key string
17+ Value *Output
18+}
19+
20+// MapIter iterates over map entries.
21+type MapIter struct {
22+ ptr *C.YMapIter
23+}
24+
25+// Destroy releases iterator resources.
26+func (it *MapIter) Destroy() {
27+ if it.ptr != nil {
28+ C.ymap_iter_destroy(it.ptr)
29+ it.ptr = nil
30+ }
31+}
32+
33+// Next returns the next entry. Returns nil when iteration is complete.
34+func (it *MapIter) Next() (*MapEntry, error) {
35+ if it.ptr == nil {
36+ return nil, ErrIteratorExhausted
37+ }
38+ entry := C.ymap_iter_next(it.ptr)
39+ if entry == nil {
40+ return nil, nil // End of iteration
41+ }
42+
43+ // Convert key from C string
44+ key := C.GoString(entry.key)
45+
46+ // Wrap the YOutput pointer
47+ // Note: YMapEntry's Drop impl destroys the value when the entry is freed,
48+ // so callers should NOT call Destroy() on entry.Value
49+ value := &Output{ptr: entry.value}
50+
51+ return &MapEntry{Key: key, Value: value}, nil
52+}
53+
54+// Iter returns an iterator over the map entries.
55+func (m *Map) Iter(txn *Transaction) (*MapIter, error) {
56+ if m.branch == nil {
57+ return nil, ErrNilBranch
58+ }
59+ if txn == nil || txn.ptr == nil {
60+ return nil, ErrNilTransaction
61+ }
62+ ptr := C.ymap_iter(m.branch, txn.ptr)
63+ if ptr == nil {
64+ return nil, fmt.Errorf("failed to create map iterator")
65+ }
66+ return &MapIter{ptr: ptr}, nil
67+}
68+
69+// MarshalJSONWithTxn implements JSON marshaling for Map.
70+// Returns a JSON object with string keys and JSON values.
71+func (m *Map) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
72+ if m.branch == nil {
73+ return nil, ErrNilBranch
74+ }
75+ if txn == nil || txn.ptr == nil {
76+ return nil, ErrNilTransaction
77+ }
78+
79+ // Collect all entries
80+ data := make(map[string]interface{})
81+
82+ iter, err := m.Iter(txn)
83+ if err != nil {
84+ return nil, err
85+ }
86+ defer iter.Destroy()
87+
88+ for {
89+ entry, err := iter.Next()
90+ if err != nil {
91+ return nil, err
92+ }
93+ if entry == nil {
94+ break
95+ }
96+
97+ // Convert value to JSON-compatible type
98+ val, err := m.valueToJSON(entry.Value, txn)
99+ if err != nil {
100+ entry.Value.Destroy()
101+ return nil, fmt.Errorf("key %q: %w", entry.Key, err)
102+ }
103+
104+ data[entry.Key] = val
105+ entry.Value.Destroy()
106+ }
107+
108+ return json.Marshal(data)
109+}
110+
111+// valueToJSON converts an Output value to a JSON-compatible Go value.
112+// For primitive types, uses Output methods. For shared types, returns error
113+// (shared type marshaling implemented in later tasks).
114+func (m *Map) valueToJSON(out *Output, txn *Transaction) (interface{}, error) {
115+ if out == nil || out.ptr == nil {
116+ return nil, nil
117+ }
118+
119+ switch out.Tag() {
120+ // Primitive JSON types - use Output methods
121+ case TagJSONNull:
122+ return nil, nil
123+ case TagJSONBool:
124+ if v, ok := out.Bool(); ok {
125+ return v, nil
126+ }
127+ return nil, fmt.Errorf("failed to read boolean")
128+ case TagJSONNum:
129+ if v, ok := out.Float(); ok {
130+ return v, nil
131+ }
132+ return nil, fmt.Errorf("failed to read float")
133+ case TagJSONInt:
134+ if v, ok := out.Int(); ok {
135+ return v, nil
136+ }
137+ return nil, fmt.Errorf("failed to read int")
138+ case TagJSONStr:
139+ if v, ok := out.String(); ok {
140+ return v, nil
141+ }
142+ return nil, fmt.Errorf("failed to read string")
143+ case TagJSONBuf:
144+ if v, ok := out.Binary(); ok {
145+ return v, nil
146+ }
147+ return nil, fmt.Errorf("failed to read binary")
148+
149+ // Shared types - recursively marshal
150+ case TagMap:
151+ branch := C.youtput_read_ymap(out.ptr)
152+ if branch == nil {
153+ return nil, fmt.Errorf("failed to read ymap from output")
154+ }
155+ m := &Map{branch: branch}
156+ data := make(map[string]interface{})
157+ iter, err := m.Iter(txn)
158+ if err != nil {
159+ return nil, err
160+ }
161+ defer iter.Destroy()
162+ for {
163+ entry, err := iter.Next()
164+ if err != nil {
165+ return nil, err
166+ }
167+ if entry == nil {
168+ break
169+ }
170+ val, err := m.valueToJSON(entry.Value, txn)
171+ if err != nil {
172+ entry.Value.Destroy()
173+ return nil, fmt.Errorf("key %q: %w", entry.Key, err)
174+ }
175+ data[entry.Key] = val
176+ entry.Value.Destroy()
177+ }
178+ return data, nil
179+ case TagArray:
180+ branch := C.youtput_read_yarray(out.ptr)
181+ if branch == nil {
182+ return nil, fmt.Errorf("failed to read yarray from output")
183+ }
184+ arr := &Array{branch: branch}
185+ var elements []interface{}
186+ iter, err := arr.Iter(txn)
187+ if err != nil {
188+ return nil, err
189+ }
190+ defer iter.Destroy()
191+ for {
192+ el, err := iter.Next()
193+ if err != nil {
194+ return nil, err
195+ }
196+ if el == nil {
197+ break
198+ }
199+ val, err := arr.valueToJSON(el, txn)
200+ if err != nil {
201+ el.Destroy()
202+ return nil, err
203+ }
204+ elements = append(elements, val)
205+ el.Destroy()
206+ }
207+ return elements, nil
208+ case TagText, TagXmlElem, TagXmlText, TagXmlFrag:
209+ // XML types not supported in map context
210+ return nil, fmt.Errorf("XML types not supported in map JSON marshaling: %v", out.Tag())
211+ case TagDoc:
212+ return nil, fmt.Errorf("nested documents not supported in JSON marshaling")
213+ case TagWeakLink, TagUndefined, TagJSONUndef:
214+ return nil, nil
215+ default:
216+ return nil, fmt.Errorf("unsupported type tag: %v", out.Tag())
217+ }
218+}
+159,
-0
1@@ -1,6 +1,7 @@
2 package ygo_test
3
4 import (
5+ "encoding/json"
6 "github.com/BTBurke/ygo"
7 "testing"
8 )
9@@ -129,3 +130,161 @@ func TestMapNested(t *testing.T) {
10 t.Fatal("expected map output, got undefined")
11 }
12 }
13+
14+func collectMapEntries(m *ygo.Map, txn *ygo.Transaction) (map[string]interface{}, error) {
15+ iter, err := m.Iter(txn)
16+ if err != nil {
17+ return nil, err
18+ }
19+ defer iter.Destroy()
20+
21+ entries := make(map[string]interface{})
22+ for {
23+ entry, err := iter.Next()
24+ if err != nil {
25+ return nil, err
26+ }
27+ if entry == nil {
28+ break
29+ }
30+
31+ // Convert value based on type
32+ var value interface{}
33+ switch entry.Value.Tag() {
34+ case ygo.TagJSONBool:
35+ if v, ok := entry.Value.Bool(); ok {
36+ value = v
37+ }
38+ case ygo.TagJSONNum:
39+ if v, ok := entry.Value.Float(); ok {
40+ value = v
41+ }
42+ case ygo.TagJSONInt:
43+ if v, ok := entry.Value.Int(); ok {
44+ value = v
45+ }
46+ case ygo.TagJSONStr:
47+ if v, ok := entry.Value.String(); ok {
48+ value = v
49+ }
50+ case ygo.TagJSONNull:
51+ value = nil
52+ default:
53+ value = nil
54+ }
55+
56+ entries[entry.Key] = value
57+ // Note: Don't destroy entry.Value - it's managed by the entry
58+ // The iterator will clean up entries when destroyed
59+ }
60+ return entries, nil
61+}
62+
63+func TestMapIter(t *testing.T) {
64+ doc, err := ygo.NewDoc()
65+ if err != nil {
66+ t.Fatalf("failed to create doc: %v", err)
67+ }
68+ defer doc.Destroy()
69+
70+ m, err := doc.GetMap("test")
71+ if err != nil {
72+ t.Fatalf("failed to get map: %v", err)
73+ }
74+ defer m.Destroy()
75+
76+ // Insert test data
77+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
78+ m.Insert(txn, "key1", ygo.String("value1"))
79+ m.Insert(txn, "key2", ygo.Int(42))
80+ m.Insert(txn, "key3", ygo.Bool(true))
81+ return nil
82+ })
83+ if err != nil {
84+ t.Fatalf("transaction failed: %v", err)
85+ }
86+
87+ // Test iteration
88+ var entries map[string]interface{}
89+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
90+ var err error
91+ entries, err = collectMapEntries(m, txn)
92+ return err
93+ })
94+ if err != nil {
95+ t.Fatalf("iteration failed: %v", err)
96+ }
97+
98+ if len(entries) != 3 {
99+ t.Errorf("expected 3 entries, got %d", len(entries))
100+ }
101+ if entries["key1"] != "value1" {
102+ t.Errorf("expected key1=value1, got %v", entries["key1"])
103+ }
104+ if entries["key2"] != int64(42) {
105+ t.Errorf("expected key2=42, got %v", entries["key2"])
106+ }
107+ if entries["key3"] != true {
108+ t.Errorf("expected key3=true, got %v", entries["key3"])
109+ }
110+}
111+
112+func TestMapMarshalJSON(t *testing.T) {
113+ doc, err := ygo.NewDoc()
114+ if err != nil {
115+ t.Fatalf("failed to create doc: %v", err)
116+ }
117+ defer doc.Destroy()
118+
119+ m, err := doc.GetMap("test")
120+ if err != nil {
121+ t.Fatalf("failed to get map: %v", err)
122+ }
123+ defer m.Destroy()
124+
125+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
126+ m.Insert(txn, "string", ygo.String("hello"))
127+ m.Insert(txn, "number", ygo.Int(42))
128+ m.Insert(txn, "float", ygo.Float(3.14))
129+ m.Insert(txn, "bool", ygo.Bool(true))
130+ m.Insert(txn, "null", ygo.Null())
131+ return nil
132+ })
133+ if err != nil {
134+ t.Fatalf("transaction failed: %v", err)
135+ }
136+
137+ // Marshal within read transaction
138+ var jsonBytes []byte
139+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
140+ var err error
141+ jsonBytes, err = m.MarshalJSONWithTxn(txn)
142+ return err
143+ })
144+ if err != nil {
145+ t.Fatalf("marshal failed: %v", err)
146+ }
147+
148+ // Verify JSON is valid
149+ var result map[string]interface{}
150+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
151+ t.Fatalf("failed to unmarshal result: %v", err)
152+ }
153+
154+ // Check values (order doesn't matter for maps)
155+ if result["string"] != "hello" {
156+ t.Errorf("expected string=hello, got %v", result["string"])
157+ }
158+ if result["number"] != float64(42) {
159+ t.Errorf("expected number=42, got %v", result["number"])
160+ }
161+ if result["float"] != 3.14 {
162+ t.Errorf("expected float=3.14, got %v", result["float"])
163+ }
164+ if result["bool"] != true {
165+ t.Errorf("expected bool=true, got %v", result["bool"])
166+ }
167+ if result["null"] != nil {
168+ t.Errorf("expected null=nil, got %v", result["null"])
169+ }
170+}
+176,
-0
1@@ -0,0 +1,176 @@
2+// marshal_test.go - Integration tests for MarshalJSON functionality
3+package ygo_test
4+
5+import (
6+ "encoding/json"
7+ "testing"
8+
9+ "github.com/BTBurke/ygo"
10+)
11+
12+func TestMarshalComplexStructure(t *testing.T) {
13+ doc, err := ygo.NewDoc()
14+ if err != nil {
15+ t.Fatalf("failed to create doc: %v", err)
16+ }
17+ defer doc.Destroy()
18+
19+ root, err := doc.GetMap("root")
20+ if err != nil {
21+ t.Fatalf("failed to get root map: %v", err)
22+ }
23+ defer root.Destroy()
24+
25+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
26+ // Add simple fields
27+ root.Insert(txn, "title", ygo.String("Complex Document"))
28+ root.Insert(txn, "count", ygo.Int(42))
29+ root.Insert(txn, "active", ygo.Bool(true))
30+
31+ // Add nested map
32+ nestedData := ygo.YMap(
33+ []string{"nested_key"},
34+ []ygo.Input{ygo.String("nested_value")},
35+ )
36+ root.Insert(txn, "metadata", nestedData)
37+
38+ // Add array
39+ arrItems := []ygo.Input{ygo.Int(1), ygo.Int(2), ygo.Int(3)}
40+ root.Insert(txn, "numbers", ygo.YArray(arrItems))
41+
42+ return nil
43+ })
44+ if err != nil {
45+ t.Fatalf("transaction failed: %v", err)
46+ }
47+
48+ // Marshal and verify
49+ var jsonBytes []byte
50+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
51+ var err error
52+ jsonBytes, err = root.MarshalJSONWithTxn(txn)
53+ return err
54+ })
55+ if err != nil {
56+ t.Fatalf("marshal failed: %v", err)
57+ }
58+
59+ var result map[string]interface{}
60+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
61+ t.Fatalf("unmarshal failed: %v", err)
62+ }
63+
64+ // Verify structure
65+ if result["title"] != "Complex Document" {
66+ t.Errorf("wrong title: %v", result["title"])
67+ }
68+
69+ metadata, ok := result["metadata"].(map[string]interface{})
70+ if !ok {
71+ t.Fatal("metadata should be a map")
72+ }
73+ if metadata["nested_key"] != "nested_value" {
74+ t.Errorf("wrong nested value: %v", metadata["nested_key"])
75+ }
76+
77+ numbers, ok := result["numbers"].([]interface{})
78+ if !ok {
79+ t.Fatal("numbers should be an array")
80+ }
81+ if len(numbers) != 3 {
82+ t.Errorf("expected 3 numbers, got %d", len(numbers))
83+ }
84+}
85+
86+func TestMarshalXMLWithNestedElements(t *testing.T) {
87+ doc, err := ygo.NewDoc()
88+ if err != nil {
89+ t.Fatalf("failed to create doc: %v", err)
90+ }
91+ defer doc.Destroy()
92+
93+ frag, err := doc.GetXmlFragment("content")
94+ if err != nil {
95+ t.Fatalf("failed to get fragment: %v", err)
96+ }
97+ defer frag.Destroy()
98+
99+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
100+ // Create structure: <article><title>Hello</title><p>World</p></article>
101+ article, err := frag.InsertElement(txn, 0, "article")
102+ if err != nil {
103+ return err
104+ }
105+ article.SetAttribute(txn, "id", ygo.String("doc-1"))
106+
107+ title, err := article.InsertElement(txn, 0, "title")
108+ if err != nil {
109+ return err
110+ }
111+ titleText, err := title.InsertText(txn, 0)
112+ if err != nil {
113+ return err
114+ }
115+ titleText.Insert(txn, 0, "Hello")
116+
117+ p, err := article.InsertElement(txn, 1, "p")
118+ if err != nil {
119+ return err
120+ }
121+ pText, err := p.InsertText(txn, 0)
122+ if err != nil {
123+ return err
124+ }
125+ pText.Insert(txn, 0, "World")
126+
127+ return nil
128+ })
129+ if err != nil {
130+ t.Fatalf("transaction failed: %v", err)
131+ }
132+
133+ // Marshal fragment
134+ var jsonBytes []byte
135+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
136+ var err error
137+ jsonBytes, err = frag.MarshalJSONWithTxn(txn)
138+ return err
139+ })
140+ if err != nil {
141+ t.Fatalf("marshal failed: %v", err)
142+ }
143+
144+ var result []interface{}
145+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
146+ t.Fatalf("unmarshal failed: %v", err)
147+ }
148+
149+ if len(result) != 1 {
150+ t.Fatalf("expected 1 root element, got %d", len(result))
151+ }
152+
153+ article, ok := result[0].(map[string]interface{})
154+ if !ok {
155+ t.Fatal("article should be an object")
156+ }
157+
158+ if article["type"] != "article" {
159+ t.Errorf("wrong type: %v", article["type"])
160+ }
161+
162+ attrs, ok := article["attrs"].(map[string]interface{})
163+ if !ok {
164+ t.Fatal("attrs should be a map")
165+ }
166+ if attrs["id"] != "doc-1" {
167+ t.Errorf("wrong id: %v", attrs["id"])
168+ }
169+
170+ children, ok := article["children"].([]interface{})
171+ if !ok {
172+ t.Fatal("children should be an array")
173+ }
174+ if len(children) != 2 {
175+ t.Errorf("expected 2 children, got %d", len(children))
176+ }
177+}
+55,
-1
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "fmt"
6 "unsafe"
7 )
8
9@@ -113,7 +114,9 @@ func (o *Output) String() (string, bool) {
10 if ptr == nil {
11 return "", false
12 }
13- return cStringToGoAndFree(ptr), true
14+ // Note: The string is owned by the YOutput and will be freed when
15+ // youtput_destroy is called. We must NOT free it here.
16+ return C.GoString(ptr), true
17 }
18
19 // Binary reads the value as a byte slice.
20@@ -131,3 +134,54 @@ func (o *Output) Binary() ([]byte, bool) {
21 copy(result, (*[1 << 30]byte)(unsafe.Pointer(ptr))[:length:length])
22 return result, true
23 }
24+
25+// ToInterface converts the output to a Go interface{} value based on its type tag.
26+// Returns nil and an error if the type cannot be converted.
27+func (o *Output) ToInterface() (interface{}, error) {
28+ if o == nil {
29+ return nil, fmt.Errorf("nil output")
30+ }
31+ if o.ptr == nil {
32+ return nil, fmt.Errorf("nil output pointer")
33+ }
34+
35+ switch o.Tag() {
36+ case TagJSONNull:
37+ return nil, nil
38+ case TagJSONUndef, TagUndefined:
39+ return nil, nil
40+ case TagJSONBool:
41+ if v, ok := o.Bool(); ok {
42+ return v, nil
43+ }
44+ return nil, fmt.Errorf("failed to read boolean")
45+ case TagJSONNum:
46+ if v, ok := o.Float(); ok {
47+ return v, nil
48+ }
49+ return nil, fmt.Errorf("failed to read float")
50+ case TagJSONInt:
51+ if v, ok := o.Int(); ok {
52+ return v, nil
53+ }
54+ return nil, fmt.Errorf("failed to read int")
55+ case TagJSONStr:
56+ if v, ok := o.String(); ok {
57+ return v, nil
58+ }
59+ return nil, fmt.Errorf("failed to read string")
60+ case TagJSONBuf:
61+ if v, ok := o.Binary(); ok {
62+ return v, nil
63+ }
64+ return nil, fmt.Errorf("failed to read binary")
65+ case TagJSON, TagJSONArr, TagJSONMap:
66+ // JSON types that require special handling
67+ return nil, fmt.Errorf("JSON container types not supported in ToInterface, use document methods: %v", o.Tag())
68+ case TagArray, TagMap, TagText, TagXmlElem, TagXmlText, TagXmlFrag, TagDoc, TagWeakLink:
69+ // Shared types - return error, caller should handle specially
70+ return nil, fmt.Errorf("shared types must be handled separately: %v", o.Tag())
71+ default:
72+ return nil, fmt.Errorf("unknown type tag: %v", o.Tag())
73+ }
74+}
+196,
-0
1@@ -0,0 +1,196 @@
2+package ygo_test
3+
4+import (
5+ "github.com/BTBurke/ygo"
6+ "testing"
7+)
8+
9+func TestOutputToInterface(t *testing.T) {
10+ doc, err := ygo.NewDoc()
11+ if err != nil {
12+ t.Fatalf("failed to create doc: %v", err)
13+ }
14+ defer doc.Destroy()
15+
16+ arr, err := doc.GetArray("test")
17+ if err != nil {
18+ t.Fatalf("failed to get array: %v", err)
19+ }
20+ defer arr.Destroy()
21+
22+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
23+ items := []ygo.Input{
24+ ygo.String("hello"),
25+ ygo.Int(42),
26+ ygo.Float(3.14),
27+ ygo.Bool(true),
28+ ygo.Null(),
29+ }
30+ return arr.InsertRange(txn, 0, items)
31+ })
32+ if err != nil {
33+ t.Fatalf("transaction failed: %v", err)
34+ }
35+
36+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
37+ expectedValues := []interface{}{"hello", int64(42), float64(3.14), true, nil}
38+ for i := 0; i < len(expectedValues); i++ {
39+ out, err := arr.Get(txn, uint32(i))
40+ if err != nil {
41+ return err
42+ }
43+
44+ val, err := out.ToInterface()
45+ if err != nil {
46+ t.Fatalf("index %d: ToInterface failed: %v", i, err)
47+ }
48+
49+ if val != expectedValues[i] {
50+ t.Errorf("expected %v, got %v", expectedValues[i], val)
51+ }
52+
53+ out.Destroy()
54+ }
55+ return nil
56+ })
57+ if err != nil {
58+ t.Fatalf("read transaction failed: %v", err)
59+ }
60+}
61+
62+func TestOutputEdgeCases(t *testing.T) {
63+ t.Run("nil output pointer", func(t *testing.T) {
64+ var out *ygo.Output
65+ val, err := out.ToInterface()
66+ if err == nil {
67+ t.Errorf("expected error for nil output, got value: %v", val)
68+ }
69+ })
70+
71+ t.Run("shared type error", func(t *testing.T) {
72+ doc, err := ygo.NewDoc()
73+ if err != nil {
74+ t.Fatalf("failed to create doc: %v", err)
75+ }
76+ defer doc.Destroy()
77+
78+ arr, err := doc.GetArray("test")
79+ if err != nil {
80+ t.Fatalf("failed to get array: %v", err)
81+ }
82+ defer arr.Destroy()
83+
84+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
85+ return arr.InsertRange(txn, 0, []ygo.Input{ygo.YArray(nil)})
86+ })
87+ if err != nil {
88+ t.Fatalf("transaction failed: %v", err)
89+ }
90+
91+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
92+ out, err := arr.Get(txn, 0)
93+ if err != nil {
94+ return err
95+ }
96+ defer out.Destroy()
97+
98+ val, err := out.ToInterface()
99+ if err == nil {
100+ t.Errorf("expected error for shared type, got value: %v", val)
101+ }
102+ return nil
103+ })
104+ if err != nil {
105+ t.Fatalf("read transaction failed: %v", err)
106+ }
107+ })
108+
109+ t.Run("binary data", func(t *testing.T) {
110+ doc, err := ygo.NewDoc()
111+ if err != nil {
112+ t.Fatalf("failed to create doc: %v", err)
113+ }
114+ defer doc.Destroy()
115+
116+ arr, err := doc.GetArray("test")
117+ if err != nil {
118+ t.Fatalf("failed to get array: %v", err)
119+ }
120+ defer arr.Destroy()
121+
122+ testData := []byte("test binary data")
123+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
124+ return arr.InsertRange(txn, 0, []ygo.Input{ygo.Binary(testData)})
125+ })
126+ if err != nil {
127+ t.Fatalf("transaction failed: %v", err)
128+ }
129+
130+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
131+ out, err := arr.Get(txn, 0)
132+ if err != nil {
133+ return err
134+ }
135+ defer out.Destroy()
136+
137+ val, err := out.ToInterface()
138+ if err != nil {
139+ t.Fatalf("ToInterface failed: %v", err)
140+ }
141+
142+ got, ok := val.([]byte)
143+ if !ok {
144+ t.Fatalf("expected []byte, got %T", val)
145+ }
146+
147+ if string(got) != string(testData) {
148+ t.Errorf("expected %q, got %q", string(testData), string(got))
149+ }
150+ return nil
151+ })
152+ if err != nil {
153+ t.Fatalf("read transaction failed: %v", err)
154+ }
155+ })
156+
157+ t.Run("undefined value", func(t *testing.T) {
158+ doc, err := ygo.NewDoc()
159+ if err != nil {
160+ t.Fatalf("failed to create doc: %v", err)
161+ }
162+ defer doc.Destroy()
163+
164+ arr, err := doc.GetArray("test")
165+ if err != nil {
166+ t.Fatalf("failed to get array: %v", err)
167+ }
168+ defer arr.Destroy()
169+
170+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
171+ return arr.InsertRange(txn, 0, []ygo.Input{ygo.Undefined()})
172+ })
173+ if err != nil {
174+ t.Fatalf("transaction failed: %v", err)
175+ }
176+
177+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
178+ out, err := arr.Get(txn, 0)
179+ if err != nil {
180+ return err
181+ }
182+ defer out.Destroy()
183+
184+ val, err := out.ToInterface()
185+ if err != nil {
186+ t.Fatalf("ToInterface failed: %v", err)
187+ }
188+ if val != nil {
189+ t.Errorf("expected nil for undefined, got %v", val)
190+ }
191+ return nil
192+ })
193+ if err != nil {
194+ t.Fatalf("read transaction failed: %v", err)
195+ }
196+ })
197+}
M
text.go
+19,
-0
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "encoding/json"
6 "fmt"
7 "runtime"
8 "unsafe"
9@@ -139,3 +140,21 @@ func (t *Text) Push(txn *Transaction, text string) error {
10 func (t *Text) Branch() unsafe.Pointer {
11 return unsafe.Pointer(t.branch)
12 }
13+
14+// MarshalJSONWithTxn implements JSON marshaling for Text.
15+// Returns the text content as a JSON string.
16+func (t *Text) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
17+ if t.branch == nil {
18+ return nil, ErrNilBranch
19+ }
20+ if txn == nil || txn.ptr == nil {
21+ return nil, ErrNilTransaction
22+ }
23+
24+ content, err := t.String(txn)
25+ if err != nil {
26+ return nil, err
27+ }
28+
29+ return json.Marshal(content)
30+}
+43,
-1
1@@ -1,8 +1,10 @@
2 package ygo_test
3
4 import (
5- "github.com/BTBurke/ygo"
6+ "encoding/json"
7 "testing"
8+
9+ "github.com/BTBurke/ygo"
10 )
11
12 func TestTextBasic(t *testing.T) {
13@@ -148,3 +150,43 @@ func TestTextUnicode(t *testing.T) {
14 t.Errorf("expected '%s', got '%s'", expected, result)
15 }
16 }
17+
18+func TestTextMarshalJSON(t *testing.T) {
19+ doc, err := ygo.NewDoc()
20+ if err != nil {
21+ t.Fatalf("failed to create doc: %v", err)
22+ }
23+ defer doc.Destroy()
24+
25+ txt, err := doc.GetText("content")
26+ if err != nil {
27+ t.Fatalf("failed to get text: %v", err)
28+ }
29+ defer txt.Destroy()
30+
31+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
32+ return txt.Push(txn, "Hello, World!")
33+ })
34+ if err != nil {
35+ t.Fatalf("transaction failed: %v", err)
36+ }
37+
38+ var jsonBytes []byte
39+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
40+ var err error
41+ jsonBytes, err = txt.MarshalJSONWithTxn(txn)
42+ return err
43+ })
44+ if err != nil {
45+ t.Fatalf("marshal failed: %v", err)
46+ }
47+
48+ // Text should marshal to a JSON string
49+ var result string
50+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
51+ t.Fatalf("failed to unmarshal: %v", err)
52+ }
53+ if result != "Hello, World!" {
54+ t.Errorf("expected 'Hello, World!', got '%s'", result)
55+ }
56+}
M
xml.go
+399,
-0
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "encoding/json"
6 "fmt"
7 "runtime"
8 "unsafe"
9@@ -301,3 +302,401 @@ func (t *XmlText) Branch() unsafe.Pointer {
10 func (f *XmlFragment) Branch() unsafe.Pointer {
11 return unsafe.Pointer(f.branch)
12 }
13+
14+// XmlAttrIter iterates over XML element attributes.
15+type XmlAttrIter struct {
16+ ptr *C.YXmlAttrIter
17+}
18+
19+// Destroy releases iterator resources.
20+func (it *XmlAttrIter) Destroy() {
21+ if it.ptr != nil {
22+ C.yxmlattr_iter_destroy(it.ptr)
23+ it.ptr = nil
24+ }
25+}
26+
27+// XmlAttr represents an XML attribute.
28+type XmlAttr struct {
29+ Name string
30+ Value *Output
31+}
32+
33+// Next returns the next attribute. Returns nil when iteration is complete.
34+func (it *XmlAttrIter) Next() (*XmlAttr, error) {
35+ if it.ptr == nil {
36+ return nil, ErrIteratorExhausted
37+ }
38+ attr := C.yxmlattr_iter_next(it.ptr)
39+ if attr == nil {
40+ return nil, nil // End of iteration
41+ }
42+
43+ name := C.GoString(attr.name)
44+ value := &Output{ptr: attr.value}
45+
46+ return &XmlAttr{Name: name, Value: value}, nil
47+}
48+
49+// AttrIter returns an iterator over the element's attributes.
50+func (e *XmlElement) AttrIter(txn *Transaction) (*XmlAttrIter, error) {
51+ if e.branch == nil {
52+ return nil, ErrNilBranch
53+ }
54+ if txn == nil || txn.ptr == nil {
55+ return nil, ErrNilTransaction
56+ }
57+ ptr := C.yxmlelem_attr_iter(e.branch, txn.ptr)
58+ if ptr == nil {
59+ return nil, fmt.Errorf("failed to create attribute iterator")
60+ }
61+ return &XmlAttrIter{ptr: ptr}, nil
62+}
63+
64+// AttrIter returns an iterator over the text node's formatting attributes.
65+func (t *XmlText) AttrIter(txn *Transaction) (*XmlAttrIter, error) {
66+ if t.branch == nil {
67+ return nil, ErrNilBranch
68+ }
69+ if txn == nil || txn.ptr == nil {
70+ return nil, ErrNilTransaction
71+ }
72+ ptr := C.yxmltext_attr_iter(t.branch, txn.ptr)
73+ if ptr == nil {
74+ return nil, fmt.Errorf("failed to create text attribute iterator")
75+ }
76+ return &XmlAttrIter{ptr: ptr}, nil
77+}
78+
79+// XmlChildIter iterates over XML element children.
80+type XmlChildIter struct {
81+ output *C.YOutput
82+ txn *Transaction
83+}
84+
85+// Destroy is a no-op - children are managed by parent.
86+func (it *XmlChildIter) Destroy() {
87+ // No-op, children are owned by parent
88+}
89+
90+// Next returns the next child node. Returns nil when iteration is complete.
91+func (it *XmlChildIter) Next() (*Output, error) {
92+ if it.output == nil {
93+ return nil, nil // End of iteration
94+ }
95+
96+ // Save the current output for return
97+ currentOutput := it.output
98+
99+ // Get the branch from current output to find next sibling
100+ var currentBranch *C.Branch
101+ tag := ValueTag(currentOutput.tag)
102+ switch tag {
103+ case TagXmlElem:
104+ currentBranch = C.youtput_read_yxmlelem(currentOutput)
105+ case TagXmlText:
106+ currentBranch = C.youtput_read_yxmltext(currentOutput)
107+ default:
108+ // Can't continue iteration
109+ it.output = nil
110+ return &Output{ptr: currentOutput}, nil
111+ }
112+
113+ // Get next sibling output
114+ nextOutput := C.yxml_next_sibling(currentBranch, it.txn.ptr)
115+
116+ // Destroy the branch we extracted (we don't need it anymore)
117+ // The branch is owned by the document, so we don't free it
118+ // But we need to destroy the current output since we're returning a new Output wrapper
119+
120+ if nextOutput == nil {
121+ it.output = nil
122+ } else {
123+ it.output = nextOutput
124+ }
125+
126+ // Return the current output
127+ return &Output{ptr: currentOutput}, nil
128+}
129+
130+// ChildIter returns an iterator over the element's direct children.
131+func (e *XmlElement) ChildIter(txn *Transaction) (*XmlChildIter, error) {
132+ if e.branch == nil {
133+ return nil, ErrNilBranch
134+ }
135+ if txn == nil || txn.ptr == nil {
136+ return nil, ErrNilTransaction
137+ }
138+
139+ // Get first child output
140+ firstOutput := C.yxmlelem_first_child(e.branch)
141+ if firstOutput == nil {
142+ return &XmlChildIter{output: nil, txn: txn}, nil
143+ }
144+
145+ // Return iterator with first output (no conversion needed now)
146+ return &XmlChildIter{output: firstOutput, txn: txn}, nil
147+}
148+
149+// FirstChild returns the first child of the fragment.
150+func (f *XmlFragment) FirstChild(txn *Transaction) (*Output, error) {
151+ if f.branch == nil {
152+ return nil, ErrNilBranch
153+ }
154+ if txn == nil || txn.ptr == nil {
155+ return nil, ErrNilTransaction
156+ }
157+
158+ child := C.yxmlelem_first_child(f.branch)
159+ if child == nil {
160+ return nil, nil // No children
161+ }
162+ return &Output{ptr: child}, nil
163+}
164+
165+// MarshalJSONWithTxn implements JSON marshaling for XmlElement.
166+// Returns JSON in the format: {"type": "tag", "attrs": {...}, "children": [...]}
167+func (e *XmlElement) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
168+ if e.branch == nil {
169+ return nil, ErrNilBranch
170+ }
171+ if txn == nil || txn.ptr == nil {
172+ return nil, ErrNilTransaction
173+ }
174+
175+ // Get tag name
176+ tag, err := e.Tag()
177+ if err != nil {
178+ return nil, fmt.Errorf("failed to get tag: %w", err)
179+ }
180+
181+ // Collect attributes
182+ attrs := make(map[string]interface{})
183+ attrIter, err := e.AttrIter(txn)
184+ if err != nil {
185+ return nil, err
186+ }
187+ defer attrIter.Destroy()
188+
189+ for {
190+ attr, err := attrIter.Next()
191+ if err != nil {
192+ return nil, err
193+ }
194+ if attr == nil {
195+ break
196+ }
197+
198+ val, err := attr.Value.ToInterface()
199+ if err != nil {
200+ // Fallback: use string representation
201+ if s, ok := attr.Value.String(); ok {
202+ val = s
203+ } else {
204+ val = nil
205+ }
206+ }
207+ attrs[attr.Name] = val
208+ attr.Value.Destroy()
209+ }
210+
211+ // Collect children
212+ var children []interface{}
213+ childIter, err := e.ChildIter(txn)
214+ if err != nil {
215+ return nil, err
216+ }
217+ defer childIter.Destroy()
218+
219+ for {
220+ child, err := childIter.Next()
221+ if err != nil {
222+ return nil, err
223+ }
224+ if child == nil {
225+ break
226+ }
227+
228+ childJSON, err := e.marshalXmlChild(child, txn)
229+ if err != nil {
230+ child.Destroy()
231+ return nil, err
232+ }
233+ children = append(children, childJSON)
234+ child.Destroy()
235+ }
236+
237+ // Build result
238+ result := map[string]interface{}{
239+ "type": tag,
240+ "attrs": attrs,
241+ "children": children,
242+ }
243+
244+ return json.Marshal(result)
245+}
246+
247+// marshalXmlChild marshals a single XML child node
248+func (e *XmlElement) marshalXmlChild(out *Output, txn *Transaction) (interface{}, error) {
249+ switch out.Tag() {
250+ case TagXmlElem:
251+ branch := C.youtput_read_yxmlelem(out.ptr)
252+ if branch == nil {
253+ return nil, fmt.Errorf("failed to read child element")
254+ }
255+ child := &XmlElement{branch: branch}
256+ defer child.Destroy()
257+
258+ jsonBytes, err := child.MarshalJSONWithTxn(txn)
259+ if err != nil {
260+ return nil, err
261+ }
262+
263+ var result map[string]interface{}
264+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
265+ return nil, err
266+ }
267+ return result, nil
268+
269+ case TagXmlText:
270+ branch := C.youtput_read_yxmltext(out.ptr)
271+ if branch == nil {
272+ return nil, fmt.Errorf("failed to read child text")
273+ }
274+ text := &XmlText{branch: branch}
275+ defer text.Destroy()
276+
277+ content, err := text.String(txn)
278+ if err != nil {
279+ return nil, err
280+ }
281+
282+ // Collect formatting attributes if any
283+ attrs := make(map[string]interface{})
284+ attrIter, err := text.AttrIter(txn)
285+ if err == nil {
286+ defer attrIter.Destroy()
287+ for {
288+ attr, err := attrIter.Next()
289+ if err != nil {
290+ break
291+ }
292+ if attr == nil {
293+ break
294+ }
295+ val, _ := attr.Value.ToInterface()
296+ attrs[attr.Name] = val
297+ attr.Value.Destroy()
298+ }
299+ }
300+
301+ result := map[string]interface{}{
302+ "type": "text",
303+ "content": content,
304+ }
305+ if len(attrs) > 0 {
306+ result["attrs"] = attrs
307+ }
308+ return result, nil
309+
310+ default:
311+ return nil, fmt.Errorf("unsupported child type: %v", out.Tag())
312+ }
313+}
314+
315+// MarshalJSONWithTxn implements JSON marshaling for XmlFragment.
316+// Returns an array of child elements since fragments don't have a tag name.
317+func (f *XmlFragment) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
318+ if f.branch == nil {
319+ return nil, ErrNilBranch
320+ }
321+ if txn == nil || txn.ptr == nil {
322+ return nil, ErrNilTransaction
323+ }
324+
325+ var children []interface{}
326+
327+ // Get first child
328+ child, err := f.FirstChild(txn)
329+ if err != nil {
330+ return nil, err
331+ }
332+
333+ for child != nil {
334+ childJSON, err := f.marshalXmlFragmentChild(child, txn)
335+ if err != nil {
336+ child.Destroy()
337+ return nil, err
338+ }
339+ children = append(children, childJSON)
340+
341+ // Get next sibling - need to extract branch from output
342+ var currentBranch *C.Branch
343+ switch child.Tag() {
344+ case TagXmlElem:
345+ currentBranch = C.youtput_read_yxmlelem(child.ptr)
346+ case TagXmlText:
347+ currentBranch = C.youtput_read_yxmltext(child.ptr)
348+ }
349+ child.Destroy()
350+
351+ if currentBranch == nil {
352+ break
353+ }
354+
355+ next := C.yxml_next_sibling(currentBranch, txn.ptr)
356+ if next == nil {
357+ break
358+ }
359+ child = &Output{ptr: next}
360+ }
361+
362+ return json.Marshal(children)
363+}
364+
365+// marshalXmlFragmentChild marshals a child from a fragment
366+func (f *XmlFragment) marshalXmlFragmentChild(out *Output, txn *Transaction) (interface{}, error) {
367+ // Same logic as XmlElement.marshalXmlChild
368+ switch out.Tag() {
369+ case TagXmlElem:
370+ branch := C.youtput_read_yxmlelem(out.ptr)
371+ if branch == nil {
372+ return nil, fmt.Errorf("failed to read child element")
373+ }
374+ child := &XmlElement{branch: branch}
375+ defer child.Destroy()
376+
377+ jsonBytes, err := child.MarshalJSONWithTxn(txn)
378+ if err != nil {
379+ return nil, err
380+ }
381+
382+ var result map[string]interface{}
383+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
384+ return nil, err
385+ }
386+ return result, nil
387+
388+ case TagXmlText:
389+ branch := C.youtput_read_yxmltext(out.ptr)
390+ if branch == nil {
391+ return nil, fmt.Errorf("failed to read child text")
392+ }
393+ text := &XmlText{branch: branch}
394+ defer text.Destroy()
395+
396+ content, err := text.String(txn)
397+ if err != nil {
398+ return nil, err
399+ }
400+
401+ result := map[string]interface{}{
402+ "type": "text",
403+ "content": content,
404+ }
405+ return result, nil
406+
407+ default:
408+ return nil, fmt.Errorf("unsupported child type: %v", out.Tag())
409+ }
410+}
+444,
-0
1@@ -1,6 +1,7 @@
2 package ygo_test
3
4 import (
5+ "encoding/json"
6 "github.com/BTBurke/ygo"
7 "strings"
8 "testing"
9@@ -203,3 +204,446 @@ func TestXmlNestedElements(t *testing.T) {
10 t.Errorf("expected 'Nested content', got '%s'", content)
11 }
12 }
13+
14+func TestXmlElementAttrIter(t *testing.T) {
15+ doc, err := ygo.NewDoc()
16+ if err != nil {
17+ t.Fatalf("failed to create doc: %v", err)
18+ }
19+ defer doc.Destroy()
20+
21+ frag, err := doc.GetXmlFragment("test")
22+ if err != nil {
23+ t.Fatalf("failed to get fragment: %v", err)
24+ }
25+ defer frag.Destroy()
26+
27+ var elem *ygo.XmlElement
28+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
29+ var err error
30+ elem, err = frag.InsertElement(txn, 0, "div")
31+ if err != nil {
32+ return err
33+ }
34+
35+ // Set attributes
36+ elem.SetAttribute(txn, "class", ygo.String("container"))
37+ elem.SetAttribute(txn, "id", ygo.String("main"))
38+ elem.SetAttribute(txn, "data-count", ygo.Int(42))
39+ return nil
40+ })
41+ if err != nil {
42+ t.Fatalf("transaction failed: %v", err)
43+ }
44+ defer elem.Destroy()
45+
46+ // Test attribute iteration
47+ var attrs map[string]interface{}
48+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
49+ var err error
50+ attrs, err = collectXmlAttrs(elem, txn)
51+ return err
52+ })
53+ if err != nil {
54+ t.Fatalf("attribute iteration failed: %v", err)
55+ }
56+
57+ if len(attrs) != 3 {
58+ t.Errorf("expected 3 attributes, got %d", len(attrs))
59+ }
60+ if attrs["class"] != "container" {
61+ t.Errorf("expected class=container, got %v", attrs["class"])
62+ }
63+ if attrs["id"] != "main" {
64+ t.Errorf("expected id=main, got %v", attrs["id"])
65+ }
66+ // Int values are returned as int64
67+ if attrs["data-count"] != int64(42) {
68+ t.Errorf("expected data-count=42, got %v", attrs["data-count"])
69+ }
70+}
71+
72+func collectXmlAttrs(elem *ygo.XmlElement, txn *ygo.Transaction) (map[string]interface{}, error) {
73+ iter, err := elem.AttrIter(txn)
74+ if err != nil {
75+ return nil, err
76+ }
77+ defer iter.Destroy()
78+
79+ attrs := make(map[string]interface{})
80+ for {
81+ attr, err := iter.Next()
82+ if err != nil {
83+ return nil, err
84+ }
85+ if attr == nil {
86+ break
87+ }
88+
89+ // Convert value to appropriate Go type
90+ var value interface{}
91+ switch attr.Value.Tag() {
92+ case ygo.TagJSONBool:
93+ if v, ok := attr.Value.Bool(); ok {
94+ value = v
95+ }
96+ case ygo.TagJSONNum:
97+ if v, ok := attr.Value.Float(); ok {
98+ value = v
99+ }
100+ case ygo.TagJSONInt:
101+ if v, ok := attr.Value.Int(); ok {
102+ value = v
103+ }
104+ case ygo.TagJSONStr:
105+ if v, ok := attr.Value.String(); ok {
106+ value = v
107+ }
108+ case ygo.TagJSONNull:
109+ value = nil
110+ default:
111+ value = nil
112+ }
113+
114+ attrs[attr.Name] = value
115+ attr.Value.Destroy()
116+ }
117+ return attrs, nil
118+}
119+
120+// Helper to collect children from iterator
121+func collectChildren(elem *ygo.XmlElement, txn *ygo.Transaction) ([]string, error) {
122+ iter, err := elem.ChildIter(txn)
123+ if err != nil {
124+ return nil, err
125+ }
126+
127+ var children []string
128+ for {
129+ child, err := iter.Next()
130+ if err != nil {
131+ return nil, err
132+ }
133+ if child == nil {
134+ break
135+ }
136+
137+ // Get tag type
138+ tag := child.Tag()
139+ switch tag {
140+ case ygo.TagXmlElem:
141+ children = append(children, "elem")
142+ case ygo.TagXmlText:
143+ children = append(children, "text")
144+ default:
145+ children = append(children, "unknown")
146+ }
147+ child.Destroy()
148+ }
149+ return children, nil
150+}
151+
152+func TestXmlChildIterEmpty(t *testing.T) {
153+ doc, err := ygo.NewDoc()
154+ if err != nil {
155+ t.Fatalf("failed to create doc: %v", err)
156+ }
157+ defer doc.Destroy()
158+
159+ frag, err := doc.GetXmlFragment("test")
160+ if err != nil {
161+ t.Fatalf("failed to get fragment: %v", err)
162+ }
163+ defer frag.Destroy()
164+
165+ var elem *ygo.XmlElement
166+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
167+ var err error
168+ elem, err = frag.InsertElement(txn, 0, "div")
169+ return err
170+ })
171+ if err != nil {
172+ t.Fatalf("transaction failed: %v", err)
173+ }
174+ defer elem.Destroy()
175+
176+ var children []string
177+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
178+ var err error
179+ children, err = collectChildren(elem, txn)
180+ return err
181+ })
182+ if err != nil {
183+ t.Fatalf("child iteration failed: %v", err)
184+ }
185+
186+ if len(children) != 0 {
187+ t.Errorf("expected 0 children for empty element, got %d: %v", len(children), children)
188+ }
189+}
190+
191+func TestXmlChildIterSingle(t *testing.T) {
192+ doc, err := ygo.NewDoc()
193+ if err != nil {
194+ t.Fatalf("failed to create doc: %v", err)
195+ }
196+ defer doc.Destroy()
197+
198+ frag, err := doc.GetXmlFragment("test")
199+ if err != nil {
200+ t.Fatalf("failed to get fragment: %v", err)
201+ }
202+ defer frag.Destroy()
203+
204+ var elem *ygo.XmlElement
205+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
206+ var err error
207+ elem, err = frag.InsertElement(txn, 0, "div")
208+ if err != nil {
209+ return err
210+ }
211+
212+ // Insert single child
213+ child, err := elem.InsertElement(txn, 0, "span")
214+ if err != nil {
215+ return err
216+ }
217+ child.Destroy()
218+
219+ return nil
220+ })
221+ if err != nil {
222+ t.Fatalf("transaction failed: %v", err)
223+ }
224+ defer elem.Destroy()
225+
226+ var children []string
227+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
228+ var err error
229+ children, err = collectChildren(elem, txn)
230+ return err
231+ })
232+ if err != nil {
233+ t.Fatalf("child iteration failed: %v", err)
234+ }
235+
236+ if len(children) != 1 {
237+ t.Errorf("expected 1 child, got %d: %v", len(children), children)
238+ }
239+ if children[0] != "elem" {
240+ t.Errorf("expected first child to be 'elem', got '%s'", children[0])
241+ }
242+}
243+
244+func TestXmlChildIterMultiple(t *testing.T) {
245+ doc, err := ygo.NewDoc()
246+ if err != nil {
247+ t.Fatalf("failed to create doc: %v", err)
248+ }
249+ defer doc.Destroy()
250+
251+ frag, err := doc.GetXmlFragment("test")
252+ if err != nil {
253+ t.Fatalf("failed to get fragment: %v", err)
254+ }
255+ defer frag.Destroy()
256+
257+ var elem *ygo.XmlElement
258+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
259+ var err error
260+ elem, err = frag.InsertElement(txn, 0, "div")
261+ if err != nil {
262+ return err
263+ }
264+
265+ // Insert multiple children
266+ for _, tag := range []string{"p", "span", "div"} {
267+ child, err := elem.InsertElement(txn, 0, tag)
268+ if err != nil {
269+ return err
270+ }
271+ child.Destroy()
272+ }
273+
274+ return nil
275+ })
276+ if err != nil {
277+ t.Fatalf("transaction failed: %v", err)
278+ }
279+ defer elem.Destroy()
280+
281+ var children []string
282+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
283+ var err error
284+ children, err = collectChildren(elem, txn)
285+ return err
286+ })
287+ if err != nil {
288+ t.Fatalf("child iteration failed: %v", err)
289+ }
290+
291+ if len(children) != 3 {
292+ t.Errorf("expected 3 children, got %d: %v", len(children), children)
293+ }
294+
295+ // Children should be in order: div, span, p (inserted at index 0)
296+ expected := []string{"elem", "elem", "elem"}
297+ for i, exp := range expected {
298+ if children[i] != exp {
299+ t.Errorf("expected child[%d] to be '%s', got '%s'", i, exp, children[i])
300+ }
301+ }
302+}
303+
304+func TestXmlChildIterMixed(t *testing.T) {
305+ doc, err := ygo.NewDoc()
306+ if err != nil {
307+ t.Fatalf("failed to create doc: %v", err)
308+ }
309+ defer doc.Destroy()
310+
311+ frag, err := doc.GetXmlFragment("test")
312+ if err != nil {
313+ t.Fatalf("failed to get fragment: %v", err)
314+ }
315+ defer frag.Destroy()
316+
317+ var elem *ygo.XmlElement
318+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
319+ var err error
320+ elem, err = frag.InsertElement(txn, 0, "div")
321+ if err != nil {
322+ return err
323+ }
324+
325+ // Insert mixed content: text, element, text, element
326+ txt1, err := elem.InsertText(txn, 0)
327+ if err != nil {
328+ return err
329+ }
330+ txt1.Insert(txn, 0, "Hello ")
331+ txt1.Destroy()
332+
333+ span, err := elem.InsertElement(txn, 1, "span")
334+ if err != nil {
335+ return err
336+ }
337+ span.Destroy()
338+
339+ txt2, err := elem.InsertText(txn, 2)
340+ if err != nil {
341+ return err
342+ }
343+ txt2.Insert(txn, 0, " World")
344+ txt2.Destroy()
345+
346+ p, err := elem.InsertElement(txn, 3, "p")
347+ if err != nil {
348+ return err
349+ }
350+ p.Destroy()
351+
352+ return nil
353+ })
354+ if err != nil {
355+ t.Fatalf("transaction failed: %v", err)
356+ }
357+ defer elem.Destroy()
358+
359+ var children []string
360+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
361+ var err error
362+ children, err = collectChildren(elem, txn)
363+ return err
364+ })
365+ if err != nil {
366+ t.Fatalf("child iteration failed: %v", err)
367+ }
368+
369+ if len(children) != 4 {
370+ t.Errorf("expected 4 children, got %d: %v", len(children), children)
371+ }
372+
373+ // Check all children are present in order
374+ expected := []string{"text", "elem", "text", "elem"}
375+ for i, exp := range expected {
376+ if children[i] != exp {
377+ t.Errorf("expected child[%d] to be '%s', got '%s'", i, exp, children[i])
378+ }
379+ }
380+}
381+
382+func TestXmlElementMarshalJSON(t *testing.T) {
383+ doc, err := ygo.NewDoc()
384+ if err != nil {
385+ t.Fatalf("failed to create doc: %v", err)
386+ }
387+ defer doc.Destroy()
388+
389+ frag, err := doc.GetXmlFragment("test")
390+ if err != nil {
391+ t.Fatalf("failed to get fragment: %v", err)
392+ }
393+ defer frag.Destroy()
394+
395+ var elem *ygo.XmlElement
396+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
397+ var err error
398+ elem, err = frag.InsertElement(txn, 0, "div")
399+ if err != nil {
400+ return err
401+ }
402+
403+ elem.SetAttribute(txn, "class", ygo.String("container"))
404+
405+ // Add text child
406+ text, err := elem.InsertText(txn, 0)
407+ if err != nil {
408+ return err
409+ }
410+ text.Insert(txn, 0, "Hello World")
411+
412+ return nil
413+ })
414+ if err != nil {
415+ t.Fatalf("transaction failed: %v", err)
416+ }
417+ defer elem.Destroy()
418+
419+ // Marshal to JSON
420+ var jsonBytes []byte
421+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
422+ var err error
423+ jsonBytes, err = elem.MarshalJSONWithTxn(txn)
424+ return err
425+ })
426+ if err != nil {
427+ t.Fatalf("marshal failed: %v", err)
428+ }
429+
430+ // Verify JSON structure
431+ var result map[string]interface{}
432+ if err := json.Unmarshal(jsonBytes, &result); err != nil {
433+ t.Fatalf("failed to unmarshal: %v", err)
434+ }
435+
436+ if result["type"] != "div" {
437+ t.Errorf("expected type=div, got %v", result["type"])
438+ }
439+
440+ attrs, ok := result["attrs"].(map[string]interface{})
441+ if !ok {
442+ t.Fatalf("expected attrs to be a map, got %T", result["attrs"])
443+ }
444+ if attrs["class"] != "container" {
445+ t.Errorf("expected class=container, got %v", attrs["class"])
446+ }
447+
448+ children, ok := result["children"].([]interface{})
449+ if !ok {
450+ t.Fatalf("expected children to be an array, got %T", result["children"])
451+ }
452+ if len(children) != 1 {
453+ t.Errorf("expected 1 child, got %d", len(children))
454+ }
455+}
M
yjs.go
+1,
-1
1@@ -2,7 +2,7 @@ package ygo
2
3 /*
4 #cgo CFLAGS: -I${SRCDIR}/lib/include
5-#cgo LDFLAGS: -L${SRCDIR}/lib -lyrs -ldl -lm
6+#cgo LDFLAGS: -L${SRCDIR}/lib/linux-amd64 -lyrs -ldl -lm
7
8 #include <stdlib.h>
9 #include "libyrs.h"