mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 06:29:08 +02:00
* use libopenapi OpenAPI generator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added a handler for v1alpha1/peers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wire up request validator Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * wired up spec-based validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use sync validation; set base url to v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * moved stuff around, extracted runtime libopenapu deps into runtime_tooling Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * testing /peers path params Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * fixed tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * added user schemas and paths Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use api validation in tests Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * post-merge fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * deleted tmp command used to test validator integration Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * require request body in post/put requests in order to force validation Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * making linter happy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * switch to api/v1alpha1 Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * go mod tidy Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * keep the original order of middleware: metrics, cors, then auth Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * return 422 on validation errors Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * v1alpha1 spec cleanups Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * no need for a double-pointer Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * more linter fixes Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * removed more double pointers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * use di to inject api_v0 and api_v1 http routers Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * do not re-add middleware on repeated call to ApiHandler Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> * calling ApiRouter() now also calls ApiV1Router() Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> --------- Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
package apiv1alpha1
|
|
|
|
import (
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/pb33f/libopenapi"
|
|
"github.com/pb33f/libopenapi/bundler"
|
|
"github.com/pb33f/libopenapi/datamodel"
|
|
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
|
|
"github.com/pb33f/libopenapi/generator/golang"
|
|
)
|
|
|
|
var ApiPath = filepath.Join("shared", "management", "http", "apiv1alpha1", "openapi.yaml")
|
|
|
|
func GenerateV1ApiBindings(openapipath string) ([]byte, *v3.Document, error) {
|
|
specFile, err := os.ReadFile(openapipath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
multiFileDoc, err := libopenapi.NewDocumentWithConfiguration(specFile, &datamodel.DocumentConfiguration{
|
|
AllowFileReferences: true,
|
|
BasePath: filepath.Dir(openapipath),
|
|
SpecFilePath: openapipath,
|
|
ExtractRefsSequentially: true,
|
|
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelError,
|
|
})),
|
|
TransformSiblingRefs: true, // enable openapi 3.1 compliance by default
|
|
MergeReferencedProperties: true, // enable enhanced resolution by default
|
|
PropertyMergeStrategy: datamodel.PreserveLocal, // local properties take precedence
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
multiFileModel, err := multiFileDoc.BuildV3Model()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
bundle, err := bundler.BundleDocumentComposed(&multiFileModel.Model, &bundler.BundleCompositionConfig{
|
|
StrictValidation: true,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
|
|
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
})),
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
model, err := doc.BuildV3Model()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return bundle, &model.Model, nil
|
|
}
|
|
|
|
func GenerateV1Schema(model *v3.Document) (*golang.GeneratedFile, error) {
|
|
// render every schema in components.schemas into one file
|
|
gen := golang.NewGenerator(
|
|
golang.WithGeneratedComment(true),
|
|
golang.WithFormatMapping("date-time", "time.Time", "time"),
|
|
golang.WithOptionalFieldsAsPointers(true),
|
|
golang.WithEnumConstants(true),
|
|
golang.WithNestedTypeNameDelimiter(""),
|
|
golang.WithPackageName("apiv1alpha1"),
|
|
golang.WithFieldNameResolver(toPublicName))
|
|
|
|
return gen.RenderSchemas(model.Components.Schemas)
|
|
}
|
|
|
|
// this is to keep existing naming of fields like "id", "url", etc
|
|
// libopenapi by default converts them to all-uppercase, like "ID", "URL", etc
|
|
func toPublicName(name string) string {
|
|
parts := splitIdentifier(name)
|
|
if len(parts) == 0 {
|
|
return "Value"
|
|
}
|
|
var b strings.Builder
|
|
for _, p := range parts {
|
|
rs := []rune(strings.ToLower(p))
|
|
rs[0] = unicode.ToUpper(rs[0])
|
|
b.WriteString(string(rs))
|
|
}
|
|
out := b.String()
|
|
first := []rune(out)[0]
|
|
if unicode.IsDigit(first) {
|
|
return "Value" + out
|
|
}
|
|
return out
|
|
}
|
|
|
|
func splitIdentifier(name string) []string {
|
|
var raw []string
|
|
var b strings.Builder
|
|
flush := func() {
|
|
if b.Len() > 0 {
|
|
raw = append(raw, b.String())
|
|
b.Reset()
|
|
}
|
|
}
|
|
for _, r := range name {
|
|
switch {
|
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
|
b.WriteRune(r)
|
|
default:
|
|
flush()
|
|
}
|
|
}
|
|
flush()
|
|
var parts []string
|
|
for _, part := range raw {
|
|
parts = append(parts, splitCamel(part)...)
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func splitCamel(value string) []string {
|
|
rs := []rune(value)
|
|
if len(rs) == 0 {
|
|
return nil
|
|
}
|
|
var parts []string
|
|
start := 0
|
|
for i := 1; i < len(rs); i++ {
|
|
prev := rs[i-1]
|
|
cur := rs[i]
|
|
var next rune
|
|
if i+1 < len(rs) {
|
|
next = rs[i+1]
|
|
}
|
|
lowerToUpper := unicode.IsLower(prev) && unicode.IsUpper(cur)
|
|
acronymToWord := unicode.IsUpper(prev) && unicode.IsUpper(cur) && next != 0 && unicode.IsLower(next)
|
|
if lowerToUpper || acronymToWord {
|
|
parts = append(parts, string(rs[start:i]))
|
|
start = i
|
|
}
|
|
}
|
|
parts = append(parts, string(rs[start:]))
|
|
return parts
|
|
}
|