Repository: slack-go/slack
Pull request #1260 (2024-02-11 05:24:05)

Add rich_text_quote and rich_text_preformatted
This is a minor update adding rich_text_quote and rich_text_preformatted unmarshalling logic.

It does not change any existing functionality, just adds unmarshalling for two aforementioned rich text blocks, and they pretty much derive the RichTextSection

##### PR preparation
- [x] Run `make pr-prep` from the root of the repository to run formatting, linting and tests.




Patch:
diff --git a/block_rich_text.go b/block_rich_text.go
index aea1597..b6a4b4c 100644
--- a/block_rich_text.go
+++ b/block_rich_text.go
@@ -42,7 +42,10 @@ func (e *RichTextBlock) UnmarshalJSON(b []byte) error {
 			elem = &RichTextSection{}
 		case RTEList:
 			elem = &RichTextList{}
-
+		case RTEQuote:
+			elem = &RichTextQuote{}
+		case RTEPreformatted:
+			elem = &RichTextPreformatted{}
 		default:
 			elems = append(elems, &RichTextUnknown{
 				Type: s.Type,
@@ -150,6 +153,10 @@ func (e *RichTextList) UnmarshalJSON(b []byte) error {
 			elem = &RichTextSection{}
 		case RTEList:
 			elem = &RichTextList{}
+		case RTEQuote:
+			elem = &RichTextQuote{}
+		case RTEPreformatted:
+			elem = &RichTextPreformatted{}
 		default:
 			elems = append(elems, &RichTextUnknown{
 				Type: s.Type,
@@ -460,3 +467,62 @@ type RichTextSectionUnknownElement struct {
 func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType {
 	return r.Type
 }
+
+// RichTextQuote represents rich_text_quote element type.
+type RichTextQuote RichTextSection
+
+// RichTextElementType returns the type of the Element
+func (s *RichTextQuote) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (s *RichTextQuote) UnmarshalJSON(b []byte) error {
+	// reusing the RichTextSection struct, as it's the same as RichTextQuote.
+	var rts RichTextSection
+	if err := json.Unmarshal(b, &rts); err != nil {
+		return err
+	}
+	*s = RichTextQuote(rts)
+	s.Type = RTEQuote
+	return nil
+}
+
+// RichTextPreformatted represents rich_text_quote element type.
+type RichTextPreformatted struct {
+	RichTextSection
+	Border int `json:"border"`
+}
+
+// RichTextElementType returns the type of the Element
+func (s *RichTextPreformatted) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error {
+	var rts RichTextSection
+	if err := json.Unmarshal(b, &rts); err != nil {
+		return err
+	}
+	// we define standalone fields because we need to unmarshal the border
+	// field.  We can not directly unmarshal the data into
+	// RichTextPreformatted because it will cause an infinite loop.  We also
+	// can not define a struct with embedded RichTextSection and Border fields
+	// because the json package will not unmarshal the data into the
+	// standalone fields, once it sees UnmarshalJSON method on the embedded
+	// struct.  The drawback is that we have to process the data twice, and
+	// have to define a standalone struct with the same set of fields as the
+	// original struct, which may become a maintenance burden (i.e. update the
+	// fields in two places, should it ever change).
+	var standalone struct {
+		Border int `json:"border"`
+	}
+	if err := json.Unmarshal(b, &standalone); err != nil {
+		return err
+	}
+	*s = RichTextPreformatted{
+		RichTextSection: rts,
+		Border:          standalone.Border,
+	}
+	s.Type = RTEPreformatted
+	return nil
+}


Test patch:
diff --git a/block_rich_text_test.go b/block_rich_text_test.go
index 2788c97..a9f04b7 100644
--- a/block_rich_text_test.go
+++ b/block_rich_text_test.go
@@ -27,6 +27,60 @@ const (
     }
   ]
 }`
+
+	richTextQuotePayload = `{
+		"type": "rich_text",
+		"block_id": "G7G",
+		"elements": [
+			{
+				"type": "rich_text_section",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Holy moly\n\n"
+					}
+				]
+			},
+			{
+				"type": "rich_text_preformatted",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Preformatted\n\n"
+					}
+				],
+				"border": 2
+			},
+			{
+				"type": "rich_text_quote",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Quote\n\n"
+					}
+				]
+			},
+			{
+				"type": "rich_text_quote",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Another quote"
+					}
+				]
+			},
+			{
+				"type": "rich_text_preformatted",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Another preformatted\n\n"
+					}
+				],
+				"border": 42
+			}
+		]
+	}`
 )
 
 func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
@@ -55,6 +109,38 @@ func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 			},
 			nil,
 		},
+		{
+			[]byte(dummyPayload),
+			RichTextBlock{
+				Type:    MBTRichText,
+				BlockID: "FaYCD",
+				Elements: []RichTextElement{
+					&RichTextSection{
+						Type: RTESection,
+						Elements: []RichTextSectionElement{
+							&RichTextSectionChannelElement{Type: RTSEChannel, ChannelID: "C012345678"},
+							&RichTextSectionTextElement{Type: RTSEText, Text: "dummy_text"},
+						},
+					},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(richTextQuotePayload),
+			RichTextBlock{
+				Type:    MBTRichText,
+				BlockID: "G7G",
+				Elements: []RichTextElement{
+					&RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Holy moly\n\n"}}},
+					&RichTextPreformatted{RichTextSection: RichTextSection{Type: RTEPreformatted, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Preformatted\n\n"}}}, Border: 2},
+					&RichTextQuote{Type: RTEQuote, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Quote\n\n"}}},
+					&RichTextQuote{Type: RTEQuote, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Another quote"}}},
+					&RichTextPreformatted{RichTextSection: RichTextSection{Type: RTEPreformatted, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Another preformatted\n\n"}}}, Border: 42},
+				},
+			},
+			nil,
+		},
 	}
 	for _, tc := range cases {
 		var actual RichTextBlock
@@ -63,10 +149,10 @@ func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 			if tc.err == nil {
 				t.Errorf("unexpected error: %s", err)
 			}
-			t.Errorf("expected error is %s, but got %s", tc.err, err)
+			t.Errorf("expected error is %v, but got %v", tc.err, err)
 		}
 		if tc.err != nil {
-			t.Errorf("expected to raise an error %s", tc.err)
+			t.Errorf("expected to raise an error %v", tc.err)
 		}
 		if diff := deep.Equal(actual, tc.expected); diff != nil {
 			t.Errorf("actual value does not match expected one\n%s", diff)
@@ -199,3 +285,79 @@ func TestRichTextList_UnmarshalJSON(t *testing.T) {
 		}
 	}
 }
+
+func TestRichTextQuote_Marshal(t *testing.T) {
+	t.Run("rich_text_section", func(t *testing.T) {
+		const rawRSE = "{\"type\":\"rich_text_section\",\"elements\":[{\"type\":\"text\",\"text\":\"Some Text\"}]}"
+
+		var got RichTextSection
+		if err := json.Unmarshal([]byte(rawRSE), &got); err != nil {
+			t.Fatal(err)
+		}
+		want := RichTextSection{
+			Type: RTESection,
+			Elements: []RichTextSectionElement{
+				&RichTextSectionTextElement{Type: RTSEText, Text: "Some Text"},
+			},
+		}
+
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRSE); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+	t.Run("rich_text_quote", func(t *testing.T) {
+		const rawRTS = "{\"type\":\"rich_text_quote\",\"elements\":[{\"type\":\"text\",\"text\":\"Some text\"}]}"
+
+		var got RichTextQuote
+		if err := json.Unmarshal([]byte(rawRTS), &got); err != nil {
+			t.Fatal(err)
+		}
+		want := RichTextQuote{
+			Type: RTEQuote,
+			Elements: []RichTextSectionElement{
+				&RichTextSectionTextElement{Type: RTSEText, Text: "Some text"},
+			},
+		}
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRTS); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+	t.Run("rich_text_preformatted", func(t *testing.T) {
+		const rawRTP = "{\"type\":\"rich_text_preformatted\",\"elements\":[{\"type\":\"text\",\"text\":\"Some other text\"}],\"border\":2}"
+		want := RichTextPreformatted{
+			RichTextSection: RichTextSection{
+				Type:     RTEPreformatted,
+				Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Some other text"}},
+			},
+			Border: 2,
+		}
+		var got RichTextPreformatted
+		if err := json.Unmarshal([]byte(rawRTP), &got); err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRTP); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+}