2022-05-05 15:03:51 +01:00
|
|
|
// Copyright (C) 2022 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package apigen
|
|
|
|
|
|
|
|
import (
|
2023-02-22 10:08:34 +00:00
|
|
|
"fmt"
|
2023-08-24 17:55:59 +01:00
|
|
|
"path"
|
2023-02-22 10:08:34 +00:00
|
|
|
"reflect"
|
2023-09-23 18:37:11 +01:00
|
|
|
"regexp"
|
2023-09-25 17:43:30 +01:00
|
|
|
"sort"
|
2023-02-22 10:08:34 +00:00
|
|
|
"strings"
|
2023-10-03 15:30:01 +01:00
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
2023-09-23 19:07:45 +01:00
|
|
|
|
2022-05-05 15:03:51 +01:00
|
|
|
"storj.io/storj/private/api"
|
|
|
|
)
|
|
|
|
|
2023-10-03 18:41:59 +01:00
|
|
|
// groupNameAndPrefixRegExp guarantees that Group name and prefix are empty or have are only formed
|
|
|
|
// by ASCII letters or digits and not starting with a digit.
|
|
|
|
var groupNameAndPrefixRegExp = regexp.MustCompile(`^([A-Za-z][0-9A-Za-z]*)?$`)
|
2023-09-23 18:37:11 +01:00
|
|
|
|
2022-05-05 15:03:51 +01:00
|
|
|
// API represents specific API's configuration.
|
|
|
|
type API struct {
|
2023-08-24 17:55:59 +01:00
|
|
|
// Version is the corresponding version of the API.
|
|
|
|
// It's concatenated to the BasePath, so assuming the base path is "/api" and the version is "v1"
|
|
|
|
// the API paths will begin with `/api/v1`.
|
|
|
|
// When empty, the version doesn't appear in the API paths. If it starts or ends with one or more
|
|
|
|
// "/", they are stripped from the API endpoint paths.
|
|
|
|
Version string
|
|
|
|
Description string
|
|
|
|
// The package name to use for the Go generated code.
|
|
|
|
PackageName string
|
|
|
|
// BasePath is the base path for the API endpoints. E.g. "/api".
|
|
|
|
// It doesn't require to begin with "/". When empty, "/" is used.
|
|
|
|
BasePath string
|
2022-05-05 15:03:51 +01:00
|
|
|
Auth api.Auth
|
|
|
|
EndpointGroups []*EndpointGroup
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group adds new endpoints group to API.
|
2023-09-23 18:37:11 +01:00
|
|
|
// name must be `^([A-Z0-9]\w*)?$“
|
|
|
|
// prefix must be `^\w*$`.
|
2022-05-05 15:03:51 +01:00
|
|
|
func (a *API) Group(name, prefix string) *EndpointGroup {
|
2023-10-03 18:41:59 +01:00
|
|
|
if !groupNameAndPrefixRegExp.MatchString(name) {
|
2023-09-23 18:37:11 +01:00
|
|
|
panic(
|
|
|
|
fmt.Sprintf(
|
2023-09-27 17:48:01 +01:00
|
|
|
"invalid name for API Endpoint Group. name must fulfill the regular expression %q, got %q",
|
2023-10-03 18:41:59 +01:00
|
|
|
groupNameAndPrefixRegExp,
|
2023-09-23 18:37:11 +01:00
|
|
|
name,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
2023-10-03 18:41:59 +01:00
|
|
|
if !groupNameAndPrefixRegExp.MatchString(prefix) {
|
2023-09-23 18:37:11 +01:00
|
|
|
panic(
|
|
|
|
fmt.Sprintf(
|
2023-09-27 17:48:01 +01:00
|
|
|
"invalid prefix for API Endpoint Group %q. prefix must fulfill the regular expression %q, got %q",
|
2023-09-23 18:37:11 +01:00
|
|
|
name,
|
2023-10-03 18:41:59 +01:00
|
|
|
groupNameAndPrefixRegExp,
|
2023-09-23 18:37:11 +01:00
|
|
|
prefix,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-10-03 18:27:08 +01:00
|
|
|
for _, g := range a.EndpointGroups {
|
|
|
|
if strings.EqualFold(g.Name, name) {
|
|
|
|
panic(fmt.Sprintf("name has to be case-insensitive unique across all the groups. name=%q", name))
|
|
|
|
}
|
|
|
|
if strings.EqualFold(g.Prefix, prefix) {
|
|
|
|
panic(fmt.Sprintf("prefix has to be case-insensitive unique across all the groups. prefix=%q", prefix))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-05 15:03:51 +01:00
|
|
|
group := &EndpointGroup{
|
2022-06-09 16:23:08 +01:00
|
|
|
Name: name,
|
|
|
|
Prefix: prefix,
|
2022-05-05 15:03:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
a.EndpointGroups = append(a.EndpointGroups, group)
|
|
|
|
|
|
|
|
return group
|
|
|
|
}
|
2023-02-22 10:08:34 +00:00
|
|
|
|
2023-08-24 17:55:59 +01:00
|
|
|
func (a *API) endpointBasePath() string {
|
|
|
|
if strings.HasPrefix(a.BasePath, "/") {
|
|
|
|
return path.Join(a.BasePath, a.Version)
|
|
|
|
}
|
|
|
|
|
|
|
|
return "/" + path.Join(a.BasePath, a.Version)
|
|
|
|
}
|
|
|
|
|
2023-02-22 10:08:34 +00:00
|
|
|
// StringBuilder is an extension of strings.Builder that allows for writing formatted lines.
|
|
|
|
type StringBuilder struct{ strings.Builder }
|
|
|
|
|
|
|
|
// Writelnf formats arguments according to a format specifier
|
|
|
|
// and appends the resulting string to the StringBuilder's buffer.
|
|
|
|
func (s *StringBuilder) Writelnf(format string, a ...interface{}) {
|
|
|
|
s.WriteString(fmt.Sprintf(format+"\n", a...))
|
|
|
|
}
|
|
|
|
|
2023-09-23 19:07:45 +01:00
|
|
|
// typeCustomName is a reflect.Type with a customized type's name.
|
|
|
|
type typeCustomName struct {
|
|
|
|
reflect.Type
|
|
|
|
|
|
|
|
name string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t typeCustomName) Name() string {
|
|
|
|
return t.name
|
|
|
|
}
|
|
|
|
|
2023-02-22 10:08:34 +00:00
|
|
|
// getElementaryType simplifies a Go type.
|
|
|
|
func getElementaryType(t reflect.Type) reflect.Type {
|
|
|
|
switch t.Kind() {
|
|
|
|
case reflect.Array, reflect.Chan, reflect.Ptr, reflect.Slice:
|
|
|
|
return getElementaryType(t.Elem())
|
|
|
|
default:
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// isNillableType returns whether instances of the given type can be nil.
|
|
|
|
func isNillableType(t reflect.Type) bool {
|
|
|
|
switch t.Kind() {
|
|
|
|
case reflect.Chan, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2023-09-23 19:07:45 +01:00
|
|
|
|
|
|
|
// compoundTypeName create a name composed with base and parts, by joining base as it's and
|
2023-10-03 15:30:01 +01:00
|
|
|
// capitalizing each part. base is not altered.
|
2023-09-23 19:07:45 +01:00
|
|
|
func compoundTypeName(base string, parts ...string) string {
|
|
|
|
titled := make([]string, len(parts))
|
|
|
|
for i := 0; i < len(parts); i++ {
|
2023-10-03 15:30:01 +01:00
|
|
|
titled[i] = capitalize(parts[i])
|
2023-09-23 19:07:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return base + strings.Join(titled, "")
|
|
|
|
}
|
2023-09-25 17:43:30 +01:00
|
|
|
|
2023-10-03 15:30:01 +01:00
|
|
|
func capitalize(s string) string {
|
|
|
|
r, size := utf8.DecodeRuneInString(s)
|
|
|
|
if size <= 0 {
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(unicode.ToTitle(r)) + s[size:]
|
|
|
|
}
|
|
|
|
|
2023-10-03 18:41:59 +01:00
|
|
|
func uncapitalize(s string) string {
|
|
|
|
r, size := utf8.DecodeRuneInString(s)
|
|
|
|
if size <= 0 {
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(unicode.ToLower(r)) + s[size:]
|
|
|
|
}
|
|
|
|
|
2023-09-25 17:43:30 +01:00
|
|
|
type typeAndName struct {
|
|
|
|
Type reflect.Type
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapToSlice(typesAndNames map[reflect.Type]string) []typeAndName {
|
|
|
|
list := make([]typeAndName, 0, len(typesAndNames))
|
|
|
|
for t, n := range typesAndNames {
|
|
|
|
list = append(list, typeAndName{Type: t, Name: n})
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.SliceStable(list, func(i, j int) bool {
|
|
|
|
return list[i].Name < list[j].Name
|
|
|
|
})
|
|
|
|
|
|
|
|
return list
|
|
|
|
}
|
|
|
|
|
|
|
|
// filter returns a new slice of typeAndName values that satisfy the given keep function.
|
|
|
|
func filter(types []typeAndName, keep func(typeAndName) bool) []typeAndName {
|
|
|
|
filtered := make([]typeAndName, 0, len(types))
|
|
|
|
for _, t := range types {
|
|
|
|
if keep(t) {
|
|
|
|
filtered = append(filtered, t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return filtered
|
|
|
|
}
|