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}