Files
dingtalk-workspace-cli/internal/helpers/doc/doc_jsonml_validate_v2.go

242 lines
5.9 KiB
Go

//go:build !goexperiment.jsonv2
package doc
import (
"encoding/json"
"fmt"
"strings"
)
// ValidateJsonMLBodyV2 validates a JSONML body using schema-v2.
// Only type mismatches are errors; everything else is a warning.
func ValidateJsonMLBodyV2(body []any) *JsonMLValidationResult {
r := &JsonMLValidationResult{}
if len(body) == 0 {
return r
}
// Root-wrapped, single block node, or future/unknown single node. If the
// first element is a string, the JSONML shape is a node; unknown tags should
// be warnings, not a cascade of "node must be array" errors.
if _, ok := body[0].(string); ok {
validateNodeV2(body, "$", nil, r)
return r
}
// Array of blocks
for i, node := range body {
nodePath := fmt.Sprintf("$[%d]", i)
if arr, ok := node.([]any); ok && len(arr) > 0 {
if t, ok := arr[0].(string); ok {
nodePath = fmt.Sprintf("$[%d:%s]", i, t)
}
}
validateNodeV2(node, nodePath, nil, r)
}
return r
}
// ValidateJsonMLNodeV2 validates a single JSONML node using schema-v2.
func ValidateJsonMLNodeV2(node any) *JsonMLValidationResult {
r := &JsonMLValidationResult{}
validateNodeV2(node, "$", nil, r)
return r
}
// ValidateJsonMLSource is a fallback for non-jsonv2 builds.
// It unmarshals the source and delegates to ValidateJsonMLBodyV2.
// Positions in diagnostics will not be source-accurate in this build.
func ValidateJsonMLSource(src []byte) *JsonMLValidationResult {
if len(src) == 0 {
return &JsonMLValidationResult{}
}
var body any
if err := json.Unmarshal(src, &body); err != nil {
r := &JsonMLValidationResult{}
r.Errors = append(r.Errors, "JSON parse error: "+err.Error())
return r
}
switch v := body.(type) {
case []any:
return ValidateJsonMLBodyV2(v)
case map[string]any:
if jml, ok := v["jsonml"]; ok {
if arr, ok := jml.([]any); ok {
return ValidateJsonMLBodyV2(arr)
}
}
}
r := &JsonMLValidationResult{}
r.Errors = append(r.Errors, "input is not a valid JSONML array or {\"jsonml\": [...]} wrapper")
return r
}
func validateNodeV2(node any, path string, parentSchema *TagSchema, r *JsonMLValidationResult) {
arr, ok := node.([]any)
if !ok {
r.addError(path, fmt.Sprintf("node must be array, got %T", node), "")
return
}
if len(arr) < 1 {
r.addError(path, "node array must not be empty", "")
return
}
tag, ok := arr[0].(string)
if !ok {
r.addError(path, fmt.Sprintf("tag must be string, got %T", arr[0]), "")
return
}
// Check if child is allowed by parent
if parentSchema != nil && !parentSchema.IsAllowedChild(tag) {
r.addWarn(path,
fmt.Sprintf("tag %q not in parent's allowed_children", tag), "")
}
tagSchema := schemaV2.TagSchemaFor(tag)
if tagSchema == nil {
r.addWarn(path, fmt.Sprintf("unknown tag %q", tag), "")
return
}
// Extract attrs
childStart := 1
var attrs map[string]any
if len(arr) > 1 {
if m, ok := arr[1].(map[string]any); ok {
attrs = m
childStart = 2
}
}
// Validate attrs
for key, val := range attrs {
spec, known := tagSchema.Attrs[key]
if !known {
r.addWarn(path+".attrs."+key,
fmt.Sprintf("unknown attr %q", key), "")
continue
}
checkTypeV2(val, &spec, path+".attrs."+key, r)
}
// Validate children
for i := childStart; i < len(arr); i++ {
child := arr[i]
childPath := fmt.Sprintf("%s[%d]", path, i)
switch c := child.(type) {
case string:
if !tagSchema.IsAllowedChild("#text") {
r.addWarn(childPath,
fmt.Sprintf("bare text not allowed in %q", tag), "")
}
_ = c
case []any:
if len(c) > 0 {
if childTag, ok := c[0].(string); ok {
childPath = fmt.Sprintf("%s[%d:%s]", path, i, childTag)
}
}
validateNodeV2(child, childPath, tagSchema, r)
default:
// null, number etc — skip
}
}
}
// checkTypeV2 validates a value against a TypeSpec.
// Type and enum mismatches are blocking schema errors.
func checkTypeV2(value any, spec *TypeSpec, path string, r *JsonMLValidationResult) {
switch spec.Type {
case "any":
return
case "string":
if _, ok := value.(string); !ok {
r.addError(path,
fmt.Sprintf("expected string, got %T", value), "")
}
case "number":
num, ok := toFloat64(value)
if !ok {
r.addError(path,
fmt.Sprintf("expected number, got %T", value), "")
return
}
if spec.Min != nil && num < *spec.Min {
r.addError(path,
fmt.Sprintf("value %v < min %v", num, *spec.Min), "")
}
if spec.Max != nil && num > *spec.Max {
r.addError(path,
fmt.Sprintf("value %v > max %v", num, *spec.Max), "")
}
case "boolean":
if _, ok := value.(bool); !ok {
r.addError(path,
fmt.Sprintf("expected boolean, got %T", value), "")
}
case "array":
if _, ok := value.([]any); !ok {
r.addError(path,
fmt.Sprintf("expected array, got %T", value), "")
}
case "object":
obj, ok := value.(map[string]any)
if !ok {
r.addError(path,
fmt.Sprintf("expected object, got %T", value), "")
return
}
// Deep validation if fields defined
if spec.Fields != nil {
for key, val := range obj {
fieldSpec, known := spec.Fields[key]
if !known {
r.addWarn(path+"."+key,
fmt.Sprintf("unknown field %q", key), "")
continue
}
checkTypeV2(val, &fieldSpec, path+"."+key, r)
}
}
case "enum":
str, ok := value.(string)
if !ok {
r.addError(path,
fmt.Sprintf("enum expects string, got %T", value), "")
return
}
found := false
for _, v := range spec.Values {
if v == str {
found = true
break
}
}
if !found {
r.addError(path,
fmt.Sprintf("value %q not in enum [%s]", str, strings.Join(spec.Values, ", ")), "")
}
case "union":
if matchesUnion(value, spec.Types) {
return
}
if len(spec.WarnTypes) > 0 && matchesUnion(value, spec.WarnTypes) {
r.addWarn(path,
fmt.Sprintf("value (%T) matches warn_types [%s], expected [%s]", value, strings.Join(spec.WarnTypes, ", "), strings.Join(spec.Types, ", ")), "")
return
}
r.addError(path,
fmt.Sprintf("value (%T) doesn't match any of [%s]", value, strings.Join(spec.Types, ", ")), "")
}
}