output.go

  1package ygo
  2
  3/*
  4#include "libyrs.h"
  5#include <stdlib.h>
  6*/
  7import "C"
  8import (
  9	"encoding/json"
 10	"fmt"
 11	"unsafe"
 12)
 13
 14// ValueTag identifies the type of a value in Yjs.
 15type ValueTag int8
 16
 17const (
 18	// JSON types
 19	TagJSON      ValueTag = C.Y_JSON       // JSON string to deserialize
 20	TagJSONBool  ValueTag = C.Y_JSON_BOOL  // Boolean
 21	TagJSONNum   ValueTag = C.Y_JSON_NUM   // 64-bit float
 22	TagJSONInt   ValueTag = C.Y_JSON_INT   // 64-bit int
 23	TagJSONStr   ValueTag = C.Y_JSON_STR   // String
 24	TagJSONBuf   ValueTag = C.Y_JSON_BUF   // Binary
 25	TagJSONArr   ValueTag = C.Y_JSON_ARR   // JSON array
 26	TagJSONMap   ValueTag = C.Y_JSON_MAP   // JSON map
 27	TagJSONNull  ValueTag = C.Y_JSON_NULL  // Null
 28	TagJSONUndef ValueTag = C.Y_JSON_UNDEF // Undefined
 29
 30	// Yjs shared types
 31	TagArray     ValueTag = C.Y_ARRAY     // YArray
 32	TagMap       ValueTag = C.Y_MAP       // YMap
 33	TagText      ValueTag = C.Y_TEXT      // YText
 34	TagXmlElem   ValueTag = C.Y_XML_ELEM  // YXmlElement
 35	TagXmlText   ValueTag = C.Y_XML_TEXT  // YXmlText
 36	TagXmlFrag   ValueTag = C.Y_XML_FRAG  // YXmlFragment
 37	TagDoc       ValueTag = C.Y_DOC       // Nested document
 38	TagWeakLink  ValueTag = C.Y_WEAK_LINK // Weak reference
 39	TagUndefined ValueTag = C.Y_UNDEFINED // Undefined reference
 40)
 41
 42// Output represents a value read from Yjs types.
 43type Output struct {
 44	ptr *C.YOutput
 45}
 46
 47// Tag returns the type tag of this output value.
 48func (o *Output) Tag() ValueTag {
 49	if o.ptr == nil {
 50		return TagJSONUndef
 51	}
 52	return ValueTag(o.ptr.tag)
 53}
 54
 55// Destroy releases resources associated with this output.
 56func (o *Output) Destroy() {
 57	if o.ptr != nil {
 58		C.youtput_destroy(o.ptr)
 59		o.ptr = nil
 60	}
 61}
 62
 63// Branch returns the underlying branch pointer for shared types.
 64// Only valid for outputs with TagArray, TagMap, TagText, TagXmlElem, TagXmlText, or TagXmlFrag.
 65// Returns nil for non-shared types.
 66func (o *Output) Branch() unsafe.Pointer {
 67	if o.ptr == nil {
 68		return nil
 69	}
 70	switch o.Tag() {
 71	case TagArray:
 72		branch := C.youtput_read_yarray(o.ptr)
 73		return unsafe.Pointer(branch)
 74	case TagMap:
 75		branch := C.youtput_read_ymap(o.ptr)
 76		return unsafe.Pointer(branch)
 77	default:
 78		return nil
 79	}
 80}
 81
 82// ToMap converts the output to a Map.
 83// Returns error if the output is not a Y.Map.
 84func (o *Output) ToMap() (*Map, error) {
 85	if o.Tag() != TagMap {
 86		return nil, fmt.Errorf("output is not a Y.Map (got %v)", o.Tag())
 87	}
 88	branch := C.youtput_read_ymap(o.ptr)
 89	if branch == nil {
 90		return nil, fmt.Errorf("failed to read ymap from output")
 91	}
 92	return &Map{branch: branch}, nil
 93}
 94
 95// ToArray converts the output to an Array.
 96// Returns error if the output is not a Y.Array.
 97func (o *Output) ToArray() (*Array, error) {
 98	if o.Tag() != TagArray {
 99		return nil, fmt.Errorf("output is not a Y.Array (got %v)", o.Tag())
100	}
101	branch := C.youtput_read_yarray(o.ptr)
102	if branch == nil {
103		return nil, fmt.Errorf("failed to read yarray from output")
104	}
105	return &Array{branch: branch}, nil
106}
107
108// ToXmlElement converts the output to an XmlElement.
109// Returns error if the output is not a Y.XmlElement.
110func (o *Output) ToXmlElement() (*XmlElement, error) {
111	if o.Tag() != TagXmlElem {
112		return nil, fmt.Errorf("output is not a Y.XmlElement (got %v)", o.Tag())
113	}
114	branch := C.youtput_read_yxmlelem(o.ptr)
115	if branch == nil {
116		return nil, fmt.Errorf("failed to read yxmlelem from output")
117	}
118	return &XmlElement{branch: branch}, nil
119}
120
121// IsNull returns true if the value is null.
122func (o *Output) IsNull() bool {
123	return o.Tag() == TagJSONNull
124}
125
126// IsUndefined returns true if the value is undefined.
127func (o *Output) IsUndefined() bool {
128	return o.Tag() == TagJSONUndef || o.Tag() == TagUndefined
129}
130
131// Bool reads the value as a boolean.
132func (o *Output) Bool() (bool, bool) {
133	if o.Tag() != TagJSONBool {
134		return false, false
135	}
136	ptr := C.youtput_read_bool(o.ptr)
137	if ptr == nil {
138		return false, false
139	}
140	return *ptr != 0, true
141}
142
143// Float reads the value as a float64.
144func (o *Output) Float() (float64, bool) {
145	if o.Tag() != TagJSONNum {
146		return 0, false
147	}
148	ptr := C.youtput_read_float(o.ptr)
149	if ptr == nil {
150		return 0, false
151	}
152	return float64(*ptr), true
153}
154
155// Int reads the value as an int64.
156func (o *Output) Int() (int64, bool) {
157	if o.Tag() != TagJSONInt {
158		return 0, false
159	}
160	ptr := C.youtput_read_long(o.ptr)
161	if ptr == nil {
162		return 0, false
163	}
164	return int64(*ptr), true
165}
166
167// String reads the value as a string.
168func (o *Output) String() (string, bool) {
169	if o.Tag() != TagJSONStr {
170		return "", false
171	}
172	ptr := C.youtput_read_string(o.ptr)
173	if ptr == nil {
174		return "", false
175	}
176	// Note: The string is owned by the YOutput and will be freed when
177	// youtput_destroy is called. We must NOT free it here.
178	return C.GoString(ptr), true
179}
180
181// Binary reads the value as a byte slice.
182func (o *Output) Binary() ([]byte, bool) {
183	if o.Tag() != TagJSONBuf {
184		return nil, false
185	}
186	ptr := C.youtput_read_binary(o.ptr)
187	if ptr == nil {
188		return nil, false
189	}
190	// The length is in o.ptr.len
191	length := int(o.ptr.len)
192	result := make([]byte, length)
193	copy(result, (*[1 << 30]byte)(unsafe.Pointer(ptr))[:length:length])
194	return result, true
195}
196
197// JSONBytes returns the raw JSON bytes for TagJSON, TagJSONArr, or TagJSONMap values.
198// For TagJSON, returns the raw JSON string directly.
199// For TagJSONArr, converts the array to JSON bytes.
200// For TagJSONMap, converts the map to JSON bytes.
201// This is useful for unmarshaling directly into structs without going through interface{}.
202// Returns nil and false if the output is not a JSON type.
203func (o *Output) JSONBytes() ([]byte, bool) {
204	switch o.Tag() {
205	case TagJSON:
206		ptr := C.youtput_read_string(o.ptr)
207		if ptr == nil {
208			return nil, false
209		}
210		// The string is owned by the YOutput and will be freed when
211		// youtput_destroy is called. We must NOT free it here.
212		// Return as byte slice for direct JSON unmarshaling.
213		return []byte(C.GoString(ptr)), true
214	case TagJSONArr:
215		// Convert the JSON array back to bytes
216		arr, err := o.convertToJSONArray()
217		if err != nil {
218			return nil, false
219		}
220		bytes, err := json.Marshal(arr)
221		if err != nil {
222			return nil, false
223		}
224		return bytes, true
225	case TagJSONMap:
226		// Convert the JSON map back to bytes
227		m, err := o.convertToJSONMap()
228		if err != nil {
229			return nil, false
230		}
231		bytes, err := json.Marshal(m)
232		if err != nil {
233			return nil, false
234		}
235		return bytes, true
236	default:
237		return nil, false
238	}
239}
240
241// ToInterface converts the output to a Go interface{} value based on its type tag.
242// Returns nil and an error if the type cannot be converted.
243func (o *Output) ToInterface() (interface{}, error) {
244	if o == nil {
245		return nil, fmt.Errorf("nil output")
246	}
247	if o.ptr == nil {
248		return nil, fmt.Errorf("nil output pointer")
249	}
250
251	switch o.Tag() {
252	case TagJSONNull:
253		return nil, nil
254	case TagJSONUndef, TagUndefined:
255		return nil, nil
256	case TagJSONBool:
257		if v, ok := o.Bool(); ok {
258			return v, nil
259		}
260		return nil, fmt.Errorf("failed to read boolean")
261	case TagJSONNum:
262		if v, ok := o.Float(); ok {
263			return v, nil
264		}
265		return nil, fmt.Errorf("failed to read float")
266	case TagJSONInt:
267		if v, ok := o.Int(); ok {
268			return v, nil
269		}
270		return nil, fmt.Errorf("failed to read int")
271	case TagJSONStr:
272		if v, ok := o.String(); ok {
273			return v, nil
274		}
275		return nil, fmt.Errorf("failed to read string")
276	case TagJSONBuf:
277		if v, ok := o.Binary(); ok {
278			return v, nil
279		}
280		return nil, fmt.Errorf("failed to read binary")
281	case TagJSONMap:
282		return o.convertToJSONMap()
283	case TagJSONArr:
284		return o.convertToJSONArray()
285	case TagJSON:
286		// Raw JSON string - read as string then parse
287		if v, ok := o.String(); ok {
288			return json.Marshal(v)
289		}
290		return nil, fmt.Errorf("failed to read raw JSON string")
291	case TagArray, TagMap, TagText, TagXmlElem, TagXmlText, TagXmlFrag, TagDoc, TagWeakLink:
292		// Shared types - return error, caller should handle specially
293		return nil, fmt.Errorf("shared types must be handled separately: %v", o.Tag())
294	default:
295		return nil, fmt.Errorf("unknown type tag: %v", o.Tag())
296	}
297}
298
299// JSON unmarshals the Output value into the given receiver.
300// The receiver must be a pointer to a Go struct.
301// Returns error if value is null/undefined, or unmarshal fails.
302// Never sets receiver to nil.
303func (o *Output) JSON(receiver interface{}) error {
304	if o == nil || o.ptr == nil {
305		return fmt.Errorf("nil output")
306	}
307
308	tag := o.Tag()
309
310	switch tag {
311	case TagJSONNull, TagJSONUndef, TagUndefined:
312		return fmt.Errorf("cannot unmarshal %v value", tag)
313	case TagJSONStr:
314		// Plain string - unmarshal as JSON
315		if v, ok := o.String(); ok {
316			return json.Unmarshal([]byte(v), receiver)
317		}
318		return fmt.Errorf("failed to read string")
319	case TagJSON, TagJSONMap, TagJSONArr:
320		// Convert to interface{} then marshal/unmarshal
321		v, err := o.ToInterface()
322		if err != nil {
323			return fmt.Errorf("failed to convert to interface: %w", err)
324		}
325		data, err := json.Marshal(v)
326		if err != nil {
327			return fmt.Errorf("failed to marshal interface: %w", err)
328		}
329		return json.Unmarshal(data, receiver)
330	case TagJSONBool:
331		if v, ok := o.Bool(); ok {
332			return json.Unmarshal([]byte(fmt.Sprintf("%t", v)), receiver)
333		}
334		return fmt.Errorf("failed to read boolean")
335	case TagJSONNum:
336		if v, ok := o.Float(); ok {
337			return json.Unmarshal([]byte(fmt.Sprintf("%f", v)), receiver)
338		}
339		return fmt.Errorf("failed to read float")
340	case TagJSONInt:
341		if v, ok := o.Int(); ok {
342			return json.Unmarshal([]byte(fmt.Sprintf("%d", v)), receiver)
343		}
344		return fmt.Errorf("failed to read int")
345	default:
346		return fmt.Errorf("unsupported tag for JSON unmarshal: %v", tag)
347	}
348}
349
350// convertToJSONMap converts Output with TagJSONMap to Go map
351func (o *Output) convertToJSONMap() (map[string]interface{}, error) {
352	entriesPtr := C.youtput_read_json_map(o.ptr)
353	if entriesPtr == nil {
354		if int(o.ptr.len) == 0 {
355			return make(map[string]interface{}), nil
356		}
357		return nil, fmt.Errorf("failed to read json map")
358	}
359
360	length := int(o.ptr.len)
361	result := make(map[string]interface{}, length)
362
363	for i := 0; i < length; i++ {
364		entry := (*[1 << 30]C.YMapEntry)(unsafe.Pointer(entriesPtr))[i]
365		key := C.GoString(entry.key)
366		val := &Output{ptr: entry.value}
367		valInterface, err := val.ToInterface()
368		if err != nil {
369			return nil, fmt.Errorf("key %q: %w", key, err)
370		}
371		result[key] = valInterface
372	}
373	return result, nil
374}
375
376// convertToJSONArray converts Output with TagJSONArr to Go slice
377func (o *Output) convertToJSONArray() ([]interface{}, error) {
378	outputsPtr := C.youtput_read_json_array(o.ptr)
379	if outputsPtr == nil {
380		if int(o.ptr.len) == 0 {
381			return []interface{}{}, nil
382		}
383		return nil, fmt.Errorf("failed to read json array")
384	}
385
386	length := int(o.ptr.len)
387	result := make([]interface{}, 0, length)
388
389	for i := 0; i < length; i++ {
390		ptr := (*C.YOutput)(unsafe.Pointer(uintptr(unsafe.Pointer(outputsPtr)) + uintptr(i)*unsafe.Sizeof(C.YOutput{})))
391		out := &Output{ptr: ptr}
392		valInterface, err := out.ToInterface()
393		if err != nil {
394			return nil, fmt.Errorf("index %d: %w", i, err)
395		}
396		result = append(result, valInterface)
397	}
398	return result, nil
399}