tangled
alpha
login
or
join now
desertthunder.dev
/
noteleaf
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐
charm
leaflet
readability
golang
29
fork
atom
overview
issues
2
pulls
pipelines
feat(pub): CBOR to JSON
desertthunder.dev
3 months ago
3db8ba04
cec8e1be
+362
-8
2 changed files
expand all
collapse all
unified
split
internal
services
atproto.go
atproto_test.go
+60
-8
internal/services/atproto.go
···
59
}
60
}
61
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
62
// PublicationWithMeta combines a publication with its metadata
63
type PublicationWithMeta struct {
64
Publication public.Publication
···
378
379
doc.Type = collection
380
381
-
recordBytes, err := json.Marshal(doc)
382
if err != nil {
383
-
return nil, fmt.Errorf("failed to marshal document: %w", err)
0
0
0
0
0
0
0
0
0
0
0
0
384
}
385
386
record := &lexutil.LexiconTypeDecoder{}
387
-
if err := record.UnmarshalJSON(recordBytes); err != nil {
388
-
return nil, fmt.Errorf("failed to unmarshal document to lexicon type: %w", err)
389
}
390
391
input := &atproto.RepoCreateRecord_Input{
···
440
441
doc.Type = collection
442
443
-
recordBytes, err := json.Marshal(doc)
444
if err != nil {
445
-
return nil, fmt.Errorf("failed to marshal document: %w", err)
0
0
0
0
0
0
0
0
0
0
0
0
446
}
447
448
record := &lexutil.LexiconTypeDecoder{}
449
-
if err := record.UnmarshalJSON(recordBytes); err != nil {
450
-
return nil, fmt.Errorf("failed to unmarshal document to lexicon type: %w", err)
451
}
452
453
input := &atproto.RepoPutRecord_Input{
···
59
}
60
}
61
62
+
// convertJSONToCBORCompatible recursively converts JSON-compatible data structures to CBOR types
63
+
//
64
+
// This converts map[string]any to map[any]any to allow proper CBOR encoding for AT Protocol
65
+
func convertJSONToCBORCompatible(data any) any {
66
+
switch v := data.(type) {
67
+
case map[string]any:
68
+
result := make(map[any]any, len(v))
69
+
for key, value := range v {
70
+
result[key] = convertJSONToCBORCompatible(value)
71
+
}
72
+
return result
73
+
case map[any]any:
74
+
result := make(map[any]any, len(v))
75
+
for key, value := range v {
76
+
result[key] = convertJSONToCBORCompatible(value)
77
+
}
78
+
return result
79
+
case []any:
80
+
result := make([]any, len(v))
81
+
for i, item := range v {
82
+
result[i] = convertJSONToCBORCompatible(item)
83
+
}
84
+
return result
85
+
default:
86
+
return v
87
+
}
88
+
}
89
+
90
// PublicationWithMeta combines a publication with its metadata
91
type PublicationWithMeta struct {
92
Publication public.Publication
···
406
407
doc.Type = collection
408
409
+
jsonBytes, err := json.Marshal(doc)
410
if err != nil {
411
+
return nil, fmt.Errorf("failed to marshal document to JSON: %w", err)
412
+
}
413
+
414
+
var jsonData map[string]any
415
+
if err := json.Unmarshal(jsonBytes, &jsonData); err != nil {
416
+
return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err)
417
+
}
418
+
419
+
cborCompatible := convertJSONToCBORCompatible(jsonData)
420
+
421
+
cborBytes, err := cbor.Marshal(cborCompatible)
422
+
if err != nil {
423
+
return nil, fmt.Errorf("failed to marshal to CBOR: %w", err)
424
}
425
426
record := &lexutil.LexiconTypeDecoder{}
427
+
if err := cbor.Unmarshal(cborBytes, record); err != nil {
428
+
return nil, fmt.Errorf("failed to unmarshal CBOR to lexicon type: %w", err)
429
}
430
431
input := &atproto.RepoCreateRecord_Input{
···
480
481
doc.Type = collection
482
483
+
jsonBytes, err := json.Marshal(doc)
484
if err != nil {
485
+
return nil, fmt.Errorf("failed to marshal document to JSON: %w", err)
486
+
}
487
+
488
+
var jsonData map[string]any
489
+
if err := json.Unmarshal(jsonBytes, &jsonData); err != nil {
490
+
return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err)
491
+
}
492
+
493
+
cborCompatible := convertJSONToCBORCompatible(jsonData)
494
+
495
+
cborBytes, err := cbor.Marshal(cborCompatible)
496
+
if err != nil {
497
+
return nil, fmt.Errorf("failed to marshal to CBOR: %w", err)
498
}
499
500
record := &lexutil.LexiconTypeDecoder{}
501
+
if err := cbor.Unmarshal(cborBytes, record); err != nil {
502
+
return nil, fmt.Errorf("failed to unmarshal CBOR to lexicon type: %w", err)
503
}
504
505
input := &atproto.RepoPutRecord_Input{
+302
internal/services/atproto_test.go
···
2
3
import (
4
"context"
0
5
"strings"
6
"testing"
7
"time"
8
0
9
"github.com/stormlightlabs/noteleaf/internal/public"
10
)
11
···
1437
1438
if svc.IsAuthenticated() {
1439
t.Error("Expected IsAuthenticated to return false after close")
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1440
}
1441
})
1442
})
···
2
3
import (
4
"context"
5
+
"encoding/json"
6
"strings"
7
"testing"
8
"time"
9
10
+
"github.com/fxamacker/cbor/v2"
11
"github.com/stormlightlabs/noteleaf/internal/public"
12
)
13
···
1439
1440
if svc.IsAuthenticated() {
1441
t.Error("Expected IsAuthenticated to return false after close")
1442
+
}
1443
+
})
1444
+
})
1445
+
1446
+
t.Run("CBOR Conversion Functions", func(t *testing.T) {
1447
+
t.Run("convertCBORToJSONCompatible handles simple map", func(t *testing.T) {
1448
+
input := map[any]any{
1449
+
"key1": "value1",
1450
+
"key2": 42,
1451
+
"key3": true,
1452
+
}
1453
+
1454
+
result := convertCBORToJSONCompatible(input)
1455
+
1456
+
mapResult, ok := result.(map[string]any)
1457
+
if !ok {
1458
+
t.Fatal("Expected result to be map[string]any")
1459
+
}
1460
+
1461
+
if mapResult["key1"] != "value1" {
1462
+
t.Errorf("Expected key1='value1', got '%v'", mapResult["key1"])
1463
+
}
1464
+
if mapResult["key2"] != 42 {
1465
+
t.Errorf("Expected key2=42, got %v", mapResult["key2"])
1466
+
}
1467
+
if mapResult["key3"] != true {
1468
+
t.Errorf("Expected key3=true, got %v", mapResult["key3"])
1469
+
}
1470
+
})
1471
+
1472
+
t.Run("convertCBORToJSONCompatible handles nested maps", func(t *testing.T) {
1473
+
input := map[any]any{
1474
+
"outer": map[any]any{
1475
+
"inner": map[any]any{
1476
+
"deep": "value",
1477
+
},
1478
+
},
1479
+
}
1480
+
1481
+
result := convertCBORToJSONCompatible(input)
1482
+
1483
+
mapResult, ok := result.(map[string]any)
1484
+
if !ok {
1485
+
t.Fatal("Expected result to be map[string]any")
1486
+
}
1487
+
1488
+
outer, ok := mapResult["outer"].(map[string]any)
1489
+
if !ok {
1490
+
t.Fatal("Expected outer to be map[string]any")
1491
+
}
1492
+
1493
+
inner, ok := outer["inner"].(map[string]any)
1494
+
if !ok {
1495
+
t.Fatal("Expected inner to be map[string]any")
1496
+
}
1497
+
1498
+
if inner["deep"] != "value" {
1499
+
t.Errorf("Expected deep='value', got '%v'", inner["deep"])
1500
+
}
1501
+
})
1502
+
1503
+
t.Run("convertCBORToJSONCompatible handles arrays", func(t *testing.T) {
1504
+
input := []any{
1505
+
"string",
1506
+
42,
1507
+
map[any]any{"nested": "map"},
1508
+
[]any{"nested", "array"},
1509
+
}
1510
+
1511
+
result := convertCBORToJSONCompatible(input)
1512
+
1513
+
arrayResult, ok := result.([]any)
1514
+
if !ok {
1515
+
t.Fatal("Expected result to be []any")
1516
+
}
1517
+
1518
+
if len(arrayResult) != 4 {
1519
+
t.Fatalf("Expected 4 elements, got %d", len(arrayResult))
1520
+
}
1521
+
1522
+
if arrayResult[0] != "string" {
1523
+
t.Errorf("Expected arrayResult[0]='string', got '%v'", arrayResult[0])
1524
+
}
1525
+
1526
+
nestedMap, ok := arrayResult[2].(map[string]any)
1527
+
if !ok {
1528
+
t.Fatal("Expected arrayResult[2] to be map[string]any")
1529
+
}
1530
+
if nestedMap["nested"] != "map" {
1531
+
t.Errorf("Expected nested='map', got '%v'", nestedMap["nested"])
1532
+
}
1533
+
1534
+
nestedArray, ok := arrayResult[3].([]any)
1535
+
if !ok {
1536
+
t.Fatal("Expected arrayResult[3] to be []any")
1537
+
}
1538
+
if len(nestedArray) != 2 {
1539
+
t.Errorf("Expected nested array length 2, got %d", len(nestedArray))
1540
+
}
1541
+
})
1542
+
1543
+
t.Run("convertJSONToCBORCompatible handles simple map", func(t *testing.T) {
1544
+
input := map[string]any{
1545
+
"key1": "value1",
1546
+
"key2": 42,
1547
+
"key3": true,
1548
+
}
1549
+
1550
+
result := convertJSONToCBORCompatible(input)
1551
+
1552
+
mapResult, ok := result.(map[any]any)
1553
+
if !ok {
1554
+
t.Fatal("Expected result to be map[any]any")
1555
+
}
1556
+
1557
+
if mapResult["key1"] != "value1" {
1558
+
t.Errorf("Expected key1='value1', got '%v'", mapResult["key1"])
1559
+
}
1560
+
if mapResult["key2"] != 42 {
1561
+
t.Errorf("Expected key2=42, got %v", mapResult["key2"])
1562
+
}
1563
+
if mapResult["key3"] != true {
1564
+
t.Errorf("Expected key3=true, got %v", mapResult["key3"])
1565
+
}
1566
+
})
1567
+
1568
+
t.Run("convertJSONToCBORCompatible handles nested maps", func(t *testing.T) {
1569
+
input := map[string]any{
1570
+
"outer": map[string]any{
1571
+
"inner": map[string]any{
1572
+
"deep": "value",
1573
+
},
1574
+
},
1575
+
}
1576
+
1577
+
result := convertJSONToCBORCompatible(input)
1578
+
1579
+
mapResult, ok := result.(map[any]any)
1580
+
if !ok {
1581
+
t.Fatal("Expected result to be map[any]any")
1582
+
}
1583
+
1584
+
outer, ok := mapResult["outer"].(map[any]any)
1585
+
if !ok {
1586
+
t.Fatal("Expected outer to be map[any]any")
1587
+
}
1588
+
1589
+
inner, ok := outer["inner"].(map[any]any)
1590
+
if !ok {
1591
+
t.Fatal("Expected inner to be map[any]any")
1592
+
}
1593
+
1594
+
if inner["deep"] != "value" {
1595
+
t.Errorf("Expected deep='value', got '%v'", inner["deep"])
1596
+
}
1597
+
})
1598
+
1599
+
t.Run("convertJSONToCBORCompatible handles arrays", func(t *testing.T) {
1600
+
input := []any{
1601
+
"string",
1602
+
42,
1603
+
map[string]any{"nested": "map"},
1604
+
[]any{"nested", "array"},
1605
+
}
1606
+
1607
+
result := convertJSONToCBORCompatible(input)
1608
+
1609
+
arrayResult, ok := result.([]any)
1610
+
if !ok {
1611
+
t.Fatal("Expected result to be []any")
1612
+
}
1613
+
1614
+
if len(arrayResult) != 4 {
1615
+
t.Fatalf("Expected 4 elements, got %d", len(arrayResult))
1616
+
}
1617
+
1618
+
if arrayResult[0] != "string" {
1619
+
t.Errorf("Expected arrayResult[0]='string', got '%v'", arrayResult[0])
1620
+
}
1621
+
1622
+
nestedMap, ok := arrayResult[2].(map[any]any)
1623
+
if !ok {
1624
+
t.Fatal("Expected arrayResult[2] to be map[any]any")
1625
+
}
1626
+
if nestedMap["nested"] != "map" {
1627
+
t.Errorf("Expected nested='map', got '%v'", nestedMap["nested"])
1628
+
}
1629
+
1630
+
nestedArray, ok := arrayResult[3].([]any)
1631
+
if !ok {
1632
+
t.Fatal("Expected arrayResult[3] to be []any")
1633
+
}
1634
+
if len(nestedArray) != 2 {
1635
+
t.Errorf("Expected nested array length 2, got %d", len(nestedArray))
1636
+
}
1637
+
})
1638
+
1639
+
t.Run("round-trip conversion preserves data", func(t *testing.T) {
1640
+
original := map[string]any{
1641
+
"title": "Test Document",
1642
+
"author": "did:plc:test123",
1643
+
"content": []any{"paragraph1", "paragraph2"},
1644
+
"metadata": map[string]any{
1645
+
"tags": []any{"test", "document"},
1646
+
"published": true,
1647
+
"count": 42,
1648
+
},
1649
+
}
1650
+
1651
+
cborCompatible := convertJSONToCBORCompatible(original)
1652
+
jsonCompatible := convertCBORToJSONCompatible(cborCompatible)
1653
+
1654
+
originalJSON, err := json.Marshal(original)
1655
+
if err != nil {
1656
+
t.Fatalf("Failed to marshal original: %v", err)
1657
+
}
1658
+
1659
+
resultJSON, err := json.Marshal(jsonCompatible)
1660
+
if err != nil {
1661
+
t.Fatalf("Failed to marshal result: %v", err)
1662
+
}
1663
+
1664
+
if string(originalJSON) != string(resultJSON) {
1665
+
t.Errorf("Round-trip conversion changed data.\nOriginal: %s\nResult: %s", originalJSON, resultJSON)
1666
+
}
1667
+
})
1668
+
1669
+
t.Run("Document conversion through CBOR preserves structure", func(t *testing.T) {
1670
+
doc := public.Document{
1671
+
Type: public.TypeDocument,
1672
+
Title: "Test Document",
1673
+
Pages: []public.LinearDocument{
1674
+
{
1675
+
Type: public.TypeLinearDocument,
1676
+
Blocks: []public.BlockWrap{
1677
+
{
1678
+
Type: public.TypeBlock,
1679
+
Block: public.TextBlock{
1680
+
Type: public.TypeTextBlock,
1681
+
Plaintext: "Hello, world!",
1682
+
},
1683
+
},
1684
+
},
1685
+
},
1686
+
},
1687
+
PublishedAt: time.Now().UTC().Format(time.RFC3339),
1688
+
}
1689
+
1690
+
jsonBytes, err := json.Marshal(doc)
1691
+
if err != nil {
1692
+
t.Fatalf("Failed to marshal document to JSON: %v", err)
1693
+
}
1694
+
1695
+
var jsonData map[string]any
1696
+
if err := json.Unmarshal(jsonBytes, &jsonData); err != nil {
1697
+
t.Fatalf("Failed to unmarshal JSON to map: %v", err)
1698
+
}
1699
+
1700
+
cborCompatible := convertJSONToCBORCompatible(jsonData)
1701
+
1702
+
cborBytes, err := cbor.Marshal(cborCompatible)
1703
+
if err != nil {
1704
+
t.Fatalf("Failed to marshal to CBOR: %v", err)
1705
+
}
1706
+
1707
+
var cborData any
1708
+
if err := cbor.Unmarshal(cborBytes, &cborData); err != nil {
1709
+
t.Fatalf("Failed to unmarshal CBOR: %v", err)
1710
+
}
1711
+
1712
+
jsonCompatible := convertCBORToJSONCompatible(cborData)
1713
+
1714
+
finalJSONBytes, err := json.Marshal(jsonCompatible)
1715
+
if err != nil {
1716
+
t.Fatalf("Failed to marshal final JSON: %v", err)
1717
+
}
1718
+
1719
+
var finalDoc public.Document
1720
+
if err := json.Unmarshal(finalJSONBytes, &finalDoc); err != nil {
1721
+
t.Fatalf("Failed to unmarshal final document: %v", err)
1722
+
}
1723
+
1724
+
if finalDoc.Title != doc.Title {
1725
+
t.Errorf("Title changed: expected '%s', got '%s'", doc.Title, finalDoc.Title)
1726
+
}
1727
+
1728
+
if len(finalDoc.Pages) != len(doc.Pages) {
1729
+
t.Errorf("Pages length changed: expected %d, got %d", len(doc.Pages), len(finalDoc.Pages))
1730
+
}
1731
+
1732
+
if len(finalDoc.Pages) > 0 && len(finalDoc.Pages[0].Blocks) > 0 {
1733
+
if textBlock, ok := finalDoc.Pages[0].Blocks[0].Block.(public.TextBlock); ok {
1734
+
expectedBlock := doc.Pages[0].Blocks[0].Block.(public.TextBlock)
1735
+
if textBlock.Plaintext != expectedBlock.Plaintext {
1736
+
t.Errorf("Block plaintext changed: expected '%s', got '%s'",
1737
+
expectedBlock.Plaintext, textBlock.Plaintext)
1738
+
}
1739
+
} else {
1740
+
t.Error("Expected Block to be TextBlock")
1741
+
}
1742
}
1743
})
1744
})