cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

feat(pub): CBOR to JSON

+362 -8
+60 -8
internal/services/atproto.go
··· 59 59 } 60 60 } 61 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 + 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 - recordBytes, err := json.Marshal(doc) 409 + jsonBytes, err := json.Marshal(doc) 382 410 if err != nil { 383 - return nil, fmt.Errorf("failed to marshal document: %w", err) 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) 384 424 } 385 425 386 426 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) 427 + if err := cbor.Unmarshal(cborBytes, record); err != nil { 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 - recordBytes, err := json.Marshal(doc) 483 + jsonBytes, err := json.Marshal(doc) 444 484 if err != nil { 445 - return nil, fmt.Errorf("failed to marshal document: %w", err) 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) 446 498 } 447 499 448 500 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) 501 + if err := cbor.Unmarshal(cborBytes, record); err != nil { 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 + "encoding/json" 5 6 "strings" 6 7 "testing" 7 8 "time" 8 9 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 + } 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 + } 1440 1742 } 1441 1743 }) 1442 1744 })