(fix): finalize v2 contracts (tests): cover v2 migration (doc): prepare release guidance
This commit is contained in:
+62
-29
@@ -13,11 +13,20 @@ const (
|
||||
maximumRichJSONNodes = 10_000
|
||||
)
|
||||
|
||||
var errInvalidUnknownRichJSON = errors.New("invalid JSON in unknown rich object")
|
||||
|
||||
var knownRichTextTypes = map[string]bool{
|
||||
"url": true, "email_address": true, "phone_number": true,
|
||||
"bank_card_number": true, "mention": true, "hashtag": true,
|
||||
"cashtag": true, "bot_command": true, "anchor_link": true,
|
||||
"reference": true, "reference_link": true, "date_time": true,
|
||||
"text_mention": true, "custom_emoji": true,
|
||||
"mathematical_expression": true, "anchor": true, "button": true,
|
||||
}
|
||||
|
||||
// UnmarshalRichText parses a RichText tree from JSON: a string, an array, or
|
||||
// a typed object. Unknown object types that carry a text field are preserved
|
||||
// as RichTextWrap so their nested text remains usable; unmodeled fields are
|
||||
// discarded. The fallback representation is subject to change in v2 so unknown
|
||||
// fields can be preserved losslessly.
|
||||
// a typed object. Unknown object types are preserved as RichTextUnknown without
|
||||
// discarding fields.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichText(data []byte) (RichText, error) {
|
||||
@@ -57,6 +66,12 @@ func unmarshalRichText(data []byte) (RichText, error) {
|
||||
if err := json.Unmarshal(data, &head); err != nil {
|
||||
return nil, fmt.Errorf("richtext: not a string, array or object: %w", err)
|
||||
}
|
||||
if head.Type == "" {
|
||||
return nil, errors.New("richtext: object type is required")
|
||||
}
|
||||
if !richTextWrapTags[head.Type] && !knownRichTextTypes[head.Type] {
|
||||
return RichTextUnknown{Raw: cloneRawJSON(data)}, nil
|
||||
}
|
||||
|
||||
// Recursively parse the nested text, if any.
|
||||
var inner RichText
|
||||
@@ -204,21 +219,23 @@ func unmarshalRichText(data []byte) (RichText, error) {
|
||||
return nil, err
|
||||
}
|
||||
return RichTextAnchor{v.Name}, nil
|
||||
case "button":
|
||||
var raw struct {
|
||||
Button RichMessageButton `json:"button"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return RichTextButton{Button: raw.Button}, nil
|
||||
|
||||
default:
|
||||
// forward-compat: keep an unknown tag with a text field as
|
||||
// RichTextWrap; without text it is an error (the shape cannot be guessed).
|
||||
if inner != nil {
|
||||
return RichTextWrap{Tag: head.Type, Text: inner}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("richtext: unknown type %q", head.Type)
|
||||
return nil, fmt.Errorf("richtext: unsupported type %q", head.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalRichBlock parses a single RichBlock from JSON, dispatching on the
|
||||
// type tag. Unknown types that carry a text field are decoded as RichBlockWrap
|
||||
// so their nested text remains usable; unmodeled fields are discarded. The
|
||||
// fallback representation is subject to change in v2 for lossless round trips.
|
||||
// type tag. Unknown object types are preserved as RichBlockUnknown without
|
||||
// discarding fields.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
@@ -236,6 +253,9 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
if err := json.Unmarshal(data, &head); err != nil {
|
||||
return nil, fmt.Errorf("richblock: %w", err)
|
||||
}
|
||||
if head.Type == "" {
|
||||
return nil, errors.New("richblock: object type is required")
|
||||
}
|
||||
|
||||
if richBlockWrapTags[head.Type] {
|
||||
text, err := parseOptRichText(head.Text)
|
||||
@@ -290,7 +310,7 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
}
|
||||
return RichBlockQuotation{blocks, credit}, nil
|
||||
|
||||
case "pullquote":
|
||||
case "expandable_blockquote", "pullquote":
|
||||
var raw struct {
|
||||
Credit json.RawMessage `json:"credit"`
|
||||
}
|
||||
@@ -305,6 +325,9 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("richblock %q: credit: %w", head.Type, err)
|
||||
}
|
||||
if head.Type == "expandable_blockquote" {
|
||||
return RichBlockExpandableBlockQuotation{text, credit}, nil
|
||||
}
|
||||
return RichBlockPullQuotation{text, credit}, nil
|
||||
|
||||
case "list":
|
||||
@@ -368,6 +391,7 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
Cells [][]RichBlockTableCell `json:"cells"`
|
||||
IsBordered bool `json:"is_bordered"`
|
||||
IsStriped bool `json:"is_striped"`
|
||||
IsCompact bool `json:"is_compact"`
|
||||
Caption json.RawMessage `json:"caption"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
@@ -377,7 +401,7 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("richblock %q: caption: %w", head.Type, err)
|
||||
}
|
||||
return RichBlockTable{raw.Cells, raw.IsBordered, raw.IsStriped, caption}, nil
|
||||
return RichBlockTable{raw.Cells, raw.IsBordered, raw.IsStriped, raw.IsCompact, caption}, nil
|
||||
|
||||
case "map":
|
||||
var v struct {
|
||||
@@ -392,6 +416,18 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
}
|
||||
return RichBlockMap{v.Location, v.Zoom, v.Width, v.Height, v.Caption}, nil
|
||||
|
||||
case "buttons":
|
||||
var block RichBlockButtons
|
||||
if err := json.Unmarshal(data, &block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return block, nil
|
||||
case "document":
|
||||
var block RichBlockDocument
|
||||
if err := json.Unmarshal(data, &block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return block, nil
|
||||
case "photo":
|
||||
var v struct {
|
||||
Photo []PhotoSize `json:"photo"`
|
||||
@@ -467,26 +503,15 @@ func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
return RichBlockAnchor{v.Name}, nil
|
||||
|
||||
default:
|
||||
// forward-compat: unknown type with text -> RichBlockWrap, without text -> error.
|
||||
if text, err := parseOptRichText(head.Text); err == nil && text != nil {
|
||||
return RichBlockWrap{Tag: head.Type, Text: text}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("richblock: unknown type %q", head.Type)
|
||||
return RichBlockUnknown{Raw: cloneRawJSON(data)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalRichMessage parses a root RichMessage from JSON.
|
||||
//
|
||||
// For v1 compatibility, missing and null blocks are accepted as an empty
|
||||
// message. This permissive behavior is subject to change in v2; use
|
||||
// UnmarshalRichMessageStrict when validating untrusted input.
|
||||
// UnmarshalRichMessage parses a RichMessage and requires a non-null blocks array.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return RichMessage{}, err
|
||||
}
|
||||
return unmarshalRichMessage(data)
|
||||
return unmarshalRichMessageStrict(data)
|
||||
}
|
||||
|
||||
func unmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
@@ -508,6 +533,10 @@ func unmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichMessageStrict(data []byte) (RichMessage, error) {
|
||||
return unmarshalRichMessageStrict(data)
|
||||
}
|
||||
|
||||
func unmarshalRichMessageStrict(data []byte) (RichMessage, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return RichMessage{}, err
|
||||
}
|
||||
@@ -550,6 +579,10 @@ func parseOptRichText(raw json.RawMessage) (RichText, error) {
|
||||
return unmarshalRichText(raw)
|
||||
}
|
||||
|
||||
func cloneRawJSON(raw []byte) json.RawMessage {
|
||||
return append(json.RawMessage(nil), raw...)
|
||||
}
|
||||
|
||||
func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil, nil
|
||||
|
||||
Reference in New Issue
Block a user