Adds handling for Input and Output JSON-encoded values,
marshaling/unmarshaling to struct
8 files changed,  +390, -27
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)
M map_test.go
+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
M marshal_test.go
+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
M output.go
+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+}
M output_test.go
+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+}
M text_test.go
+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)