175 lines
4.4 KiB
Go
175 lines
4.4 KiB
Go
package contract
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// Documentation is part of the contract compiler, so CLI and runtime agree.
|
|
func applyDocumentation(f *field, tags map[string]string) error {
|
|
s := f.value.schema
|
|
if f.maxBytes > 0 {
|
|
v := f.maxBytes
|
|
s.MaxBytes = &v
|
|
}
|
|
if f.source == "file" && f.value.typ.Kind() == reflect.Slice {
|
|
v := f.maxFiles
|
|
if v == 0 {
|
|
v = 16
|
|
}
|
|
s.MaxItems = &v
|
|
}
|
|
s.Description = tags["description"]
|
|
if raw, ok := tags["enumDescriptions"]; ok {
|
|
if err := json.Unmarshal([]byte(raw), &s.EnumDescriptions); err != nil {
|
|
return errors.New("enumDescriptions must be a JSON string array")
|
|
}
|
|
count := len(s.Enum)
|
|
if s.Nullable && count > 0 && s.Enum[count-1] == nil {
|
|
count--
|
|
}
|
|
if count == 0 || len(s.EnumDescriptions) != count {
|
|
return errors.New("enumDescriptions must describe every declared enum value")
|
|
}
|
|
for _, v := range s.EnumDescriptions {
|
|
if strings.TrimSpace(v) == "" {
|
|
return errors.New("enum description cannot be empty")
|
|
}
|
|
}
|
|
}
|
|
if raw, ok := tags["example"]; ok {
|
|
var value any
|
|
if base(f.value).typ.Kind() == reflect.String && base(f.value).scalar == nil {
|
|
value = raw
|
|
} else {
|
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
|
decoder.UseNumber()
|
|
var err error
|
|
value, err = decodeValue(decoder, 0)
|
|
if err != nil {
|
|
return errors.New("invalid example JSON")
|
|
}
|
|
if _, err = decoder.Token(); err != io.EOF {
|
|
return errors.New("example requires one value")
|
|
}
|
|
}
|
|
// A schema-only custom scalar never executes project decoders.
|
|
if !documentationHasScalar(f.value) {
|
|
v := reflect.New(f.value.typ).Elem()
|
|
if err := setValue(f.value, v, value); err != nil {
|
|
return fmt.Errorf("example: %w", err)
|
|
}
|
|
if err := checkField(*f, v); err != nil {
|
|
return fmt.Errorf("example: %w", err)
|
|
}
|
|
} else if err := checkDocumentationScalarTree(f.value, value); err != nil {
|
|
return err
|
|
}
|
|
s.Example = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func documentationHasScalar(n *node) bool {
|
|
if n.scalar != nil {
|
|
return true
|
|
}
|
|
if n.elem != nil && documentationHasScalar(n.elem) {
|
|
return true
|
|
}
|
|
for _, f := range n.fields {
|
|
if documentationHasScalar(f.value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Metadata never invokes application decoders, including nested scalar types.
|
|
func checkDocumentationScalarTree(n *node, value any) error {
|
|
if value == nil {
|
|
if n.typ.Kind() == reflect.Pointer || n.typ.Kind() == reflect.Interface {
|
|
return nil
|
|
}
|
|
return errors.New("example null requires a nullable field")
|
|
}
|
|
if n.typ.Kind() == reflect.Pointer {
|
|
return checkDocumentationScalarTree(n.elem, value)
|
|
}
|
|
if n.scalar != nil {
|
|
for _, kind := range strings.Split(n.scalar.JSONInput, "|") {
|
|
if scalarAccepts(strings.TrimSpace(kind), value) {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("example does not match scalar input")
|
|
}
|
|
if !documentationHasScalar(n) {
|
|
v := reflect.New(n.typ).Elem()
|
|
return setValue(n, v, value)
|
|
}
|
|
switch n.typ.Kind() {
|
|
case reflect.Slice:
|
|
values, ok := value.([]any)
|
|
if !ok {
|
|
return errors.New("example requires an array")
|
|
}
|
|
if n.schema.MinItems != nil && len(values) < *n.schema.MinItems || n.schema.MaxItems != nil && len(values) > *n.schema.MaxItems {
|
|
return errors.New("example violates item limits")
|
|
}
|
|
for _, v := range values {
|
|
if err := checkDocumentationScalarTree(n.elem, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case reflect.Map:
|
|
values, ok := value.(map[string]any)
|
|
if !ok {
|
|
return errors.New("example requires an object")
|
|
}
|
|
for _, v := range values {
|
|
if err := checkDocumentationScalarTree(n.elem, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case reflect.Struct:
|
|
values, ok := value.(map[string]any)
|
|
if !ok {
|
|
return errors.New("example requires an object")
|
|
}
|
|
declared := map[string]bool{}
|
|
for _, f := range n.fields {
|
|
declared[f.name] = true
|
|
v, ok := values[f.name]
|
|
if !ok {
|
|
if !f.optional && !f.hasDefault {
|
|
return errors.New("example missing field")
|
|
}
|
|
continue
|
|
}
|
|
if err := checkDocumentationScalarTree(f.value, v); err != nil {
|
|
return err
|
|
}
|
|
if !documentationHasScalar(f.value) {
|
|
typed := reflect.New(f.value.typ).Elem()
|
|
if err := setValue(f.value, typed, v); err != nil {
|
|
return err
|
|
}
|
|
if err := checkField(f, typed); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
for name := range values {
|
|
if !declared[name] {
|
|
return errors.New("unknown example field")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|