feat: complete contract-driven business development and MySQL integration
This commit is contained in:
174
contract/documentation.go
Normal file
174
contract/documentation.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user