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

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
)

var (
	endpointGoSuffixes   = []string{"URL", "URLs", "Url", "Urls", "URI", "URIs", "Uri", "Uris", "Path", "Paths", "File", "Files"}
	endpointJSONSuffixes = []string{"url", "urls", "uri", "uris", "path", "paths", "file", "files"}
	lowerCasingSuffixes  = []string{"Url", "Urls", "Uri", "Uris"}
)

func jsonEndpointName(name string) bool {
	lower := strings.ToLower(name)
	for _, s := range endpointJSONSuffixes {
		if lower == s {
			return true
		}
		if !strings.HasSuffix(lower, s) {
			continue
		}
		at := len(name) - len(s)
		if c := name[at]; c >= 'A' && c <= 'Z' {
			return true
		}
	}
	return false
}

func hasSuffix(name string, suffixes []string) bool {
	for _, s := range suffixes {
		if strings.HasSuffix(name, s) {
			return true
		}
	}
	return false
}

func endpointName(goName, jsonName string) bool {
	return hasSuffix(goName, endpointGoSuffixes) || jsonEndpointName(jsonName)
}

func (b *builder) checkCasing(t reflect.Type) error {
	var errs []error
	for f := range t.Fields() {
		if f.Anonymous {
			continue
		}
		if hasSuffix(f.Name, lowerCasingSuffixes) {
			errs = append(errs, fmt.Errorf("%s.%s: write the initialism in capitals (URL, URI)", t.Name(), f.Name))
		}
	}
	return errors.Join(errs...)
}

func (b *builder) checkSpecEndpoints(root reflect.Type) error {
	spec, ok := root.FieldByName("Spec")
	if !ok {
		return nil
	}
	var errs []error
	b.walkEndpoints(spec.Type, "spec", map[reflect.Type]bool{}, &errs)
	return errors.Join(errs...)
}

func (b *builder) reportOnce(key string) bool {
	if b.reported == nil {
		b.reported = map[string]bool{}
	}
	if b.reported[key] {
		return false
	}
	b.reported[key] = true
	return true
}

func (b *builder) walkEndpoints(t reflect.Type, path string, seen map[reflect.Type]bool, errs *[]error) {
	for t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice || t.Kind() == reflect.Map {
		t = t.Elem()
	}
	if t.Kind() != reflect.Struct || t.PkgPath() != b.pkgPath || seen[t] {
		return
	}
	seen[t] = true
	defer delete(seen, t)
	for f := range t.Fields() {
		name, _, _ := strings.Cut(f.Tag.Get("json"), ",")
		if name == "-" {
			continue
		}
		if f.Anonymous && name == "" {
			b.walkEndpoints(f.Type, path, seen, errs)
			continue
		}
		at := path + "." + name
		tag, err := parseTag(f.Tag.Get("apigen"))
		if err == nil && !tag.UserEndpoint && endpointName(f.Name, name) && b.reportOnce(t.Name()+"."+f.Name) {
			*errs = append(*errs, fmt.Errorf("%s (%s.%s) is a URL, URI, path or file in spec: publish the address in "+
				"the producer's status.address and reference the producer, or tag it apigen:%q when it is user input",
				at, t.Name(), f.Name, userEndpointTag))
		}
		b.walkEndpoints(f.Type, at, seen, errs)
	}
}