api: generate most of OpenAPI automatically (#5918)
enums and structs are now generated automatically. This eliminates some inconsistencies and makes development easier.
This commit is contained in:
@@ -52,18 +52,6 @@ jobs:
|
||||
|
||||
- run: make lint-conf
|
||||
|
||||
go2api:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- run: make lint-go2api
|
||||
|
||||
docslinks:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -88,13 +76,13 @@ jobs:
|
||||
|
||||
- run: make lint-docsorder
|
||||
|
||||
api_docs:
|
||||
apidocs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- run: make lint-api-docs
|
||||
- run: make lint-apidocs
|
||||
|
||||
other:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -16,6 +16,7 @@ help:
|
||||
@echo " lint run linters"
|
||||
@echo " binaries build binaries for all supported platforms"
|
||||
@echo " dockerhub build and push images to Docker Hub"
|
||||
@echo " apidocs generate API documentation"
|
||||
@echo ""
|
||||
|
||||
blank :=
|
||||
|
||||
+1522
-1498
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
// Package main contains a tool to generate openapi.yaml.
|
||||
package main
|
||||
|
||||
import (
|
||||
goast "go/ast"
|
||||
goparser "go/parser"
|
||||
gotoken "go/token"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
)
|
||||
|
||||
var enums = []struct {
|
||||
externalName string
|
||||
internalName string
|
||||
File string
|
||||
}{
|
||||
{
|
||||
externalName: "ErrorStatus",
|
||||
internalName: "APIErrorStatus",
|
||||
File: filepath.Join("internal", "defs", "api.go"),
|
||||
},
|
||||
{
|
||||
externalName: "AlwaysAvailableTrackCodec",
|
||||
internalName: "AlwaysAvailableTrackCodec",
|
||||
File: filepath.Join("internal", "conf", "always_available_track_codec.go"),
|
||||
},
|
||||
{
|
||||
externalName: "AuthAction",
|
||||
internalName: "AuthAction",
|
||||
File: filepath.Join("internal", "conf", "auth_action.go"),
|
||||
},
|
||||
{
|
||||
externalName: "AuthMethod",
|
||||
internalName: "AuthMethod",
|
||||
File: filepath.Join("internal", "conf", "auth_method.go"),
|
||||
},
|
||||
{
|
||||
externalName: "Encryption",
|
||||
internalName: "Encryption",
|
||||
File: filepath.Join("internal", "conf", "encryption.go"),
|
||||
},
|
||||
{
|
||||
externalName: "MoQSessionState",
|
||||
internalName: "APIMoQSessionState",
|
||||
File: filepath.Join("internal", "defs", "api_moq.go"),
|
||||
},
|
||||
{
|
||||
externalName: "OKStatus",
|
||||
internalName: "APIOKStatus",
|
||||
File: filepath.Join("internal", "defs", "api.go"),
|
||||
},
|
||||
{
|
||||
externalName: "PathReaderType",
|
||||
internalName: "APIPathReaderType",
|
||||
File: filepath.Join("internal", "defs", "api_path.go"),
|
||||
},
|
||||
{
|
||||
externalName: "PathSourceType",
|
||||
internalName: "APIPathSourceType",
|
||||
File: filepath.Join("internal", "defs", "api_path.go"),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodec",
|
||||
internalName: "Label",
|
||||
File: filepath.Join("internal", "formatlabel", "label.go"),
|
||||
},
|
||||
{
|
||||
externalName: "RTMPConnState",
|
||||
internalName: "APIRTMPConnState",
|
||||
File: filepath.Join("internal", "defs", "api_rtmp.go"),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPRangeType",
|
||||
internalName: "RTSPRangeType",
|
||||
File: filepath.Join("internal", "conf", "rtsp_range_type.go"),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPSessionState",
|
||||
internalName: "APIRTSPSessionState",
|
||||
File: filepath.Join("internal", "defs", "api_rtsp.go"),
|
||||
},
|
||||
{
|
||||
externalName: "RecordFormat",
|
||||
internalName: "RecordFormat",
|
||||
File: filepath.Join("internal", "conf", "record_format.go"),
|
||||
},
|
||||
{
|
||||
externalName: "SRTConnState",
|
||||
internalName: "APISRTConnState",
|
||||
File: filepath.Join("internal", "defs", "api_srt.go"),
|
||||
},
|
||||
{
|
||||
externalName: "WebRTCSessionState",
|
||||
internalName: "APIWebRTCSessionState",
|
||||
File: filepath.Join("internal", "defs", "api_webrtc.go"),
|
||||
},
|
||||
}
|
||||
|
||||
func extractEnumValues(name, file string) ([]string, error) {
|
||||
fset := gotoken.NewFileSet()
|
||||
|
||||
f, err := goparser.ParseFile(fset, file, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var values []string
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*goast.GenDecl)
|
||||
if !ok || genDecl.Tok != gotoken.CONST {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
var valSpec *goast.ValueSpec
|
||||
valSpec, ok = spec.(*goast.ValueSpec)
|
||||
if !ok || valSpec.Type == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var ident *goast.Ident
|
||||
ident, ok = valSpec.Type.(*goast.Ident)
|
||||
if !ok || ident.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, val := range valSpec.Values {
|
||||
var lit *goast.BasicLit
|
||||
lit, ok = val.(*goast.BasicLit)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
values = append(values, strings.Trim(lit.Value, `"`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func addEnums(astFile *ast.File) error {
|
||||
for _, e := range enums {
|
||||
values, err := extractEnumValues(e.internalName, e.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schema := &openAPISchema{
|
||||
Type: "string",
|
||||
Enum: values,
|
||||
}
|
||||
|
||||
schemaNode, err := yaml.ValueToNode(map[string]*openAPISchema{e.externalName: schema})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indentBlockSequences(schemaNode)
|
||||
|
||||
addBlankLineBeforeNewSchemaEntry(schemaNode)
|
||||
|
||||
schemasPath, err := yaml.PathString("$.components.schemas")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = schemasPath.MergeFromNode(astFile, schemaNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"github.com/goccy/go-yaml/parser"
|
||||
"github.com/goccy/go-yaml/token"
|
||||
)
|
||||
|
||||
const (
|
||||
templateFile = "internal/apidocsgen/openapi.template.yaml"
|
||||
outputFile = "api/openapi.yaml"
|
||||
)
|
||||
|
||||
type openAPISchema struct {
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Enum []string `yaml:"enum,omitempty"`
|
||||
OneOf []openAPIProperty `yaml:"oneOf,omitempty"`
|
||||
Properties map[string]openAPIProperty `yaml:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type openAPIProperty struct {
|
||||
Ref string `yaml:"$ref,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Format string `yaml:"format,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
AllOf []openAPIProperty `yaml:"allOf,omitempty"`
|
||||
Nullable bool `yaml:"nullable,omitempty"`
|
||||
Deprecated bool `yaml:"deprecated,omitempty"`
|
||||
Enum []string `yaml:"enum,omitempty"`
|
||||
Items *openAPIProperty `yaml:"items,omitempty"`
|
||||
}
|
||||
|
||||
func indentBlockSequences(node ast.Node) {
|
||||
switch n := node.(type) {
|
||||
case *ast.MappingNode:
|
||||
for _, value := range n.Values {
|
||||
indentBlockSequences(value)
|
||||
}
|
||||
|
||||
case *ast.MappingValueNode:
|
||||
if seq, ok := n.Value.(*ast.SequenceNode); ok && !seq.IsFlowStyle && len(seq.Values) > 0 {
|
||||
seq.AddColumn(2)
|
||||
}
|
||||
indentBlockSequences(n.Value)
|
||||
|
||||
case *ast.SequenceNode:
|
||||
for _, value := range n.Values {
|
||||
indentBlockSequences(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBlankLineBeforeNewSchemaEntry(schemaNode ast.Node) {
|
||||
if schemasMapping, ok := schemaNode.(*ast.MappingNode); ok && len(schemasMapping.Values) > 0 {
|
||||
keyTk := schemasMapping.Values[0].Key.GetToken()
|
||||
keyTk.Position.Line = 3
|
||||
keyTk.Prev = &token.Token{
|
||||
Type: token.StringType,
|
||||
Position: &token.Position{Line: 1},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseTemplate() (*ast.File, error) {
|
||||
data, err := os.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parser.ParseBytes(data, parser.ParseComments)
|
||||
}
|
||||
|
||||
func generate() ([]byte, error) {
|
||||
astFile, err := parseTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = addEnums(astFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = addStructs(astFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(astFile.String()), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
check := flag.Bool("check", false, "check whether the generated OpenAPI matches the file on disk")
|
||||
flag.Parse()
|
||||
|
||||
generated, err := generate()
|
||||
if err != nil {
|
||||
log.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *check {
|
||||
var existing []byte
|
||||
existing, err = os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
log.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !bytes.Equal(existing, generated) {
|
||||
log.Printf("error: %v\n", fmt.Errorf("%s is outdated, run `go run ./internal/apidocsgen`", outputFile))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(outputFile, generated, 0o644)
|
||||
if err != nil {
|
||||
log.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,525 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goast "go/ast"
|
||||
goparser "go/parser"
|
||||
gotoken "go/token"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var structs = []struct {
|
||||
externalName string
|
||||
typ reflect.Type
|
||||
}{
|
||||
{
|
||||
externalName: "AlwaysAvailableTrack",
|
||||
typ: reflect.TypeOf(conf.AlwaysAvailableTrack{}),
|
||||
},
|
||||
{
|
||||
externalName: "AuthInternalUser",
|
||||
typ: reflect.TypeOf(conf.AuthInternalUser{}),
|
||||
},
|
||||
{
|
||||
externalName: "AuthInternalUserPermission",
|
||||
typ: reflect.TypeOf(conf.AuthInternalUserPermission{}),
|
||||
},
|
||||
{
|
||||
externalName: "Error",
|
||||
typ: reflect.TypeOf(defs.APIError{}),
|
||||
},
|
||||
{
|
||||
externalName: "GlobalConf",
|
||||
typ: reflect.TypeOf(conf.Conf{}),
|
||||
},
|
||||
{
|
||||
externalName: "HLSMuxer",
|
||||
typ: reflect.TypeOf(defs.APIHLSMuxer{}),
|
||||
},
|
||||
{
|
||||
externalName: "HLSMuxerList",
|
||||
typ: reflect.TypeOf(defs.APIHLSMuxerList{}),
|
||||
},
|
||||
{
|
||||
externalName: "HLSSession",
|
||||
typ: reflect.TypeOf(defs.APIHLSSession{}),
|
||||
},
|
||||
{
|
||||
externalName: "HLSSessionList",
|
||||
typ: reflect.TypeOf(defs.APIHLSSessionList{}),
|
||||
},
|
||||
{
|
||||
externalName: "Info",
|
||||
typ: reflect.TypeOf(defs.APIInfo{}),
|
||||
},
|
||||
{
|
||||
externalName: "MoQSession",
|
||||
typ: reflect.TypeOf(defs.APIMoQSession{}),
|
||||
},
|
||||
{
|
||||
externalName: "MoQSessionList",
|
||||
typ: reflect.TypeOf(defs.APIMoQSessionList{}),
|
||||
},
|
||||
{
|
||||
externalName: "OK",
|
||||
typ: reflect.TypeOf(defs.APIOK{}),
|
||||
},
|
||||
{
|
||||
externalName: "Path",
|
||||
typ: reflect.TypeOf(defs.APIPath{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathConf",
|
||||
typ: reflect.TypeOf(conf.Path{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathConfList",
|
||||
typ: reflect.TypeOf(defs.APIPathConfList{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathList",
|
||||
typ: reflect.TypeOf(defs.APIPathList{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathReader",
|
||||
typ: reflect.TypeOf(defs.APIPathReader{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathSource",
|
||||
typ: reflect.TypeOf(defs.APIPathSource{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrack",
|
||||
typ: reflect.TypeOf(defs.APIPathTrack{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsAC3",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsAC3{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsAV1",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsAV1{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsG711",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsG711{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsH264",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsH264{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsH265",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsH265{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsLPCM",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsLPCM{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsMPEG4Audio",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsMPEG4Audio{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsOpus",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsOpus{}),
|
||||
},
|
||||
{
|
||||
externalName: "PathTrackCodecPropsVP9",
|
||||
typ: reflect.TypeOf(defs.APIPathTrackCodecPropsVP9{}),
|
||||
},
|
||||
{
|
||||
externalName: "Recording",
|
||||
typ: reflect.TypeOf(defs.APIRecording{}),
|
||||
},
|
||||
{
|
||||
externalName: "RecordingList",
|
||||
typ: reflect.TypeOf(defs.APIRecordingList{}),
|
||||
},
|
||||
{
|
||||
externalName: "RecordingSegment",
|
||||
typ: reflect.TypeOf(defs.APIRecordingSegment{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTMPConn",
|
||||
typ: reflect.TypeOf(defs.APIRTMPConn{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTMPConnList",
|
||||
typ: reflect.TypeOf(defs.APIRTMPConnList{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPConn",
|
||||
typ: reflect.TypeOf(defs.APIRTSPConn{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPConnList",
|
||||
typ: reflect.TypeOf(defs.APIRTSPConnsList{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPSession",
|
||||
typ: reflect.TypeOf(defs.APIRTSPSession{}),
|
||||
},
|
||||
{
|
||||
externalName: "RTSPSessionList",
|
||||
typ: reflect.TypeOf(defs.APIRTSPSessionList{}),
|
||||
},
|
||||
{
|
||||
externalName: "SRTConn",
|
||||
typ: reflect.TypeOf(defs.APISRTConn{}),
|
||||
},
|
||||
{
|
||||
externalName: "SRTConnList",
|
||||
typ: reflect.TypeOf(defs.APISRTConnList{}),
|
||||
},
|
||||
{
|
||||
externalName: "WebRTCICEServer",
|
||||
typ: reflect.TypeOf(conf.WebRTCICEServer{}),
|
||||
},
|
||||
{
|
||||
externalName: "WebRTCSession",
|
||||
typ: reflect.TypeOf(defs.APIWebRTCSession{}),
|
||||
},
|
||||
{
|
||||
externalName: "WebRTCSessionList",
|
||||
typ: reflect.TypeOf(defs.APIWebRTCSessionList{}),
|
||||
},
|
||||
}
|
||||
|
||||
const modulePathPrefix = "github.com/bluenviron/mediamtx/"
|
||||
|
||||
func wrapRef(rt reflect.Type, p openAPIProperty) openAPIProperty {
|
||||
if p.Ref == "" {
|
||||
return p
|
||||
}
|
||||
|
||||
if isStructEnum(rt) {
|
||||
p.Type = "string"
|
||||
} else if rt.Kind() == reflect.Struct {
|
||||
p.Type = "object"
|
||||
}
|
||||
|
||||
p.AllOf = []openAPIProperty{{Ref: p.Ref}}
|
||||
p.Ref = ""
|
||||
return p
|
||||
}
|
||||
|
||||
func goTypeToOpenAPI(rt reflect.Type) (openAPIProperty, error) {
|
||||
if rt.Kind() == reflect.Pointer {
|
||||
prop, err := goTypeToOpenAPI(rt.Elem())
|
||||
if err != nil {
|
||||
return openAPIProperty{}, err
|
||||
}
|
||||
|
||||
prop = wrapRef(rt.Elem(), prop)
|
||||
prop.Nullable = true
|
||||
return prop, nil
|
||||
}
|
||||
|
||||
if isStructEnum(rt) {
|
||||
return openAPIProperty{Ref: "#/components/schemas/" + schemaName(rt)}, nil
|
||||
}
|
||||
|
||||
if rt == reflect.TypeOf((*defs.APIPathTrackCodecProps)(nil)).Elem() {
|
||||
return openAPIProperty{
|
||||
Type: "object",
|
||||
AllOf: []openAPIProperty{{Ref: "#/components/schemas/" + schemaName(rt)}},
|
||||
Nullable: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case rt == reflect.TypeOf(uuid.UUID{}):
|
||||
return openAPIProperty{Type: "string", Format: "uuid"}, nil
|
||||
|
||||
case rt == reflect.TypeOf(time.Time{}):
|
||||
return openAPIProperty{Type: "string"}, nil
|
||||
|
||||
case rt == reflect.TypeOf(conf.Duration(0)):
|
||||
return openAPIProperty{Type: "string"}, nil
|
||||
|
||||
case rt == reflect.TypeOf(conf.IPNetwork{}):
|
||||
return openAPIProperty{Type: "string"}, nil
|
||||
|
||||
case rt == reflect.TypeOf(conf.StringSize(0)):
|
||||
return openAPIProperty{Type: "string"}, nil
|
||||
|
||||
case rt == reflect.TypeOf(conf.RTSPTransports{}):
|
||||
items := openAPIProperty{Type: "string", Enum: []string{"udp", "multicast", "tcp"}}
|
||||
return openAPIProperty{Type: "array", Items: &items}, nil
|
||||
|
||||
case rt.Kind() == reflect.String:
|
||||
return openAPIProperty{Type: "string"}, nil
|
||||
|
||||
case rt.Kind() >= reflect.Int && rt.Kind() <= reflect.Int64:
|
||||
return openAPIProperty{Type: "integer", Format: "int64"}, nil
|
||||
|
||||
case rt.Kind() >= reflect.Uint && rt.Kind() <= reflect.Uint64:
|
||||
return openAPIProperty{Type: "integer", Format: "uint64"}, nil
|
||||
|
||||
case rt.Kind() == reflect.Float32 || rt.Kind() == reflect.Float64:
|
||||
return openAPIProperty{Type: "number", Format: "double"}, nil
|
||||
|
||||
case rt.Kind() == reflect.Bool:
|
||||
return openAPIProperty{Type: "boolean"}, nil
|
||||
|
||||
case rt.Kind() == reflect.Struct:
|
||||
return openAPIProperty{Ref: "#/components/schemas/" + schemaName(rt)}, nil
|
||||
|
||||
case rt.Kind() == reflect.Slice:
|
||||
items, err := goTypeToOpenAPI(rt.Elem())
|
||||
if err != nil {
|
||||
return openAPIProperty{}, err
|
||||
}
|
||||
|
||||
return openAPIProperty{Type: "array", Items: &items}, nil
|
||||
|
||||
default:
|
||||
return openAPIProperty{}, fmt.Errorf("unhandled type: %s", rt.String())
|
||||
}
|
||||
}
|
||||
|
||||
func schemaName(rt reflect.Type) string {
|
||||
if rt == reflect.TypeOf(conf.Path{}) {
|
||||
return "PathConf"
|
||||
}
|
||||
|
||||
if rt == reflect.TypeOf(defs.APIPathTrackCodec("")) {
|
||||
return "PathTrackCodec"
|
||||
}
|
||||
|
||||
if rt == reflect.TypeOf((*defs.APIPathTrackCodecProps)(nil)).Elem() {
|
||||
return "PathTrackCodecProps"
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(rt.Name(), "API")
|
||||
}
|
||||
|
||||
func isStructEnum(rt reflect.Type) bool {
|
||||
switch rt {
|
||||
case reflect.TypeOf(defs.APIOKStatus("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIErrorStatus("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.AuthAction("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.AlwaysAvailableTrackCodec("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.AuthMethod("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.Encryption("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.HLSVariant(0)):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.LogDestination(0)):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.LogLevel(0)):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.RecordFormat("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPAuthMethod(0)):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPRangeType("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPTransport{}):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathSourceType("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathReaderType("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathTrackCodec("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIRTMPConnState("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIRTSPSessionState("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIWebRTCSessionState("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APIMoQSessionState("")):
|
||||
return true
|
||||
|
||||
case reflect.TypeOf(defs.APISRTConnState("")):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func extractStructDescriptions(rt reflect.Type) (map[string]string, error) {
|
||||
pkgPath := rt.PkgPath()
|
||||
dirPath, hasModulePrefix := strings.CutPrefix(pkgPath, modulePathPrefix)
|
||||
if !hasModulePrefix {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
fset := gotoken.NewFileSet()
|
||||
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := dirPath + "/" + entry.Name()
|
||||
file, parseErr := goparser.ParseFile(fset, filePath, nil, goparser.ParseComments)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
for _, decl := range file.Decls {
|
||||
genDecl, genOK := decl.(*goast.GenDecl)
|
||||
if !genOK || genDecl.Tok != gotoken.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, typeOK := spec.(*goast.TypeSpec)
|
||||
if !typeOK || typeSpec.Name.Name != rt.Name() {
|
||||
continue
|
||||
}
|
||||
|
||||
structType, structOK := typeSpec.Type.(*goast.StructType)
|
||||
if !structOK {
|
||||
continue
|
||||
}
|
||||
|
||||
descriptions := make(map[string]string)
|
||||
for _, field := range structType.Fields.List {
|
||||
if field.Tag == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonTag := extractStructTag(field.Tag.Value, "json")
|
||||
jsonName, _, _ := strings.Cut(jsonTag, ",")
|
||||
if jsonName == "" || jsonName == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
description := ""
|
||||
if field.Doc != nil {
|
||||
description = normalizeDescription(field.Doc.Text())
|
||||
} else if field.Comment != nil {
|
||||
description = normalizeDescription(field.Comment.Text())
|
||||
}
|
||||
|
||||
if description != "" {
|
||||
descriptions[jsonName] = description
|
||||
}
|
||||
}
|
||||
|
||||
return descriptions, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
func extractStructTag(tagValue, key string) string {
|
||||
tagValue = strings.Trim(tagValue, "`")
|
||||
return reflect.StructTag(tagValue).Get(key)
|
||||
}
|
||||
|
||||
func normalizeDescription(description string) string {
|
||||
return strings.Join(strings.Fields(description), " ")
|
||||
}
|
||||
|
||||
func generateStructSchema(rt reflect.Type, descriptions map[string]string) (openAPISchema, error) {
|
||||
schema := openAPISchema{
|
||||
Type: "object",
|
||||
Properties: make(map[string]openAPIProperty),
|
||||
}
|
||||
|
||||
for field := range rt.Fields() {
|
||||
jsonTag := field.Tag.Get("json")
|
||||
name, _, _ := strings.Cut(jsonTag, ",")
|
||||
deprecated := field.Tag.Get("deprecated") == "true"
|
||||
|
||||
if name == "" || name == "-" || name == "pathDefaults" || name == "paths" ||
|
||||
(strings.Contains(jsonTag, ",omitempty") && !deprecated) {
|
||||
continue
|
||||
}
|
||||
|
||||
prop, err := goTypeToOpenAPI(field.Type)
|
||||
if err != nil {
|
||||
return openAPISchema{}, err
|
||||
}
|
||||
|
||||
prop.Deprecated = deprecated
|
||||
if deprecated {
|
||||
prop = wrapRef(field.Type, prop)
|
||||
}
|
||||
prop.Description = descriptions[name]
|
||||
|
||||
schema.Properties[name] = prop
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func addStructs(astFile *ast.File) error {
|
||||
for _, s := range structs {
|
||||
descriptions, err := extractStructDescriptions(s.typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schema, err := generateStructSchema(s.typ, descriptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaNode, err := yaml.ValueToNode(map[string]openAPISchema{s.externalName: schema})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indentBlockSequences(schemaNode)
|
||||
|
||||
addBlankLineBeforeNewSchemaEntry(schemaNode)
|
||||
|
||||
schemasPath, err := yaml.PathString("$.components.schemas")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = schemasPath.MergeFromNode(astFile, schemaNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -69,7 +69,7 @@ type APISRTConn struct {
|
||||
// The total accumulated time in microseconds, during which the SRT sender has some data to transmit,
|
||||
// including packets that have been sent, but not yet acknowledged
|
||||
UsSndDuration uint64 `json:"usSndDuration"`
|
||||
// ??
|
||||
|
||||
PacketsReceivedBelated uint64 `json:"packetsReceivedBelated"`
|
||||
// The total number of dropped by the SRT sender DATA packets that have no chance to be delivered in time
|
||||
PacketsSendDrop uint64 `json:"packetsSendDrop"`
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
//go:build enable_linters
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/defs"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type openAPIProperty struct {
|
||||
Ref string `yaml:"$ref"`
|
||||
Type string `yaml:"type"`
|
||||
Format string `yaml:"format"`
|
||||
AllOf []openAPIProperty `yaml:"allOf"`
|
||||
Nullable bool `yaml:"nullable"`
|
||||
Deprecated bool `yaml:"deprecated"`
|
||||
Enum []string `yaml:"enum"`
|
||||
Items *openAPIProperty `yaml:"items"`
|
||||
}
|
||||
|
||||
func wrapRef(rt reflect.Type, p openAPIProperty) openAPIProperty {
|
||||
if p.Ref == "" {
|
||||
return p
|
||||
}
|
||||
|
||||
if _, ok := goEnumToApi(rt); ok {
|
||||
p.Type = "string"
|
||||
} else if rt.Kind() == reflect.Struct {
|
||||
p.Type = "object"
|
||||
}
|
||||
|
||||
p.AllOf = []openAPIProperty{{Ref: p.Ref}}
|
||||
p.Ref = ""
|
||||
return p
|
||||
}
|
||||
|
||||
type openAPISchema struct {
|
||||
Type string `yaml:"type"`
|
||||
Enum []string `yaml:"enum"`
|
||||
OneOf []openAPIProperty `yaml:"oneOf"`
|
||||
Properties map[string]openAPIProperty `yaml:"properties"`
|
||||
}
|
||||
|
||||
type openAPI struct {
|
||||
Components struct {
|
||||
Schemas map[string]openAPISchema `yaml:"schemas"`
|
||||
} `yaml:"components"`
|
||||
}
|
||||
|
||||
func schemaName(rt reflect.Type) string {
|
||||
name := strings.TrimPrefix(rt.Name(), "API")
|
||||
|
||||
if rt.PkgPath() == "github.com/bluenviron/mediamtx/internal/conf" && name == "Path" {
|
||||
return "PathConf"
|
||||
}
|
||||
|
||||
if rt == reflect.TypeOf(defs.APIPathTrackCodec("")) {
|
||||
return "PathTrackCodec"
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func goStructToApi(t *testing.T, rt reflect.Type) openAPIProperty {
|
||||
if rt.Kind() == reflect.Pointer {
|
||||
prop := goStructToApi(t, rt.Elem())
|
||||
prop = wrapRef(rt.Elem(), prop)
|
||||
prop.Nullable = true
|
||||
return prop
|
||||
}
|
||||
|
||||
if _, ok := goEnumToApi(rt); ok {
|
||||
return openAPIProperty{Ref: "#/components/schemas/" + schemaName(rt)}
|
||||
}
|
||||
|
||||
if rt == reflect.TypeOf((*defs.APIPathTrackCodecProps)(nil)).Elem() {
|
||||
return openAPIProperty{
|
||||
Type: "object",
|
||||
AllOf: []openAPIProperty{{Ref: "#/components/schemas/" + schemaName(rt)}},
|
||||
Nullable: true,
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case rt == reflect.TypeOf(""):
|
||||
return openAPIProperty{Type: "string"}
|
||||
|
||||
case rt == reflect.TypeOf(int(0)):
|
||||
return openAPIProperty{Type: "integer", Format: "int64"}
|
||||
|
||||
case rt == reflect.TypeOf(uint(0)):
|
||||
return openAPIProperty{Type: "integer", Format: "uint64"}
|
||||
|
||||
case rt == reflect.TypeOf(uint64(0)):
|
||||
return openAPIProperty{Type: "integer", Format: "uint64"}
|
||||
|
||||
case rt == reflect.TypeOf(float64(0)):
|
||||
return openAPIProperty{Type: "number", Format: "double"}
|
||||
|
||||
case rt == reflect.TypeOf(false):
|
||||
return openAPIProperty{Type: "boolean"}
|
||||
|
||||
case rt == reflect.TypeOf(uuid.UUID{}):
|
||||
return openAPIProperty{Type: "string", Format: "uuid"}
|
||||
|
||||
case rt == reflect.TypeOf(time.Time{}) ||
|
||||
rt == reflect.TypeOf(conf.Duration(0)) ||
|
||||
rt == reflect.TypeOf(conf.IPNetwork{}) ||
|
||||
rt == reflect.TypeOf(conf.Credential("")) ||
|
||||
rt == reflect.TypeOf(conf.StringSize(0)):
|
||||
return openAPIProperty{Type: "string"}
|
||||
|
||||
case rt == reflect.TypeOf(conf.RTSPTransports{}):
|
||||
return openAPIProperty{
|
||||
Type: "array",
|
||||
Items: &openAPIProperty{
|
||||
Type: "string",
|
||||
Enum: []string{"udp", "multicast", "tcp"},
|
||||
},
|
||||
}
|
||||
|
||||
case rt.Kind() == reflect.Struct:
|
||||
return openAPIProperty{
|
||||
Ref: "#/components/schemas/" + schemaName(rt),
|
||||
}
|
||||
|
||||
case rt.Kind() == reflect.Slice:
|
||||
items := goStructToApi(t, rt.Elem())
|
||||
return openAPIProperty{
|
||||
Type: "array",
|
||||
Items: &items,
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled type: %v", rt)
|
||||
return openAPIProperty{}
|
||||
}
|
||||
}
|
||||
|
||||
func goEnumToApi(rt reflect.Type) (openAPISchema, bool) {
|
||||
switch rt {
|
||||
case reflect.TypeOf(defs.APIOKStatus("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"ok"}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIErrorStatus("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"error"}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathSourceType("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"hlsSource",
|
||||
"redirect",
|
||||
"rpiCameraSource",
|
||||
"rtmpConn",
|
||||
"rtmpsConn",
|
||||
"rtmpSource",
|
||||
"rtspSession",
|
||||
"rtspSource",
|
||||
"rtspsSession",
|
||||
"srtConn",
|
||||
"srtSource",
|
||||
"mpegtsSource",
|
||||
"rtpSource",
|
||||
"webRTCSession",
|
||||
"webRTCSource",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathReaderType("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"hlsSession",
|
||||
"rtmpConn",
|
||||
"rtmpsConn",
|
||||
"rtspConn",
|
||||
"rtspSession",
|
||||
"rtspsConn",
|
||||
"rtspsSession",
|
||||
"srtConn",
|
||||
"webRTCSession",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIPathTrackCodec("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"AV1",
|
||||
"VP9",
|
||||
"VP8",
|
||||
"H265",
|
||||
"H264",
|
||||
"MPEG-4 Video",
|
||||
"MPEG-1/2 Video",
|
||||
"M-JPEG",
|
||||
"Opus",
|
||||
"Vorbis",
|
||||
"MPEG-4 Audio",
|
||||
"MPEG-4 Audio LATM",
|
||||
"MPEG-1/2 Audio",
|
||||
"AC3",
|
||||
"Speex",
|
||||
"G726",
|
||||
"G722",
|
||||
"G711",
|
||||
"LPCM",
|
||||
"MPEG-TS",
|
||||
"KLV",
|
||||
"Generic",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.AlwaysAvailableTrackCodec("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"AV1",
|
||||
"VP9",
|
||||
"H265",
|
||||
"H264",
|
||||
"MPEG4Audio",
|
||||
"Opus",
|
||||
"G711",
|
||||
"LPCM",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.AuthAction("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"publish",
|
||||
"read",
|
||||
"playback",
|
||||
"api",
|
||||
"metrics",
|
||||
"pprof",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.AuthMethod("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"internal",
|
||||
"http",
|
||||
"jwt",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.Encryption("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"no",
|
||||
"optional",
|
||||
"strict",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.HLSVariant(0)):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"mpegts",
|
||||
"fmp4",
|
||||
"lowLatency",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.LogDestination(0)):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"stdout",
|
||||
"file",
|
||||
"syslog",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.LogLevel(0)):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"error",
|
||||
"warn",
|
||||
"info",
|
||||
"debug",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.RecordFormat("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"fmp4",
|
||||
"mpegts",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPAuthMethod(0)):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"basic",
|
||||
"digest",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPRangeType("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"",
|
||||
"clock",
|
||||
"npt",
|
||||
"smpte",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(conf.RTSPTransport{}):
|
||||
return openAPISchema{Type: "string", Enum: []string{
|
||||
"udp",
|
||||
"multicast",
|
||||
"tcp",
|
||||
"automatic",
|
||||
}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIRTMPConnState("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"idle", "read", "publish"}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIWebRTCSessionState("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"read", "publish"}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APISRTConnState("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"idle", "read", "publish"}}, true
|
||||
|
||||
case reflect.TypeOf(defs.APIRTSPSessionState("")):
|
||||
return openAPISchema{Type: "string", Enum: []string{"idle", "read", "publish"}}, true
|
||||
}
|
||||
|
||||
return openAPISchema{}, false
|
||||
}
|
||||
|
||||
func TestGo2API(t *testing.T) {
|
||||
byts, err := os.ReadFile("../../../api/openapi.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
var doc openAPI
|
||||
err = yaml.Unmarshal(byts, &doc)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("structs", func(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
openAPIKey string
|
||||
goStruct any
|
||||
}{
|
||||
{
|
||||
"AlwaysAvailableTrack",
|
||||
conf.AlwaysAvailableTrack{},
|
||||
},
|
||||
{
|
||||
"AuthInternalUser",
|
||||
conf.AuthInternalUser{},
|
||||
},
|
||||
{
|
||||
"AuthInternalUserPermission",
|
||||
conf.AuthInternalUserPermission{},
|
||||
},
|
||||
{
|
||||
"GlobalConf",
|
||||
conf.Conf{},
|
||||
},
|
||||
{
|
||||
"HLSMuxer",
|
||||
defs.APIHLSMuxer{},
|
||||
},
|
||||
{
|
||||
"HLSMuxerList",
|
||||
defs.APIHLSMuxerList{},
|
||||
},
|
||||
{
|
||||
"HLSSession",
|
||||
defs.APIHLSSession{},
|
||||
},
|
||||
{
|
||||
"HLSSessionList",
|
||||
defs.APIHLSSessionList{},
|
||||
},
|
||||
{
|
||||
"Info",
|
||||
defs.APIInfo{},
|
||||
},
|
||||
{
|
||||
"Path",
|
||||
defs.APIPath{},
|
||||
},
|
||||
{
|
||||
"PathConf",
|
||||
conf.Path{},
|
||||
},
|
||||
{
|
||||
"PathConfList",
|
||||
defs.APIPathConfList{},
|
||||
},
|
||||
{
|
||||
"PathList",
|
||||
defs.APIPathList{},
|
||||
},
|
||||
{
|
||||
"PathReader",
|
||||
defs.APIPathReader{},
|
||||
},
|
||||
{
|
||||
"PathSource",
|
||||
defs.APIPathSource{},
|
||||
},
|
||||
{
|
||||
"PathTrack",
|
||||
defs.APIPathTrack{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsAV1",
|
||||
defs.APIPathTrackCodecPropsAV1{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsVP9",
|
||||
defs.APIPathTrackCodecPropsVP9{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsH265",
|
||||
defs.APIPathTrackCodecPropsH265{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsH264",
|
||||
defs.APIPathTrackCodecPropsH264{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsOpus",
|
||||
defs.APIPathTrackCodecPropsOpus{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsMPEG4Audio",
|
||||
defs.APIPathTrackCodecPropsMPEG4Audio{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsAC3",
|
||||
defs.APIPathTrackCodecPropsAC3{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsG711",
|
||||
defs.APIPathTrackCodecPropsG711{},
|
||||
},
|
||||
{
|
||||
"PathTrackCodecPropsLPCM",
|
||||
defs.APIPathTrackCodecPropsLPCM{},
|
||||
},
|
||||
{
|
||||
"Recording",
|
||||
defs.APIRecording{},
|
||||
},
|
||||
{
|
||||
"RecordingList",
|
||||
defs.APIRecordingList{},
|
||||
},
|
||||
{
|
||||
"RecordingSegment",
|
||||
defs.APIRecordingSegment{},
|
||||
},
|
||||
{
|
||||
"RTMPConn",
|
||||
defs.APIRTMPConn{},
|
||||
},
|
||||
{
|
||||
"RTMPConnList",
|
||||
defs.APIRTMPConnList{},
|
||||
},
|
||||
{
|
||||
"RTSPConn",
|
||||
defs.APIRTSPConn{},
|
||||
},
|
||||
{
|
||||
"RTSPConnList",
|
||||
defs.APIRTSPConnsList{},
|
||||
},
|
||||
{
|
||||
"RTSPSession",
|
||||
defs.APIRTSPSession{},
|
||||
},
|
||||
{
|
||||
"RTSPSessionList",
|
||||
defs.APIRTSPSessionList{},
|
||||
},
|
||||
{
|
||||
"SRTConn",
|
||||
defs.APISRTConn{},
|
||||
},
|
||||
{
|
||||
"SRTConnList",
|
||||
defs.APISRTConnList{},
|
||||
},
|
||||
{
|
||||
"WebRTCSession",
|
||||
defs.APIWebRTCSession{},
|
||||
},
|
||||
{
|
||||
"WebRTCSessionList",
|
||||
defs.APIWebRTCSessionList{},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.openAPIKey, func(t *testing.T) {
|
||||
content1 := doc.Components.Schemas[ca.openAPIKey]
|
||||
|
||||
content2 := openAPISchema{
|
||||
Type: "object",
|
||||
Properties: make(map[string]openAPIProperty),
|
||||
}
|
||||
|
||||
ty := reflect.TypeOf(ca.goStruct)
|
||||
|
||||
for i := range ty.NumField() {
|
||||
sf := ty.Field(i)
|
||||
js := sf.Tag.Get("json")
|
||||
name, _, _ := strings.Cut(js, ",")
|
||||
deprecated := sf.Tag.Get("deprecated") == "true"
|
||||
|
||||
if name != "" && name != "-" && name != "paths" && name != "pathDefaults" &&
|
||||
(!strings.Contains(js, ",omitempty") || deprecated) {
|
||||
prop := goStructToApi(t, sf.Type)
|
||||
prop.Deprecated = deprecated
|
||||
if deprecated {
|
||||
prop = wrapRef(sf.Type, prop)
|
||||
}
|
||||
content2.Properties[name] = prop
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, content2, content1)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oneOfs", func(t *testing.T) {
|
||||
require.Equal(t, openAPISchema{OneOf: []openAPIProperty{
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsAV1"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsVP9"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsH265"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsH264"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsOpus"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsMPEG4Audio"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsAC3"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsG711"},
|
||||
{Ref: "#/components/schemas/PathTrackCodecPropsLPCM"},
|
||||
}}, doc.Components.Schemas["PathTrackCodecProps"])
|
||||
})
|
||||
|
||||
t.Run("enums", func(t *testing.T) {
|
||||
for _, rt := range []reflect.Type{
|
||||
reflect.TypeOf(defs.APIOKStatus("")),
|
||||
reflect.TypeOf(defs.APIErrorStatus("")),
|
||||
reflect.TypeOf(defs.APIPathSourceType("")),
|
||||
reflect.TypeOf(defs.APIPathReaderType("")),
|
||||
reflect.TypeOf(defs.APIPathTrackCodec("")),
|
||||
reflect.TypeOf(conf.AlwaysAvailableTrackCodec("")),
|
||||
reflect.TypeOf(conf.AuthAction("")),
|
||||
reflect.TypeOf(conf.AuthMethod("")),
|
||||
reflect.TypeOf(conf.Encryption("")),
|
||||
reflect.TypeOf(conf.HLSVariant(0)),
|
||||
reflect.TypeOf(conf.LogDestination(0)),
|
||||
reflect.TypeOf(conf.LogLevel(0)),
|
||||
reflect.TypeOf(conf.RecordFormat("")),
|
||||
reflect.TypeOf(conf.RTSPAuthMethod(0)),
|
||||
reflect.TypeOf(conf.RTSPRangeType("")),
|
||||
reflect.TypeOf(conf.RTSPTransport{}),
|
||||
reflect.TypeOf(defs.APIRTMPConnState("")),
|
||||
reflect.TypeOf(defs.APIRTSPSessionState("")),
|
||||
reflect.TypeOf(defs.APISRTConnState("")),
|
||||
reflect.TypeOf(defs.APIWebRTCSessionState("")),
|
||||
} {
|
||||
t.Run(rt.Name(), func(t *testing.T) {
|
||||
content1 := doc.Components.Schemas[schemaName(rt)]
|
||||
content2, ok := goEnumToApi(rt)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, content2, content1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
apidocs:
|
||||
go run ./internal/apidocsgen
|
||||
+6
-8
@@ -1,8 +1,8 @@
|
||||
define DOCKERFILE_API_DOCS_LINT
|
||||
define DOCKERFILE_APIDOCS_LINT
|
||||
FROM $(NODE_IMAGE)
|
||||
RUN yarn global add @redocly/cli@1.0.0-beta.123
|
||||
endef
|
||||
export DOCKERFILE_API_DOCS_LINT
|
||||
export DOCKERFILE_APIDOCS_LINT
|
||||
|
||||
lint-go:
|
||||
docker run --rm -v "$(shell pwd):/app" -w /app \
|
||||
@@ -15,17 +15,15 @@ lint-go-mod:
|
||||
lint-conf:
|
||||
go test -v -tags enable_linters ./internal/linters/conf
|
||||
|
||||
lint-go2api:
|
||||
go test -v -tags enable_linters ./internal/linters/go2api
|
||||
|
||||
lint-docslinks:
|
||||
go test -v -tags enable_linters ./internal/linters/docslinks
|
||||
|
||||
lint-docsorder:
|
||||
go test -v -tags enable_linters ./internal/linters/docsorder
|
||||
|
||||
lint-api-docs:
|
||||
echo "$$DOCKERFILE_API_DOCS_LINT" | docker build . -f - -t temp
|
||||
lint-apidocs:
|
||||
go run ./internal/apidocsgen --check
|
||||
echo "$$DOCKERFILE_APIDOCS_LINT" | docker build . -f - -t temp
|
||||
docker run --rm -v "$(shell pwd)/api:/s" -w /s temp \
|
||||
sh -c "openapi lint openapi.yaml"
|
||||
|
||||
@@ -34,4 +32,4 @@ lint-other:
|
||||
docker run --rm -v "$(shell pwd)/:/s" -w /s temp \
|
||||
sh -c "prettier --check ."
|
||||
|
||||
lint: lint-go lint-go-mod lint-conf lint-go2api lint-docslinks lint-docsorder lint-api-docs lint-other
|
||||
lint: lint-go lint-go-mod lint-conf lint-docslinks lint-docsorder lint-apidocs lint-other
|
||||
|
||||
Reference in New Issue
Block a user