Repository: jomei/notionapi
Pull request #52 (2021-11-30 00:38:47)

Added Property array to support unmarshalling
This is response to #51.

This adds the `PropertyArray` type and defines an unmarshalling function
for it. This will allow for the unmarshalling of Rollup arrays.

This also moves some switch logic in `parsePageProperties` to a
`decodeProperty` function which so it can be used in the unmarshalling
of `PropertyArray`.

jomei: Hey, @kertox662!!
Could you please provide a test case to reproduce this error?

Patch:
diff --git a/block.go b/block.go
index fc6e2bd..348e89d 100644
--- a/block.go
+++ b/block.go
@@ -141,6 +141,12 @@ type AppendBlockChildrenRequest struct {
 
 type Block interface {
 	GetType() BlockType
+	GetID() BlockID
+	GetObject() ObjectType
+	GetCreatedTime() *time.Time
+	GetLastEditedTime() *time.Time
+	GetHasChildren() bool
+	GetArchived() bool
 }
 
 // BasicBlock defines the common fields of all Notion block types.
@@ -160,6 +166,30 @@ func (b BasicBlock) GetType() BlockType {
 	return b.Type
 }
 
+func (b BasicBlock) GetID() BlockID {
+	return b.ID
+}
+
+func (b BasicBlock) GetObject() ObjectType {
+	return b.Object
+}
+
+func (b BasicBlock) GetCreatedTime() *time.Time {
+	return b.CreatedTime
+}
+
+func (b BasicBlock) GetLastEditedTime() *time.Time {
+	return b.LastEditedTime
+}
+
+func (b BasicBlock) GetHasChildren() bool {
+	return b.HasChildren
+}
+
+func (b BasicBlock) GetArchived() bool {
+	return b.Archived
+}
+
 type ParagraphBlock struct {
 	BasicBlock
 	Paragraph Paragraph `json:"paragraph"`
diff --git a/database.go b/database.go
index a60f657..5d7d73a 100644
--- a/database.go
+++ b/database.go
@@ -110,6 +110,8 @@ type Database struct {
 	CreatedTime    time.Time  `json:"created_time"`
 	LastEditedTime time.Time  `json:"last_edited_time"`
 	Title          []RichText `json:"title"`
+	Parent         Parent     `json:"parent"`
+	URL            string     `json:"url"`
 	// Properties is a map of property configurations that defines what Page.Properties each page of the database can use
 	Properties PropertyConfigs `json:"properties"`
 }
diff --git a/property.go b/property.go
index a578297..7d33082 100644
--- a/property.go
+++ b/property.go
@@ -14,6 +14,30 @@ type Property interface {
 	GetType() PropertyType
 }
 
+type PropertyArray []Property
+
+func (arr *PropertyArray) UnmarshalJSON(data []byte) error {
+	var err error
+	mapArr := make([]map[string]interface{}, 0)
+	if err = json.Unmarshal(data, &mapArr); err != nil {
+		return err
+	}
+
+	result := make([]Property, len(mapArr))
+	for i, prop := range mapArr {
+		if result[i], err = decodeProperty(prop); err != nil {
+			return err
+		}
+	}
+
+	if err = json.Unmarshal(data, &result); err != nil {
+		return err
+	}
+
+	*arr = result
+	return nil
+}
+
 type TitleProperty struct {
 	ID    PropertyID   `json:"id,omitempty"`
 	Type  PropertyType `json:"type,omitempty"`
@@ -138,10 +162,10 @@ type RollupProperty struct {
 type RollupType string
 
 type Rollup struct {
-	Type   RollupType  `json:"type,omitempty"`
-	Number float64     `json:"number,omitempty"`
-	Date   *DateObject `json:"date,omitempty"`
-	Array  []Property  `json:"array,omitempty"` //todo: unmarshal
+	Type   RollupType    `json:"type,omitempty"`
+	Number float64       `json:"number,omitempty"`
+	Date   *DateObject   `json:"date,omitempty"`
+	Array  PropertyArray `json:"array,omitempty"`
 }
 
 func (p RollupProperty) GetType() PropertyType {
@@ -267,52 +291,11 @@ func (p *Properties) UnmarshalJSON(data []byte) error {
 func parsePageProperties(raw map[string]interface{}) (map[string]Property, error) {
 	result := make(map[string]Property)
 	for k, v := range raw {
-		var p Property
 		switch rawProperty := v.(type) {
 		case map[string]interface{}:
-			switch PropertyType(rawProperty["type"].(string)) {
-			case PropertyTypeTitle:
-				p = &TitleProperty{}
-			case PropertyTypeRichText:
-				p = &RichTextProperty{}
-			case PropertyTypeText:
-				p = &RichTextProperty{}
-			case PropertyTypeNumber:
-				p = &NumberProperty{}
-			case PropertyTypeSelect:
-				p = &SelectProperty{}
-			case PropertyTypeMultiSelect:
-				p = &MultiSelectProperty{}
-			case PropertyTypeDate:
-				p = &DateProperty{}
-			case PropertyTypeFormula:
-				p = &FormulaProperty{}
-			case PropertyTypeRelation:
-				p = &RelationProperty{}
-			case PropertyTypeRollup:
-				p = &RollupProperty{}
-			case PropertyTypePeople:
-				p = &PeopleProperty{}
-			case PropertyTypeFiles:
-				p = &FilesProperty{}
-			case PropertyTypeCheckbox:
-				p = &CheckboxProperty{}
-			case PropertyTypeURL:
-				p = &URLProperty{}
-			case PropertyTypeEmail:
-				p = &EmailProperty{}
-			case PropertyTypePhoneNumber:
-				p = &PhoneNumberProperty{}
-			case PropertyTypeCreatedTime:
-				p = &CreatedTimeProperty{}
-			case PropertyTypeCreatedBy:
-				p = &CreatedByProperty{}
-			case PropertyTypeLastEditedTime:
-				p = &LastEditedTimeProperty{}
-			case PropertyTypeLastEditedBy:
-				p = &LastEditedByProperty{}
-			default:
-				return nil, errors.New(fmt.Sprintf("unsupported property type: %s", rawProperty["type"].(string)))
+			p, err := decodeProperty(rawProperty)
+			if err != nil {
+				return nil, err
 			}
 			b, err := json.Marshal(rawProperty)
 			if err != nil {
@@ -331,3 +314,53 @@ func parsePageProperties(raw map[string]interface{}) (map[string]Property, error
 
 	return result, nil
 }
+
+func decodeProperty(raw map[string]interface{}) (Property, error) {
+	var p Property
+	switch PropertyType(raw["type"].(string)) {
+	case PropertyTypeTitle:
+		p = &TitleProperty{}
+	case PropertyTypeRichText:
+		p = &RichTextProperty{}
+	case PropertyTypeText:
+		p = &RichTextProperty{}
+	case PropertyTypeNumber:
+		p = &NumberProperty{}
+	case PropertyTypeSelect:
+		p = &SelectProperty{}
+	case PropertyTypeMultiSelect:
+		p = &MultiSelectProperty{}
+	case PropertyTypeDate:
+		p = &DateProperty{}
+	case PropertyTypeFormula:
+		p = &FormulaProperty{}
+	case PropertyTypeRelation:
+		p = &RelationProperty{}
+	case PropertyTypeRollup:
+		p = &RollupProperty{}
+	case PropertyTypePeople:
+		p = &PeopleProperty{}
+	case PropertyTypeFiles:
+		p = &FilesProperty{}
+	case PropertyTypeCheckbox:
+		p = &CheckboxProperty{}
+	case PropertyTypeURL:
+		p = &URLProperty{}
+	case PropertyTypeEmail:
+		p = &EmailProperty{}
+	case PropertyTypePhoneNumber:
+		p = &PhoneNumberProperty{}
+	case PropertyTypeCreatedTime:
+		p = &CreatedTimeProperty{}
+	case PropertyTypeCreatedBy:
+		p = &CreatedByProperty{}
+	case PropertyTypeLastEditedTime:
+		p = &LastEditedTimeProperty{}
+	case PropertyTypeLastEditedBy:
+		p = &LastEditedByProperty{}
+	default:
+		return nil, errors.New(fmt.Sprintf("unsupported property type: %s", raw["type"].(string)))
+	}
+
+	return p, nil
+}


Test patch:
diff --git a/database_test.go b/database_test.go
index f6f3e96..30c2025 100644
--- a/database_test.go
+++ b/database_test.go
@@ -243,6 +243,10 @@ func TestDatabaseClient(t *testing.T) {
 					ID:             "some_id",
 					CreatedTime:    timestamp,
 					LastEditedTime: timestamp,
+					Parent: notionapi.Parent{
+						Type:   "page_id",
+						PageID: "48f8fee9-cd79-4180-bc2f-ec0398253067",
+					},
 					Title: []notionapi.RichText{
 						{
 							Type: notionapi.ObjectTypeText,
@@ -307,6 +311,10 @@ func TestDatabaseClient(t *testing.T) {
 					ID:             "some_id",
 					CreatedTime:    timestamp,
 					LastEditedTime: timestamp,
+					Parent: notionapi.Parent{
+						Type:   "page_id",
+						PageID: "a7744006-9233-4cd0-bf44-3a49de2c01b5",
+					},
 					Title: []notionapi.RichText{
 						{
 							Type:        notionapi.ObjectTypeText,
diff --git a/page_test.go b/page_test.go
index 1b3e9d9..6647c28 100644
--- a/page_test.go
+++ b/page_test.go
@@ -43,6 +43,84 @@ func TestPageClient(t *testing.T) {
 					},
 					Archived: false,
 					URL:      "some_url",
+					Properties: notionapi.Properties{
+						"Tags": &notionapi.MultiSelectProperty{
+							ID:   ";s|V",
+							Type: "multi_select",
+							MultiSelect: []notionapi.Option{
+								{
+									ID:    "some_id",
+									Name:  "tag",
+									Color: "blue",
+								},
+							},
+						},
+						"Some another column": &notionapi.PeopleProperty{
+							ID:   "rJt\\",
+							Type: "people",
+							People: []notionapi.User{
+								{
+									Object:    "user",
+									ID:        "some_id",
+									Name:      "some name",
+									AvatarURL: "some.url",
+									Type:      "person",
+									Person: &notionapi.Person{
+										Email: "some@email.com",
+									},
+								},
+							},
+						},
+						"SomeColumn": &notionapi.RichTextProperty{
+							ID:   "~j_@",
+							Type: "rich_text",
+							RichText: []notionapi.RichText{
+								{
+									Type: "text",
+									Text: notionapi.Text{
+										Content: "some text",
+									},
+									Annotations: &notionapi.Annotations{
+										Color: "default",
+									},
+									PlainText: "some text",
+								},
+							},
+						},
+						"Name": &notionapi.TitleProperty{
+							ID:   "title",
+							Type: "title",
+							Title: []notionapi.RichText{
+								{
+									Type: "text",
+									Text: notionapi.Text{
+										Content: "Hello",
+									},
+									Annotations: &notionapi.Annotations{
+										Color: "default",
+									},
+									PlainText: "Hello",
+								},
+							},
+						},
+						"RollupArray": &notionapi.RollupProperty{
+							ID:   "abcd",
+							Type: "rollup",
+							Rollup: notionapi.Rollup{
+								Type: "array",
+								Array: notionapi.PropertyArray{
+									&notionapi.NumberProperty{
+										Type:   "number",
+										Number: 42.2,
+									},
+									&notionapi.NumberProperty{
+										Type:   "number",
+										Number: 56,
+									},
+								},
+							},
+						},
+					},
 				},
 			},
 			{
@@ -77,8 +155,7 @@ func TestPageClient(t *testing.T) {
 					}
 					return
 				}
-				// TODO: remove properties from comparing for a while. Have to compare with interface somehow
-				got.Properties = nil
+
 				if !reflect.DeepEqual(got, tt.want) {
 					t.Errorf("Get() got = %v, want %v", got, tt.want)
 				}
diff --git a/testdata/page_get.json b/testdata/page_get.json
index 3a1ff89..4143297 100644
--- a/testdata/page_get.json
+++ b/testdata/page_get.json
@@ -13,75 +13,81 @@
     "Tags": {
       "id": ";s|V",
       "type": "multi_select",
-      "multi_select": [
-        {
-          "id": "some_id",
-          "name": "tag",
-          "color": "blue"
-        }
-      ]
+      "multi_select": [{
+        "id": "some_id",
+        "name": "tag",
+        "color": "blue"
+      }]
     },
     "Some another column": {
       "id": "rJt\\",
       "type": "people",
-      "people": [
-        {
-          "object": "user",
-          "id": "some_id",
-          "name": "some name",
-          "avatar_url": "some.url",
-          "type": "person",
-          "person": {
-            "email": "some@email.com"
-          }
+      "people": [{
+        "object": "user",
+        "id": "some_id",
+        "name": "some name",
+        "avatar_url": "some.url",
+        "type": "person",
+        "person": {
+          "email": "some@email.com"
         }
-      ]
+      }]
     },
     "SomeColumn": {
       "id": "~j_@",
       "type": "rich_text",
-      "rich_text": [
-        {
-          "type": "text",
-          "text": {
-            "content": "some text",
-            "link": null
-          },
-          "annotations": {
-            "bold": false,
-            "italic": false,
-            "strikethrough": false,
-            "underline": false,
-            "code": false,
-            "color": "default"
-          },
-          "plain_text": "some text",
-          "href": null
-        }
-      ]
+      "rich_text": [{
+        "type": "text",
+        "text": {
+          "content": "some text",
+          "link": null
+        },
+        "annotations": {
+          "bold": false,
+          "italic": false,
+          "strikethrough": false,
+          "underline": false,
+          "code": false,
+          "color": "default"
+        },
+        "plain_text": "some text",
+        "href": null
+      }]
     },
     "Name": {
       "id": "title",
       "type": "title",
-      "title": [
-        {
-          "type": "text",
-          "text": {
-            "content": "Hello",
-            "link": null
-          },
-          "annotations": {
-            "bold": false,
-            "italic": false,
-            "strikethrough": false,
-            "underline": false,
-            "code": false,
-            "color": "default"
-          },
-          "plain_text": "Hello",
-          "href": null
-        }
-      ]
+      "title": [{
+        "type": "text",
+        "text": {
+          "content": "Hello",
+          "link": null
+        },
+        "annotations": {
+          "bold": false,
+          "italic": false,
+          "strikethrough": false,
+          "underline": false,
+          "code": false,
+          "color": "default"
+        },
+        "plain_text": "Hello",
+        "href": null
+      }]
+    },
+    "RollupArray": {
+      "id": "abcd",
+      "type": "rollup",
+      "rollup": {
+        "type": "array",
+        "array": [{
+          "type": "number",
+          "number": 42.2
+        }, {
+          "type": "number",
+          "number": 56
+        }]
+      }
     }
   }
-}
+}
\ No newline at end of file