Repository: jomei/notionapi (go)

Issue:
Interface to unify Pdf, FileBlock, Image
Currently these share the same underlying structure, though I guess it's not guaranteed to stay that way. To reduce switch-case statements in client code, it would be helpful to define an interface to get these common fields, maybe
```
type DownloadableFileBlock interface {
  Block
  GetURL() string
  GetExpiryTime() *time.Time
} 
```

Pull request:
Add DownloadableFileBlock interface for Pdf, File, and Image blocks
Fixes #164

This PR adds a new `DownloadableFileBlock` interface to unify common functionality between Pdf, FileBlock, and Image blocks.

Changes:
- Added new `DownloadableFileBlock` interface
- Implemented interface for PdfBlock, FileBlock, and ImageBlock
- Added comprehensive test coverage including external URLs
- All existing tests pass

The interface provides:
- `GetURL() string` - returns the URL of the file
- `GetExpiryTime() *time.Time` - returns the expiry time of the file

This reduces switch-case statements in client code by providing a common interface for these similar block types.

Patch:
diff --git a/downloadable_interface.go b/downloadable_interface.go
new file mode 100644
index 0000000..24e8c62
--- /dev/null
+++ b/downloadable_interface.go
@@ -0,0 +1,69 @@
+package notionapi
+
+import "time"
+
+// DownloadableFileBlock is an interface for blocks that can be downloaded
+// such as Pdf, FileBlock, and Image
+type DownloadableFileBlock interface {
+	Block
+	GetURL() string
+	GetExpiryTime() *time.Time
+}
+
+// GetURL implements DownloadableFileBlock interface for PdfBlock
+func (b *PdfBlock) GetURL() string {
+	if b.Pdf.File != nil {
+		return b.Pdf.File.URL
+	}
+	if b.Pdf.External != nil {
+		return b.Pdf.External.URL
+	}
+	return ""
+}
+
+// GetExpiryTime implements DownloadableFileBlock interface for PdfBlock
+func (b *PdfBlock) GetExpiryTime() *time.Time {
+	if b.Pdf.File != nil {
+		return b.Pdf.File.ExpiryTime
+	}
+	return nil
+}
+
+// GetURL implements DownloadableFileBlock interface for FileBlock
+func (b *FileBlock) GetURL() string {
+	if b.File.File != nil {
+		return b.File.File.URL
+	}
+	if b.File.External != nil {
+		return b.File.External.URL
+	}
+	return ""
+}
+
+// GetExpiryTime implements DownloadableFileBlock interface for FileBlock
+func (b *FileBlock) GetExpiryTime() *time.Time {
+	if b.File.File != nil {
+		return b.File.File.ExpiryTime
+	}
+	return nil
+}
+
+// GetURL implements DownloadableFileBlock interface for ImageBlock
+func (b *ImageBlock) GetURL() string {
+	return b.Image.GetURL()
+}
+
+// GetExpiryTime implements DownloadableFileBlock interface for ImageBlock
+func (b *ImageBlock) GetExpiryTime() *time.Time {
+	if b.Image.File != nil {
+		return b.Image.File.ExpiryTime
+	}
+	return nil
+}
+
+// Verify that types implement DownloadableFileBlock interface
+var (
+	_ DownloadableFileBlock = (*PdfBlock)(nil)
+	_ DownloadableFileBlock = (*FileBlock)(nil)
+	_ DownloadableFileBlock = (*ImageBlock)(nil)
+)


Test patch:
diff --git a/downloadable_interface_test.go b/downloadable_interface_test.go
new file mode 100644
index 0000000..50622c9
--- /dev/null
+++ b/downloadable_interface_test.go
@@ -0,0 +1,126 @@
+package notionapi
+
+import (
+	"testing"
+	"time"
+)
+
+func TestPdfBlockImplementsDownloadableFileBlock(t *testing.T) {
+	// Test setup
+	now := time.Now()
+	pdfBlock := &PdfBlock{
+		Pdf: Pdf{
+			File: &FileObject{
+				URL:        "https://example.com/file.pdf",
+				ExpiryTime: &now,
+			},
+		},
+	}
+
+	// Test GetURL
+	if url := pdfBlock.GetURL(); url != "https://example.com/file.pdf" {
+		t.Errorf("Expected URL to be 'https://example.com/file.pdf', got %s", url)
+	}
+
+	// Test GetExpiryTime
+	if expiry := pdfBlock.GetExpiryTime(); expiry != &now {
+		t.Errorf("Expected expiry time to be %v, got %v", now, expiry)
+	}
+}
+
+func TestFileBlockImplementsDownloadableFileBlock(t *testing.T) {
+	// Test setup
+	now := time.Now()
+	fileBlock := &FileBlock{
+		File: BlockFile{
+			File: &FileObject{
+				URL:        "https://example.com/file.txt",
+				ExpiryTime: &now,
+			},
+		},
+	}
+
+	// Test GetURL
+	if url := fileBlock.GetURL(); url != "https://example.com/file.txt" {
+		t.Errorf("Expected URL to be 'https://example.com/file.txt', got %s", url)
+	}
+
+	// Test GetExpiryTime
+	if expiry := fileBlock.GetExpiryTime(); expiry != &now {
+		t.Errorf("Expected expiry time to be %v, got %v", now, expiry)
+	}
+}
+
+func TestImageBlockImplementsDownloadableFileBlock(t *testing.T) {
+	// Test setup
+	now := time.Now()
+	imageBlock := &ImageBlock{
+		Image: Image{
+			File: &FileObject{
+				URL:        "https://example.com/image.jpg",
+				ExpiryTime: &now,
+			},
+		},
+	}
+
+	// Test GetURL
+	if url := imageBlock.GetURL(); url != "https://example.com/image.jpg" {
+		t.Errorf("Expected URL to be 'https://example.com/image.jpg', got %s", url)
+	}
+
+	// Test GetExpiryTime
+	if expiry := imageBlock.GetExpiryTime(); expiry != &now {
+		t.Errorf("Expected expiry time to be %v, got %v", now, expiry)
+	}
+}
+
+func TestExternalURLCases(t *testing.T) {
+	// Test External URLs for each block type
+	testCases := []struct {
+		name     string
+		block    DownloadableFileBlock
+		expected string
+	}{
+		{
+			name: "PDF with external URL",
+			block: &PdfBlock{
+				Pdf: Pdf{
+					External: &FileObject{
+						URL: "https://external.com/file.pdf",
+					},
+				},
+			},
+			expected: "https://external.com/file.pdf",
+		},
+		{
+			name: "File with external URL",
+			block: &FileBlock{
+				File: BlockFile{
+					External: &FileObject{
+						URL: "https://external.com/file.txt",
+					},
+				},
+			},
+			expected: "https://external.com/file.txt",
+		},
+		{
+			name: "Image with external URL",
+			block: &ImageBlock{
+				Image: Image{
+					External: &FileObject{
+						URL: "https://external.com/image.jpg",
+					},
+				},
+			},
+			expected: "https://external.com/image.jpg",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			if url := tc.block.GetURL(); url != tc.expected {
+				t.Errorf("Expected URL to be '%s', got '%s'", tc.expected, url)
+			}
+		})
+	}
+}