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