M
array.go
+4,
-0
1@@ -251,6 +251,10 @@ func (a *Array) valueToJSON(out *Output, txn *Transaction) (interface{}, error)
2 }
3 return nil, fmt.Errorf("failed to read binary")
4
5+ // JSON array - parsed JSON array
6+ case TagJSONArr:
7+ return out.convertToJSONArray()
8+
9 // Shared types - recursively marshal
10 case TagMap:
11 branch := C.youtput_read_ymap(out.ptr)
M
input.go
+26,
-9
1@@ -5,7 +5,11 @@ package ygo
2 #include <stdlib.h>
3 */
4 import "C"
5-import "unsafe"
6+import (
7+ "encoding/json"
8+ "fmt"
9+ "unsafe"
10+)
11
12 // Input represents a value to be inserted into Yjs types.
13 // This is a wrapper around C YInput with proper memory management.
14@@ -48,6 +52,17 @@ func String(v string) Input {
15 return Input{cInput: C.yinput_string(cStr)}
16 }
17
18+// JSON creates a JSON-embedded input value from any JSON-marshalable Go value.
19+// Returns (Input, error) - the error is non-nil if marshaling fails.
20+func JSON(v interface{}) (Input, error) {
21+ data, err := json.Marshal(v)
22+ if err != nil {
23+ return Input{}, fmt.Errorf("failed to marshal JSON: %w", err)
24+ }
25+ cStr := C.CString(string(data))
26+ return Input{cInput: C.yinput_json(cStr)}, nil
27+}
28+
29 // Binary creates a binary input value.
30 func Binary(v []byte) Input {
31 if len(v) == 0 {
32@@ -72,17 +87,18 @@ func JSONArray(items []Input) Input {
33 }
34
35 // JSONMap creates a JSON map input from key-value pairs.
36+// Returns (Input, error) - the error is non-nil if keys and values have different lengths.
37 // NOTE: This creates C strings that will be leaked. The yffi library stores
38 // pointers to these strings in the YInput struct but doesn't take ownership.
39 // For production use, proper memory management needs to be implemented.
40-func JSONMap(keys []string, values []Input) Input {
41+func JSONMap(keys []string, values []Input) (Input, error) {
42 if len(keys) != len(values) {
43- panic("keys and values must have same length")
44+ return Input{}, fmt.Errorf("keys and values must have same length: got %d keys and %d values", len(keys), len(values))
45 }
46 if len(keys) == 0 {
47 var keysPtr **C.char
48 var valsPtr *C.YInput
49- return Input{cInput: C.yinput_json_map(keysPtr, valsPtr, 0)}
50+ return Input{cInput: C.yinput_json_map(keysPtr, valsPtr, 0)}, nil
51 }
52
53 // Convert keys to C strings - these are NOT freed because
54@@ -104,7 +120,7 @@ func JSONMap(keys []string, values []Input) Input {
55 // the pointers in the YInput struct and uses them later.
56 // TODO: Implement proper memory management for Input allocations
57
58- return result
59+ return result, nil
60 }
61
62 // YArray creates a YArray shared type input.
63@@ -121,17 +137,18 @@ func YArray(items []Input) Input {
64 }
65
66 // YMap creates a YMap shared type input.
67+// Returns (Input, error) - the error is non-nil if keys and values have different lengths.
68 // NOTE: This creates C strings that will be leaked. The yffi library stores
69 // pointers to these strings in the YInput struct but doesn't take ownership.
70 // For production use, proper memory management needs to be implemented.
71-func YMap(keys []string, values []Input) Input {
72+func YMap(keys []string, values []Input) (Input, error) {
73 if len(keys) != len(values) {
74- panic("keys and values must have same length")
75+ return Input{}, fmt.Errorf("keys and values must have same length: got %d keys and %d values", len(keys), len(values))
76 }
77 if len(keys) == 0 {
78 var keysPtr **C.char
79 var valsPtr *C.YInput
80- return Input{cInput: C.yinput_ymap(keysPtr, valsPtr, 0)}
81+ return Input{cInput: C.yinput_ymap(keysPtr, valsPtr, 0)}, nil
82 }
83
84 cKeys := make([]*C.char, len(keys))
85@@ -150,7 +167,7 @@ func YMap(keys []string, values []Input) Input {
86 // the pointers in the YInput struct and uses them later.
87 // TODO: Implement proper memory management for Input allocations
88
89- return result
90+ return result, nil
91 }
92
93 // YText creates a YText shared type input with initial content.
M
map.go
+4,
-0
1@@ -259,6 +259,10 @@ func (m *Map) valueToJSON(out *Output, txn *Transaction) (interface{}, error) {
2 }
3 return nil, fmt.Errorf("failed to read binary")
4
5+ // JSON map - parsed JSON object
6+ case TagJSONMap:
7+ return out.convertToJSONMap()
8+
9 // Shared types - recursively marshal
10 case TagMap:
11 branch := C.youtput_read_ymap(out.ptr)
+11,
-6
1@@ -100,15 +100,20 @@ func TestMapNested(t *testing.T) {
2 err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
3 // Insert nested maps
4 innerValue := ygo.String("Nested data")
5- innerMap := ygo.YMap([]string{"text"}, []ygo.Input{innerValue})
6- outerMap := ygo.YMap([]string{"innerMap"}, []ygo.Input{innerMap})
7+ innerMap, mapErr := ygo.YMap([]string{"text"}, []ygo.Input{innerValue})
8+ if mapErr != nil {
9+ return mapErr
10+ }
11+ outerMap, mapErr := ygo.YMap([]string{"innerMap"}, []ygo.Input{innerMap})
12+ if mapErr != nil {
13+ return mapErr
14+ }
15
16 m.Insert(txn, "outerMap", outerMap)
17
18- var err error
19- length, err = m.Len(txn)
20- if err != nil {
21- return err
22+ length, mapErr = m.Len(txn)
23+ if mapErr != nil {
24+ return mapErr
25 }
26
27 // Retrieve and verify nested structure
+4,
-1
1@@ -28,10 +28,13 @@ func TestMarshalComplexStructure(t *testing.T) {
2 root.Insert(txn, "active", ygo.Bool(true))
3
4 // Add nested map
5- nestedData := ygo.YMap(
6+ nestedData, err := ygo.YMap(
7 []string{"nested_key"},
8 []ygo.Input{ygo.String("nested_value")},
9 )
10+ if err != nil {
11+ return err
12+ }
13 root.Insert(txn, "metadata", nestedData)
14
15 // Add array
+107,
-3
1@@ -6,6 +6,7 @@ package ygo
2 */
3 import "C"
4 import (
5+ "encoding/json"
6 "fmt"
7 "unsafe"
8 )
9@@ -175,9 +176,16 @@ func (o *Output) ToInterface() (interface{}, error) {
10 return v, nil
11 }
12 return nil, fmt.Errorf("failed to read binary")
13- case TagJSON, TagJSONArr, TagJSONMap:
14- // JSON types that require special handling
15- return nil, fmt.Errorf("JSON container types not supported in ToInterface, use document methods: %v", o.Tag())
16+ case TagJSONMap:
17+ return o.convertToJSONMap()
18+ case TagJSONArr:
19+ return o.convertToJSONArray()
20+ case TagJSON:
21+ // Raw JSON string - read as string then parse
22+ if v, ok := o.String(); ok {
23+ return json.Marshal(v)
24+ }
25+ return nil, fmt.Errorf("failed to read raw JSON string")
26 case TagArray, TagMap, TagText, TagXmlElem, TagXmlText, TagXmlFrag, TagDoc, TagWeakLink:
27 // Shared types - return error, caller should handle specially
28 return nil, fmt.Errorf("shared types must be handled separately: %v", o.Tag())
29@@ -185,3 +193,99 @@ func (o *Output) ToInterface() (interface{}, error) {
30 return nil, fmt.Errorf("unknown type tag: %v", o.Tag())
31 }
32 }
33+
34+// JSON unmarshals the Output value into the given receiver.
35+// The receiver must be a pointer to a Go struct.
36+// Returns error if value is null/undefined, or unmarshal fails.
37+// Never sets receiver to nil.
38+func (o *Output) JSON(receiver interface{}) error {
39+ if o == nil || o.ptr == nil {
40+ return fmt.Errorf("nil output")
41+ }
42+
43+ tag := o.Tag()
44+
45+ switch tag {
46+ case TagJSONNull, TagJSONUndef, TagUndefined:
47+ return fmt.Errorf("cannot unmarshal %v value", tag)
48+ case TagJSONStr:
49+ // Plain string - unmarshal as JSON
50+ if v, ok := o.String(); ok {
51+ return json.Unmarshal([]byte(v), receiver)
52+ }
53+ return fmt.Errorf("failed to read string")
54+ case TagJSON, TagJSONMap, TagJSONArr:
55+ // Convert to interface{} then marshal/unmarshal
56+ v, err := o.ToInterface()
57+ if err != nil {
58+ return fmt.Errorf("failed to convert to interface: %w", err)
59+ }
60+ data, err := json.Marshal(v)
61+ if err != nil {
62+ return fmt.Errorf("failed to marshal interface: %w", err)
63+ }
64+ return json.Unmarshal(data, receiver)
65+ case TagJSONBool:
66+ if v, ok := o.Bool(); ok {
67+ return json.Unmarshal([]byte(fmt.Sprintf("%t", v)), receiver)
68+ }
69+ return fmt.Errorf("failed to read boolean")
70+ case TagJSONNum:
71+ if v, ok := o.Float(); ok {
72+ return json.Unmarshal([]byte(fmt.Sprintf("%f", v)), receiver)
73+ }
74+ return fmt.Errorf("failed to read float")
75+ case TagJSONInt:
76+ if v, ok := o.Int(); ok {
77+ return json.Unmarshal([]byte(fmt.Sprintf("%d", v)), receiver)
78+ }
79+ return fmt.Errorf("failed to read int")
80+ default:
81+ return fmt.Errorf("unsupported tag for JSON unmarshal: %v", tag)
82+ }
83+}
84+
85+// convertToJSONMap converts Output with TagJSONMap to Go map
86+func (o *Output) convertToJSONMap() (map[string]interface{}, error) {
87+ entriesPtr := C.youtput_read_json_map(o.ptr)
88+ if entriesPtr == nil {
89+ return nil, fmt.Errorf("failed to read json map")
90+ }
91+
92+ length := int(o.ptr.len)
93+ result := make(map[string]interface{}, length)
94+
95+ for i := 0; i < length; i++ {
96+ entry := (*[1 << 30]C.YMapEntry)(unsafe.Pointer(entriesPtr))[i]
97+ key := C.GoString(entry.key)
98+ val := &Output{ptr: entry.value}
99+ valInterface, err := val.ToInterface()
100+ if err != nil {
101+ return nil, fmt.Errorf("key %q: %w", key, err)
102+ }
103+ result[key] = valInterface
104+ }
105+ return result, nil
106+}
107+
108+// convertToJSONArray converts Output with TagJSONArr to Go slice
109+func (o *Output) convertToJSONArray() ([]interface{}, error) {
110+ outputsPtr := C.youtput_read_json_array(o.ptr)
111+ if outputsPtr == nil {
112+ return nil, fmt.Errorf("failed to read json array")
113+ }
114+
115+ length := int(o.ptr.len)
116+ result := make([]interface{}, 0, length)
117+
118+ for i := 0; i < length; i++ {
119+ ptr := (*C.YOutput)(unsafe.Pointer(uintptr(unsafe.Pointer(outputsPtr)) + uintptr(i)*unsafe.Sizeof(C.YOutput{})))
120+ out := &Output{ptr: ptr}
121+ valInterface, err := out.ToInterface()
122+ if err != nil {
123+ return nil, fmt.Errorf("index %d: %w", i, err)
124+ }
125+ result = append(result, valInterface)
126+ }
127+ return result, nil
128+}
+222,
-0
1@@ -194,3 +194,225 @@ func TestOutputEdgeCases(t *testing.T) {
2 }
3 })
4 }
5+
6+func TestOutputJSON(t *testing.T) {
7+ t.Run("unmarshal string struct", func(t *testing.T) {
8+ doc, err := ygo.NewDoc()
9+ if err != nil {
10+ t.Fatalf("failed to create doc: %v", err)
11+ }
12+ defer doc.Destroy()
13+
14+ m, err := doc.GetMap("test")
15+ if err != nil {
16+ t.Fatalf("failed to get map: %v", err)
17+ }
18+ defer m.Destroy()
19+
20+ type Person struct {
21+ Name string `json:"name"`
22+ Age int `json:"age"`
23+ }
24+
25+ p, err := ygo.JSON(Person{Name: "Alice", Age: 30})
26+ if err != nil {
27+ t.Fatalf("JSON failed: %v", err)
28+ }
29+
30+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
31+ return m.Insert(txn, "person", p)
32+ })
33+ if err != nil {
34+ t.Fatalf("transaction failed: %v", err)
35+ }
36+
37+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
38+ out, err := m.Get(txn, "person")
39+ if err != nil {
40+ return err
41+ }
42+ defer out.Destroy()
43+
44+ var person Person
45+ if err := out.JSON(&person); err != nil {
46+ t.Fatalf("JSON unmarshal failed: %v", err)
47+ }
48+
49+ if person.Name != "Alice" {
50+ t.Errorf("expected name 'Alice', got %q", person.Name)
51+ }
52+ if person.Age != 30 {
53+ t.Errorf("expected age 30, got %d", person.Age)
54+ }
55+ return nil
56+ })
57+ if err != nil {
58+ t.Fatalf("read transaction failed: %v", err)
59+ }
60+ })
61+
62+ t.Run("unmarshal bool", func(t *testing.T) {
63+ doc, err := ygo.NewDoc()
64+ if err != nil {
65+ t.Fatalf("failed to create doc: %v", err)
66+ }
67+ defer doc.Destroy()
68+
69+ m, err := doc.GetMap("test")
70+ if err != nil {
71+ t.Fatalf("failed to get map: %v", err)
72+ }
73+ defer m.Destroy()
74+
75+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
76+ return m.Insert(txn, "flag", ygo.Bool(true))
77+ })
78+ if err != nil {
79+ t.Fatalf("transaction failed: %v", err)
80+ }
81+
82+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
83+ out, err := m.Get(txn, "flag")
84+ if err != nil {
85+ return err
86+ }
87+ defer out.Destroy()
88+
89+ var val bool
90+ if err := out.JSON(&val); err != nil {
91+ t.Fatalf("JSON unmarshal failed: %v", err)
92+ }
93+
94+ if val != true {
95+ t.Errorf("expected true, got %t", val)
96+ }
97+ return nil
98+ })
99+ if err != nil {
100+ t.Fatalf("read transaction failed: %v", err)
101+ }
102+ })
103+
104+ t.Run("unmarshal int", func(t *testing.T) {
105+ doc, err := ygo.NewDoc()
106+ if err != nil {
107+ t.Fatalf("failed to create doc: %v", err)
108+ }
109+ defer doc.Destroy()
110+
111+ m, err := doc.GetMap("test")
112+ if err != nil {
113+ t.Fatalf("failed to get map: %v", err)
114+ }
115+ defer m.Destroy()
116+
117+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
118+ return m.Insert(txn, "count", ygo.Int(42))
119+ })
120+ if err != nil {
121+ t.Fatalf("transaction failed: %v", err)
122+ }
123+
124+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
125+ out, err := m.Get(txn, "count")
126+ if err != nil {
127+ return err
128+ }
129+ defer out.Destroy()
130+
131+ var val int
132+ if err := out.JSON(&val); err != nil {
133+ t.Fatalf("JSON unmarshal failed: %v", err)
134+ }
135+
136+ if val != 42 {
137+ t.Errorf("expected 42, got %d", val)
138+ }
139+ return nil
140+ })
141+ if err != nil {
142+ t.Fatalf("read transaction failed: %v", err)
143+ }
144+ })
145+
146+ t.Run("unmarshal float", func(t *testing.T) {
147+ doc, err := ygo.NewDoc()
148+ if err != nil {
149+ t.Fatalf("failed to create doc: %v", err)
150+ }
151+ defer doc.Destroy()
152+
153+ m, err := doc.GetMap("test")
154+ if err != nil {
155+ t.Fatalf("failed to get map: %v", err)
156+ }
157+ defer m.Destroy()
158+
159+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
160+ return m.Insert(txn, "pi", ygo.Float(3.14))
161+ })
162+ if err != nil {
163+ t.Fatalf("transaction failed: %v", err)
164+ }
165+
166+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
167+ out, err := m.Get(txn, "pi")
168+ if err != nil {
169+ return err
170+ }
171+ defer out.Destroy()
172+
173+ var val float64
174+ if err := out.JSON(&val); err != nil {
175+ t.Fatalf("JSON unmarshal failed: %v", err)
176+ }
177+
178+ if val != 3.14 {
179+ t.Errorf("expected 3.14, got %f", val)
180+ }
181+ return nil
182+ })
183+ if err != nil {
184+ t.Fatalf("read transaction failed: %v", err)
185+ }
186+ })
187+
188+ t.Run("null value returns error", func(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+ m, err := doc.GetMap("test")
196+ if err != nil {
197+ t.Fatalf("failed to get map: %v", err)
198+ }
199+ defer m.Destroy()
200+
201+ err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
202+ return m.Insert(txn, "nil", ygo.Null())
203+ })
204+ if err != nil {
205+ t.Fatalf("transaction failed: %v", err)
206+ }
207+
208+ err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
209+ out, err := m.Get(txn, "nil")
210+ if err != nil {
211+ return err
212+ }
213+ defer out.Destroy()
214+
215+ var val interface{}
216+ err = out.JSON(&val)
217+ if err == nil {
218+ t.Errorf("expected error for null value")
219+ }
220+ return nil
221+ })
222+ if err != nil {
223+ t.Fatalf("read transaction failed: %v", err)
224+ }
225+ })
226+}
+12,
-8
1@@ -66,12 +66,14 @@ func TestTextInsertWithAttributes(t *testing.T) {
2 var length uint32
3
4 err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
5- attrs := ygo.JSONMap([]string{"bold"}, []ygo.Input{ygo.Bool(true)})
6+ attrs, jsonErr := ygo.JSONMap([]string{"bold"}, []ygo.Input{ygo.Bool(true)})
7+ if jsonErr != nil {
8+ return jsonErr
9+ }
10 txt.InsertWithAttributes(txn, 0, "bold text", attrs)
11
12- var err error
13- length, err = txt.Len(txn)
14- return err
15+ length, jsonErr = txt.Len(txn)
16+ return jsonErr
17 })
18 if err != nil {
19 t.Fatalf("transaction failed: %v", err)
20@@ -100,12 +102,14 @@ func TestTextFormat(t *testing.T) {
21 err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
22 txt.Insert(txn, 0, "hello world")
23
24- attrs := ygo.JSONMap([]string{"italic"}, []ygo.Input{ygo.Bool(true)})
25+ attrs, jsonErr := ygo.JSONMap([]string{"italic"}, []ygo.Input{ygo.Bool(true)})
26+ if jsonErr != nil {
27+ return jsonErr
28+ }
29 txt.Format(txn, 0, 5, attrs)
30
31- var err error
32- str, err = txt.String(txn)
33- return err
34+ str, jsonErr = txt.String(txn)
35+ return jsonErr
36 })
37 if err != nil {
38 t.Fatalf("transaction failed: %v", err)