Files
dingtalk-workspace-cli/internal/helpers/sheet_validate.go

300 lines
9.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"fmt"
"strings"
)
// ============================================================================
// range update 富格式 cell 字段级校验
// 与 MCP update_range.complexValues schema 对齐:
// cell.type 仅枚举 ["text", "richText"]
// richText.texts[].type 枚举 ["text", "link", "attachment", "image"]
// 服务端会再做一遍校验,CLI 提前拦截是为了给更友好的本地错误信息。
// ============================================================================
var complexValueStyleFields = map[string]string{
"bold": "bool",
"italic": "bool",
"underline": "bool",
"strike": "bool",
"color": "string",
"size": "number",
}
func validateComplexValueStyle(style map[string]any, path string) error {
for k, v := range style {
kind, ok := complexValueStyleFields[k]
if !ok {
return fmt.Errorf("%s 含未知字段 %q(合法字段: bold/italic/underline/strike/color/size)", path, k)
}
switch kind {
case "bool":
if _, ok := v.(bool); !ok {
return fmt.Errorf("%s.%s 必须为 boolean", path, k)
}
case "string":
if _, ok := v.(string); !ok {
return fmt.Errorf("%s.%s 必须为字符串(如 \"#FF0000\")", path, k)
}
case "number":
if _, ok := v.(float64); !ok {
return fmt.Errorf("%s.%s 必须为数字", path, k)
}
}
}
return nil
}
func validateRichTextItem(item map[string]any, path string) error {
typeRaw, ok := item["type"]
if !ok {
return fmt.Errorf("%s: 缺少 type 字段(合法值: text/link/attachment/image)", path)
}
typeVal, ok := typeRaw.(string)
if !ok {
return fmt.Errorf("%s.type 必须为字符串", path)
}
switch typeVal {
case "text":
if _, ok := item["text"].(string); !ok {
return fmt.Errorf("%s: type=text 必须包含 text 字符串字段", path)
}
case "link":
if _, ok := item["text"].(string); !ok {
return fmt.Errorf("%s: type=link 必须包含 text 字符串字段(显示文字)", path)
}
if s, ok := item["link"].(string); !ok || s == "" {
return fmt.Errorf("%s: type=link 必须包含非空 link 字符串字段(超链接 URL)", path)
}
case "attachment":
if _, ok := item["text"].(string); !ok {
return fmt.Errorf("%s: type=attachment 必须包含 text 字符串字段(显示文件名)", path)
}
if s, ok := item["resourceId"].(string); !ok || s == "" {
return fmt.Errorf("%s: type=attachment 必须包含非空 resourceId 字符串字段(通过 dws sheet media-upload 获取)", path)
}
if s, ok := item["mimeType"].(string); !ok || s == "" {
return fmt.Errorf("%s: type=attachment 必须包含非空 mimeType 字符串字段", path)
}
case "image":
if s, ok := item["resourceId"].(string); !ok || s == "" {
return fmt.Errorf("%s: type=image 必须包含非空 resourceId 字符串字段(通过 dws sheet media-upload 获取)", path)
}
if s, ok := item["resourceUrl"].(string); !ok || s == "" {
return fmt.Errorf("%s: type=image 必须包含非空 resourceUrl 字符串字段", path)
}
default:
return fmt.Errorf("%s.type 非法值 %q(合法值: text/link/attachment/image)", path, typeVal)
}
if styleRaw, exists := item["style"]; exists {
if typeVal != "text" && typeVal != "link" {
return fmt.Errorf("%s: style 字段仅 type=text / link 子项支持", path)
}
style, ok := styleRaw.(map[string]any)
if !ok {
return fmt.Errorf("%s.style 必须为 object", path)
}
if err := validateComplexValueStyle(style, path+".style"); err != nil {
return err
}
}
return nil
}
func validateComplexValueCell(cell map[string]any, path string) error {
// dataValidation 可以和 type 共存,也可以单独出现(如纯 checkbox)
if dvRaw, exists := cell["dataValidation"]; exists {
if err := validateDataValidation(dvRaw, path+".dataValidation"); err != nil {
return err
}
}
if hyperlinkRaw, exists := cell["hyperlink"]; exists {
if err := validateCellHyperlink(hyperlinkRaw, path+".hyperlink"); err != nil {
return err
}
}
typeRaw, hasType := cell["type"]
// 没有 type 时,必须有 metadata 字段(不写值,只更新元数据)
if !hasType {
_, hasDV := cell["dataValidation"]
_, hasCS := cell["cellStyles"]
_, hasHyperlink := cell["hyperlink"]
if hasDV || hasCS || hasHyperlink {
return nil
}
return fmt.Errorf("%s: 缺少 type 字段(合法值: text/richText)", path)
}
typeVal, ok := typeRaw.(string)
if !ok {
return fmt.Errorf("%s.type 必须为字符串", path)
}
switch typeVal {
case "text":
var cellText string
hasCellText := false
if t, exists := cell["text"]; exists {
textValue, ok := t.(string)
if !ok {
return fmt.Errorf("%s.text 必须为字符串(text=\"\" 表示清空 cell)", path)
}
cellText = textValue
hasCellText = true
}
if styleRaw, exists := cell["style"]; exists {
style, ok := styleRaw.(map[string]any)
if !ok {
return fmt.Errorf("%s.style 必须为 object", path)
}
if err := validateComplexValueStyle(style, path+".style"); err != nil {
return err
}
}
if hyperlinkRaw, exists := cell["hyperlink"]; exists && hyperlinkRaw != nil {
hyperlink, _ := hyperlinkRaw.(map[string]any)
if hyperlinkText, ok := hyperlink["text"].(string); ok && hasCellText && hyperlinkText != cellText {
return fmt.Errorf("%s.hyperlink.text 与 %s.text 不一致,请只传 cell.text 或保持两者相同", path, path)
}
}
case "richText":
if _, hasHyperlink := cell["hyperlink"]; hasHyperlink {
return fmt.Errorf("%s: cell-level hyperlink 不能与 type=richText 同时使用;整格链接用 hyperlink,片段链接用 richText.texts[].type=link", path)
}
textsRaw, ok := cell["texts"]
if !ok {
return fmt.Errorf("%s: type=richText 必须包含 texts 数组", path)
}
texts, ok := textsRaw.([]any)
if !ok {
return fmt.Errorf("%s.texts 必须为数组", path)
}
if len(texts) == 0 {
return fmt.Errorf("%s.texts 不能为空数组", path)
}
for i, item := range texts {
itemMap, ok := item.(map[string]any)
if !ok {
return fmt.Errorf("%s.texts[%d] 必须为 object", path, i)
}
if err := validateRichTextItem(itemMap, fmt.Sprintf("%s.texts[%d]", path, i)); err != nil {
return err
}
}
default:
return fmt.Errorf("%s.type 非法值 %q(合法值: text / richText;不再支持 number/boolean/null,数字布尔请用 {type:text,text:\"...\"} 字符串形式)", path, typeVal)
}
return nil
}
func validateCellHyperlink(raw any, path string) error {
if raw == nil {
return nil
}
hyperlink, ok := raw.(map[string]any)
if !ok {
return fmt.Errorf("%s 必须为 object 或 null", path)
}
typeRaw, ok := hyperlink["type"]
if !ok {
return fmt.Errorf("%s: 缺少 type 字段(合法值: path / sheet / range / none)", path)
}
typeVal, ok := typeRaw.(string)
if !ok {
return fmt.Errorf("%s.type 必须为字符串", path)
}
switch typeVal {
case "path", "sheet", "range":
// 写新 hyperlink:link 必填
linkRaw, ok := hyperlink["link"]
if !ok {
return fmt.Errorf("%s: 缺少 link 字段", path)
}
link, ok := linkRaw.(string)
if !ok || strings.TrimSpace(link) == "" {
return fmt.Errorf("%s.link 必须为非空字符串", path)
}
if textRaw, exists := hyperlink["text"]; exists {
if _, ok := textRaw.(string); !ok {
return fmt.Errorf("%s.text 必须为字符串", path)
}
}
case "none":
// type=none 表示显式清除单元格级超链接,不需其他字段
default:
return fmt.Errorf("%s.type 非法值 %q(合法值: path / sheet / range / none)", path, typeVal)
}
return nil
}
func validateDataValidation(dvRaw any, path string) error {
dv, ok := dvRaw.(map[string]any)
if !ok {
return fmt.Errorf("%s 必须为 object", path)
}
dvType, ok := dv["type"].(string)
if !ok || dvType == "" {
return fmt.Errorf("%s.type 必须为非空字符串(合法值: dropdown / checkbox / none)", path)
}
switch dvType {
case "dropdown":
optionsRaw, hasOptions := dv["options"]
sourceRangeRaw, hasSourceRange := dv["sourceRange"]
hasOptions = hasOptions && optionsRaw != nil
hasSourceRange = hasSourceRange && sourceRangeRaw != nil
if hasOptions == hasSourceRange {
return fmt.Errorf("%s: type=dropdown 的 options 与 sourceRange 必须且只能包含一个", path)
}
if hasOptions {
options, ok := optionsRaw.([]any)
if !ok || len(options) == 0 {
return fmt.Errorf("%s.options 必须为非空数组", path)
}
for i, opt := range options {
optMap, ok := opt.(map[string]any)
if !ok {
return fmt.Errorf("%s.options[%d] 必须为 object(如 {\"value\":\"选项\"})", path, i)
}
val, ok := optMap["value"].(string)
if !ok || val == "" {
return fmt.Errorf("%s.options[%d].value 必须为非空字符串", path, i)
}
}
} else {
sourceRange, ok := sourceRangeRaw.(map[string]any)
if !ok {
return fmt.Errorf("%s.sourceRange 必须为 object", path)
}
if _, exists := sourceRange["colors"]; exists {
return fmt.Errorf("%s.sourceRange.colors 暂不支持", path)
}
sheetID, ok := sourceRange["sheetId"].(string)
if !ok || strings.TrimSpace(sheetID) == "" {
return fmt.Errorf("%s.sourceRange.sheetId 必须为非空字符串", path)
}
a1Notation, ok := sourceRange["a1Notation"].(string)
if !ok || strings.TrimSpace(a1Notation) == "" {
return fmt.Errorf("%s.sourceRange.a1Notation 必须为非空字符串", path)
}
}
if multi, exists := dv["enableMultiSelect"]; exists {
if _, ok := multi.(bool); !ok {
return fmt.Errorf("%s.enableMultiSelect 必须为 boolean", path)
}
}
case "checkbox":
// checked 可选,布尔
if c, exists := dv["checked"]; exists {
if _, ok := c.(bool); !ok {
return fmt.Errorf("%s.checked 必须为 boolean", path)
}
}
case "none":
// type=none 表示显式清除单元格 DV,不需其他字段
default:
return fmt.Errorf("%s.type 非法值 %q(合法值: dropdown / checkbox / none)", path, dvType)
}
return nil
}