mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 22:49:10 +02:00
[management] switch to libopenapi for managing of openapi-based api (#8056)
* 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>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user