mirror of
https://github.com/dstotijn/go-notion.git
synced 2025-06-08 23:46:12 +02:00
Don't panic when unmarshaling blocks (#50)
* don't panic * Block() returns err * Rework unknown block type logic, add tests --------- Co-authored-by: David Stotijn <dstotijn@gmail.com>
This commit is contained in:
parent
87a7d4c3cd
commit
c6f2e5b343
106
block.go
106
block.go
@ -2,10 +2,14 @@ package notion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnknownBlockType is used when encountering an unknown block type.
|
||||||
|
var ErrUnknownBlockType = errors.New("unknown block type")
|
||||||
|
|
||||||
// Block represents content on the Notion platform.
|
// Block represents content on the Notion platform.
|
||||||
// See: https://developers.notion.com/reference/block
|
// See: https://developers.notion.com/reference/block
|
||||||
type Block interface {
|
type Block interface {
|
||||||
@ -63,6 +67,7 @@ type blockDTO struct {
|
|||||||
LinkToPage *LinkToPageBlock `json:"link_to_page,omitempty"`
|
LinkToPage *LinkToPageBlock `json:"link_to_page,omitempty"`
|
||||||
SyncedBlock *SyncedBlock `json:"synced_block,omitempty"`
|
SyncedBlock *SyncedBlock `json:"synced_block,omitempty"`
|
||||||
Template *TemplateBlock `json:"template,omitempty"`
|
Template *TemplateBlock `json:"template,omitempty"`
|
||||||
|
Unsupported *UnsupportedBlock `json:"unsupported,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type baseBlock struct {
|
type baseBlock struct {
|
||||||
@ -812,6 +817,24 @@ func (b BreadcrumbBlock) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnsupportedBlock struct {
|
||||||
|
baseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (b UnsupportedBlock) MarshalJSON() ([]byte, error) {
|
||||||
|
type (
|
||||||
|
blockAlias UnsupportedBlock
|
||||||
|
dto struct {
|
||||||
|
Unsupported blockAlias `json:"unsupported"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.Marshal(dto{
|
||||||
|
Unsupported: blockAlias(b),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type BlockType string
|
type BlockType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -880,13 +903,21 @@ func (resp *BlockChildrenResponse) UnmarshalJSON(b []byte) error {
|
|||||||
resp.Results = make([]Block, len(dto.Results))
|
resp.Results = make([]Block, len(dto.Results))
|
||||||
|
|
||||||
for i, blockDTO := range dto.Results {
|
for i, blockDTO := range dto.Results {
|
||||||
resp.Results[i] = blockDTO.Block()
|
block, err := blockDTO.Block()
|
||||||
|
if err != nil {
|
||||||
|
// Any error (even `ErrUnknownBlockType`) is explicitly returned.
|
||||||
|
// We don't silently drop blocks with an unknown/unmapped type,
|
||||||
|
// because this could lead to surprises/unexpected list behaviour
|
||||||
|
// for users.
|
||||||
|
return fmt.Errorf("notion: failed to parse block (id: %q, type: %q): %w", blockDTO.ID, blockDTO.Type, err)
|
||||||
|
}
|
||||||
|
resp.Results[i] = block
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dto blockDTO) Block() Block {
|
func (dto blockDTO) Block() (Block, error) {
|
||||||
baseBlock := baseBlock{
|
baseBlock := baseBlock{
|
||||||
id: dto.ID,
|
id: dto.ID,
|
||||||
hasChildren: dto.HasChildren,
|
hasChildren: dto.HasChildren,
|
||||||
@ -919,101 +950,106 @@ func (dto blockDTO) Block() Block {
|
|||||||
switch dto.Type {
|
switch dto.Type {
|
||||||
case BlockTypeParagraph:
|
case BlockTypeParagraph:
|
||||||
dto.Paragraph.baseBlock = baseBlock
|
dto.Paragraph.baseBlock = baseBlock
|
||||||
return dto.Paragraph
|
return dto.Paragraph, nil
|
||||||
case BlockTypeHeading1:
|
case BlockTypeHeading1:
|
||||||
dto.Heading1.baseBlock = baseBlock
|
dto.Heading1.baseBlock = baseBlock
|
||||||
return dto.Heading1
|
return dto.Heading1, nil
|
||||||
case BlockTypeHeading2:
|
case BlockTypeHeading2:
|
||||||
dto.Heading2.baseBlock = baseBlock
|
dto.Heading2.baseBlock = baseBlock
|
||||||
return dto.Heading2
|
return dto.Heading2, nil
|
||||||
case BlockTypeHeading3:
|
case BlockTypeHeading3:
|
||||||
dto.Heading3.baseBlock = baseBlock
|
dto.Heading3.baseBlock = baseBlock
|
||||||
return dto.Heading3
|
return dto.Heading3, nil
|
||||||
case BlockTypeBulletedListItem:
|
case BlockTypeBulletedListItem:
|
||||||
dto.BulletedListItem.baseBlock = baseBlock
|
dto.BulletedListItem.baseBlock = baseBlock
|
||||||
return dto.BulletedListItem
|
return dto.BulletedListItem, nil
|
||||||
case BlockTypeNumberedListItem:
|
case BlockTypeNumberedListItem:
|
||||||
dto.NumberedListItem.baseBlock = baseBlock
|
dto.NumberedListItem.baseBlock = baseBlock
|
||||||
return dto.NumberedListItem
|
return dto.NumberedListItem, nil
|
||||||
case BlockTypeToDo:
|
case BlockTypeToDo:
|
||||||
dto.ToDo.baseBlock = baseBlock
|
dto.ToDo.baseBlock = baseBlock
|
||||||
return dto.ToDo
|
return dto.ToDo, nil
|
||||||
case BlockTypeToggle:
|
case BlockTypeToggle:
|
||||||
dto.Toggle.baseBlock = baseBlock
|
dto.Toggle.baseBlock = baseBlock
|
||||||
return dto.Toggle
|
return dto.Toggle, nil
|
||||||
case BlockTypeChildPage:
|
case BlockTypeChildPage:
|
||||||
dto.ChildPage.baseBlock = baseBlock
|
dto.ChildPage.baseBlock = baseBlock
|
||||||
return dto.ChildPage
|
return dto.ChildPage, nil
|
||||||
case BlockTypeChildDatabase:
|
case BlockTypeChildDatabase:
|
||||||
dto.ChildDatabase.baseBlock = baseBlock
|
dto.ChildDatabase.baseBlock = baseBlock
|
||||||
return dto.ChildDatabase
|
return dto.ChildDatabase, nil
|
||||||
case BlockTypeCallout:
|
case BlockTypeCallout:
|
||||||
dto.Callout.baseBlock = baseBlock
|
dto.Callout.baseBlock = baseBlock
|
||||||
return dto.Callout
|
return dto.Callout, nil
|
||||||
case BlockTypeQuote:
|
case BlockTypeQuote:
|
||||||
dto.Quote.baseBlock = baseBlock
|
dto.Quote.baseBlock = baseBlock
|
||||||
return dto.Quote
|
return dto.Quote, nil
|
||||||
case BlockTypeCode:
|
case BlockTypeCode:
|
||||||
dto.Code.baseBlock = baseBlock
|
dto.Code.baseBlock = baseBlock
|
||||||
return dto.Code
|
return dto.Code, nil
|
||||||
case BlockTypeEmbed:
|
case BlockTypeEmbed:
|
||||||
dto.Embed.baseBlock = baseBlock
|
dto.Embed.baseBlock = baseBlock
|
||||||
return dto.Embed
|
return dto.Embed, nil
|
||||||
case BlockTypeImage:
|
case BlockTypeImage:
|
||||||
dto.Image.baseBlock = baseBlock
|
dto.Image.baseBlock = baseBlock
|
||||||
return dto.Image
|
return dto.Image, nil
|
||||||
case BlockTypeAudio:
|
case BlockTypeAudio:
|
||||||
dto.Audio.baseBlock = baseBlock
|
dto.Audio.baseBlock = baseBlock
|
||||||
return dto.Audio
|
return dto.Audio, nil
|
||||||
case BlockTypeVideo:
|
case BlockTypeVideo:
|
||||||
dto.Video.baseBlock = baseBlock
|
dto.Video.baseBlock = baseBlock
|
||||||
return dto.Video
|
return dto.Video, nil
|
||||||
case BlockTypeFile:
|
case BlockTypeFile:
|
||||||
dto.File.baseBlock = baseBlock
|
dto.File.baseBlock = baseBlock
|
||||||
return dto.File
|
return dto.File, nil
|
||||||
case BlockTypePDF:
|
case BlockTypePDF:
|
||||||
dto.PDF.baseBlock = baseBlock
|
dto.PDF.baseBlock = baseBlock
|
||||||
return dto.PDF
|
return dto.PDF, nil
|
||||||
case BlockTypeBookmark:
|
case BlockTypeBookmark:
|
||||||
dto.Bookmark.baseBlock = baseBlock
|
dto.Bookmark.baseBlock = baseBlock
|
||||||
return dto.Bookmark
|
return dto.Bookmark, nil
|
||||||
case BlockTypeEquation:
|
case BlockTypeEquation:
|
||||||
dto.Equation.baseBlock = baseBlock
|
dto.Equation.baseBlock = baseBlock
|
||||||
return dto.Equation
|
return dto.Equation, nil
|
||||||
case BlockTypeDivider:
|
case BlockTypeDivider:
|
||||||
dto.Divider.baseBlock = baseBlock
|
dto.Divider.baseBlock = baseBlock
|
||||||
return dto.Divider
|
return dto.Divider, nil
|
||||||
case BlockTypeTableOfContents:
|
case BlockTypeTableOfContents:
|
||||||
dto.TableOfContents.baseBlock = baseBlock
|
dto.TableOfContents.baseBlock = baseBlock
|
||||||
return dto.TableOfContents
|
return dto.TableOfContents, nil
|
||||||
case BlockTypeBreadCrumb:
|
case BlockTypeBreadCrumb:
|
||||||
dto.Breadcrumb.baseBlock = baseBlock
|
dto.Breadcrumb.baseBlock = baseBlock
|
||||||
return dto.Breadcrumb
|
return dto.Breadcrumb, nil
|
||||||
case BlockTypeColumnList:
|
case BlockTypeColumnList:
|
||||||
dto.ColumnList.baseBlock = baseBlock
|
dto.ColumnList.baseBlock = baseBlock
|
||||||
return dto.ColumnList
|
return dto.ColumnList, nil
|
||||||
case BlockTypeColumn:
|
case BlockTypeColumn:
|
||||||
dto.Column.baseBlock = baseBlock
|
dto.Column.baseBlock = baseBlock
|
||||||
return dto.Column
|
return dto.Column, nil
|
||||||
case BlockTypeTable:
|
case BlockTypeTable:
|
||||||
dto.Table.baseBlock = baseBlock
|
dto.Table.baseBlock = baseBlock
|
||||||
return dto.Table
|
return dto.Table, nil
|
||||||
case BlockTypeTableRow:
|
case BlockTypeTableRow:
|
||||||
dto.TableRow.baseBlock = baseBlock
|
dto.TableRow.baseBlock = baseBlock
|
||||||
return dto.TableRow
|
return dto.TableRow, nil
|
||||||
case BlockTypeLinkPreview:
|
case BlockTypeLinkPreview:
|
||||||
dto.LinkPreview.baseBlock = baseBlock
|
dto.LinkPreview.baseBlock = baseBlock
|
||||||
return dto.LinkPreview
|
return dto.LinkPreview, nil
|
||||||
case BlockTypeLinkToPage:
|
case BlockTypeLinkToPage:
|
||||||
dto.LinkToPage.baseBlock = baseBlock
|
dto.LinkToPage.baseBlock = baseBlock
|
||||||
return dto.LinkToPage
|
return dto.LinkToPage, nil
|
||||||
case BlockTypeSyncedBlock:
|
case BlockTypeSyncedBlock:
|
||||||
dto.SyncedBlock.baseBlock = baseBlock
|
dto.SyncedBlock.baseBlock = baseBlock
|
||||||
return dto.SyncedBlock
|
return dto.SyncedBlock, nil
|
||||||
case BlockTypeTemplate:
|
case BlockTypeTemplate:
|
||||||
dto.Template.baseBlock = baseBlock
|
dto.Template.baseBlock = baseBlock
|
||||||
return dto.Template
|
return dto.Template, nil
|
||||||
|
case BlockTypeUnsupported:
|
||||||
|
dto.Unsupported.baseBlock = baseBlock
|
||||||
|
return dto.Unsupported, nil
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("type %q is unsupported", dto.Type))
|
// When this case is selected, the block type is supported in the Notion
|
||||||
|
// API, but unknown in this library.
|
||||||
|
return nil, ErrUnknownBlockType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -437,7 +437,7 @@ func (c *Client) FindBlockByID(ctx context.Context, blockID string) (Block, erro
|
|||||||
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto.Block(), nil
|
return dto.Block()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBlock updates a block.
|
// UpdateBlock updates a block.
|
||||||
@ -472,10 +472,11 @@ func (c *Client) UpdateBlock(ctx context.Context, blockID string, block Block) (
|
|||||||
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto.Block(), nil
|
return dto.Block()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBlock sets `archived: true` on a (page) block object.
|
// DeleteBlock sets `archived: true` on a (page) block object.
|
||||||
|
// Will return UnsupportedBlockError if it deletes the block but cannot decode it
|
||||||
// See: https://developers.notion.com/reference/delete-a-block
|
// See: https://developers.notion.com/reference/delete-a-block
|
||||||
func (c *Client) DeleteBlock(ctx context.Context, blockID string) (Block, error) {
|
func (c *Client) DeleteBlock(ctx context.Context, blockID string) (Block, error) {
|
||||||
req, err := c.newRequest(ctx, http.MethodDelete, "/blocks/"+blockID, nil)
|
req, err := c.newRequest(ctx, http.MethodDelete, "/blocks/"+blockID, nil)
|
||||||
@ -500,7 +501,7 @@ func (c *Client) DeleteBlock(ctx context.Context, blockID string) (Block, error)
|
|||||||
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
return nil, fmt.Errorf("notion: failed to parse HTTP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto.Block(), nil
|
return dto.Block()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindUserByID fetches a user by ID.
|
// FindUserByID fetches a user by ID.
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -3074,6 +3075,15 @@ func TestFindBlockChildrenById(t *testing.T) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"id": "5e113754-eae4-4da9-96d2-675977acce99",
|
||||||
|
"created_time": "2021-05-14T09:15:00.000Z",
|
||||||
|
"last_edited_time": "2021-05-14T09:15:00.000Z",
|
||||||
|
"has_children": false,
|
||||||
|
"type": "unsupported",
|
||||||
|
"unsupported": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"next_cursor": "A^hd",
|
"next_cursor": "A^hd",
|
||||||
@ -3102,6 +3112,7 @@ func TestFindBlockChildrenById(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
¬ion.UnsupportedBlock{},
|
||||||
},
|
},
|
||||||
HasMore: true,
|
HasMore: true,
|
||||||
NextCursor: notion.StringPtr("A^hd"),
|
NextCursor: notion.StringPtr("A^hd"),
|
||||||
@ -3114,6 +3125,13 @@ func TestFindBlockChildrenById(t *testing.T) {
|
|||||||
hasChildren: false,
|
hasChildren: false,
|
||||||
archived: false,
|
archived: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "5e113754-eae4-4da9-96d2-675977acce99",
|
||||||
|
createdTime: mustParseTime(time.RFC3339, "2021-05-14T09:15:00.000Z"),
|
||||||
|
lastEditedTime: mustParseTime(time.RFC3339, "2021-05-14T09:15:00.000Z"),
|
||||||
|
hasChildren: false,
|
||||||
|
archived: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expError: nil,
|
expError: nil,
|
||||||
},
|
},
|
||||||
@ -3139,6 +3157,30 @@ func TestFindBlockChildrenById(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expError: nil,
|
expError: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "unknown block type",
|
||||||
|
respBody: func(_ *http.Request) io.Reader {
|
||||||
|
return strings.NewReader(
|
||||||
|
`{
|
||||||
|
"object": "list",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"id": "ae9c9a31-1c1e-4ae2-a5ee-c539a2d43113",
|
||||||
|
"created_time": "2021-05-14T09:15:00.000Z",
|
||||||
|
"last_edited_time": "2021-05-14T09:15:00.000Z",
|
||||||
|
"has_children": false,
|
||||||
|
"type": "foobar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"next_cursor": null,
|
||||||
|
"has_more": false
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
respStatusCode: http.StatusOK,
|
||||||
|
expError: fmt.Errorf(`notion: failed to parse HTTP response: notion: failed to parse block (id: "ae9c9a31-1c1e-4ae2-a5ee-c539a2d43113", type: "foobar"): unknown block type`),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "error response",
|
name: "error response",
|
||||||
respBody: func(_ *http.Request) io.Reader {
|
respBody: func(_ *http.Request) io.Reader {
|
||||||
@ -3200,7 +3242,7 @@ func TestFindBlockChildrenById(t *testing.T) {
|
|||||||
t.Fatalf("error not equal (expected: %v, got: %v)", tt.expError, err)
|
t.Fatalf("error not equal (expected: %v, got: %v)", tt.expError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.expResponse, resp, cmpopts.IgnoreUnexported(notion.ParagraphBlock{})); diff != "" {
|
if diff := cmp.Diff(tt.expResponse, resp, cmpopts.IgnoreUnexported(notion.ParagraphBlock{}, notion.UnsupportedBlock{})); diff != "" {
|
||||||
t.Fatalf("response not equal (-exp, +got):\n%v", diff)
|
t.Fatalf("response not equal (-exp, +got):\n%v", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user