~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
197 lines · 4.3 KB Raw History
package apigen

import (
	"cmp"
	"fmt"
	"math"
	"regexp"
	"slices"
	"sort"
	"strings"
	"unicode/utf8"
)

type Issue struct {
	Path    string
	Message string
}

func (i Issue) String() string {
	path := i.Path
	path = cmp.Or(path, "(root)")
	return path + ": " + i.Message
}

const (
	msgMissing = "missing required field"
	msgUnknown = "unknown field"
)

func (m *Model) Validate(typeName string, value any) []Issue {
	s, ok := m.Named[typeName]
	if !ok {
		return []Issue{{Message: fmt.Sprintf("no type named %s", typeName)}}
	}
	var out []Issue
	check(s, value, "", &out)
	return out
}

func joinPath(path, key string) string {
	if path == "" {
		return key
	}
	return path + "." + key
}

func describe(v any) string {
	switch v.(type) {
	case nil:
		return "null"
	case map[string]any:
		return "an object"
	case []any:
		return "an array"
	case string:
		return "a string"
	case bool:
		return "a boolean"
	case float64:
		return "a number"
	default:
		return fmt.Sprintf("%T", v)
	}
}

func expected(want string, got any) string {
	return fmt.Sprintf("expected %s, got %s", want, describe(got))
}

func check(s *Schema, value any, path string, out *[]Issue) {
	add := func(p, msg string) { *out = append(*out, Issue{Path: p, Message: msg}) }
	switch s.Type {
	case TypeObject:
		obj, ok := value.(map[string]any)
		if !ok {
			add(path, expected("an object", value))
			return
		}
		if s.PreserveUnknown {
			return
		}
		if s.AdditionalProperties != nil {
			keys := make([]string, 0, len(obj))
			for k := range obj {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			for _, k := range keys {
				check(s.AdditionalProperties, obj[k], joinPath(path, k), out)
			}
			return
		}
		if !s.Closed {
			return
		}
		declared := map[string]bool{}
		for _, p := range s.Properties {
			declared[p.Name] = true
			v, present := obj[p.Name]
			if !present {
				if p.Required {
					add(joinPath(path, p.Name), msgMissing)
				}
				continue
			}
			check(p.Schema, v, joinPath(path, p.Name), out)
		}
		var unknown []string
		for k := range obj {
			if !declared[k] {
				unknown = append(unknown, k)
			}
		}
		sort.Strings(unknown)
		for _, k := range unknown {
			add(joinPath(path, k), msgUnknown)
		}
	case TypeArray:
		arr, ok := value.([]any)
		if !ok {
			add(path, expected("an array", value))
			return
		}
		for i, v := range arr {
			check(s.Items, v, fmt.Sprintf("%s[%d]", path, i), out)
		}
		checkList(s, arr, path, add)
	case TypeString:
		str, ok := value.(string)
		if !ok {
			add(path, expected("a string", value))
			return
		}
		if s.MaxLength != nil && int64(utf8.RuneCountInString(str)) > *s.MaxLength {
			add(path, fmt.Sprintf("%d characters is over the maxLength %d", utf8.RuneCountInString(str), *s.MaxLength))
		}
		if s.Pattern != "" {
			if re, err := regexp.Compile(s.Pattern); err == nil && !re.MatchString(str) {
				add(path, fmt.Sprintf("%q does not match the pattern %s", str, s.Pattern))
			}
		}
		if len(s.Enum) == 0 {
			return
		}
		if slices.Contains(s.Enum, str) {
			return
		}
		add(path, fmt.Sprintf("%q is not one of %s", str, strings.Join(s.Enum, ", ")))
	case TypeInteger:
		n, ok := value.(float64)
		if !ok || n != math.Trunc(n) || math.IsInf(n, 0) {
			add(path, expected("an integer", value))
			return
		}
		if s.Minimum != nil && n < float64(*s.Minimum) {
			add(path, fmt.Sprintf("%v is below the minimum %d", n, *s.Minimum))
		}
		if s.Maximum != nil && n > float64(*s.Maximum) {
			add(path, fmt.Sprintf("%v is above the maximum %d", n, *s.Maximum))
		}
	case TypeBoolean:
		if _, ok := value.(bool); !ok {
			add(path, expected("a boolean", value))
		}
	default:
		add(path, fmt.Sprintf("schema type %q has no validator", s.Type))
	}
}

func checkList(s *Schema, arr []any, path string, add func(p, msg string)) {
	switch s.ListType {
	case ListTypeSet:
		seen := map[any]bool{}
		for i, v := range arr {
			if seen[v] {
				add(fmt.Sprintf("%s[%d]", path, i), "duplicate entry in a set")
			}
			seen[v] = true
		}
	case ListTypeMap:
		seen := map[string]bool{}
		for i, v := range arr {
			obj, ok := v.(map[string]any)
			if !ok {
				continue
			}
			var key strings.Builder
			for _, k := range s.ListMapKeys {
				key.WriteString(fmt.Sprintf("%v\x00", obj[k]))
			}
			if seen[key.String()] {
				add(fmt.Sprintf("%s[%d]", path, i), "duplicate map key "+strings.Join(s.ListMapKeys, ","))
			}
			seen[key.String()] = true
		}
	}
}