1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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)
}
}