You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by GitBox <gi...@apache.org> on 2021/01/13 07:10:52 UTC
[GitHub] [pulsar] flowchartsman commented on issue #9200: Swagger is broken.
flowchartsman commented on issue #9200:
URL: https://github.com/apache/pulsar/issues/9200#issuecomment-759253839
Attached is the code (in Go) I've attempted to use to resolve the duplicated operationIds through heuristics. It seems to work rather well, but I am unsure how to resolve the multiple body parameters:
```go
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/fatih/camelcase"
"github.com/go-openapi/analysis"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
)
const pulsarVersion = `2.7.0`
var specFiles = []string{
"swagger.json",
"swaggerfunctions.json",
"swaggersource.json",
"swaggersink.json",
}
type opConflict struct {
op *spec.Operation
route string
method string
}
type conflictList struct {
conflicts []*opConflict
unchanged []*opConflict
}
type opConflicts map[string]*conflictList
func main() {
log.SetFlags(0)
specFile := flag.String("specfile", "", "an individual spec file to download and fix (no merge")
flag.Parse()
var (
specs []*spec.Swagger
err error
)
if *specFile == "" {
specs, err = downloadSpecs(specFiles)
if err != nil {
log.Fatal(err)
}
for _, spec := range specs {
if err := resolveDuplicatOpIDs(spec); err != nil {
log.Fatalf("error resolving duplicate OpIds: %v", err)
}
if err := fixPaths(spec); err != nil {
log.Fatalf("error performing fixPaths on %s", spec.ID)
}
}
if err := ensureNoDuplicateDefinitions(specs); err != nil {
log.Fatalf("error resolving duplicate definitions: %v", err)
}
collisions := analysis.Mixin(specs[0], specs[1:]...)
for _, collision := range collisions {
log.Println(collision)
}
} else {
// otherwise download and fix a single spec file
specs, err = downloadSpecs([]string{*specFile})
if err != nil {
log.Fatal(err)
}
if err := resolveDuplicatOpIDs(specs[0]); err != nil {
log.Fatalf("error resolving duplicate OpIds: %v", err)
}
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(specs[0])
}
// download all specs
func downloadSpecs(specFiles []string) ([]*spec.Swagger, error) {
tmpDir, err := ioutil.TempDir("", "pulsar-spec")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmpDir)
specs := make([]*spec.Swagger, len(specFiles))
for i, specFile := range specFiles {
url := fmt.Sprintf(`https://pulsar.apache.org/swagger/%s/%s`, pulsarVersion, specFile)
log.Printf("downloading %s", url)
fileName, err := getURLToFile(tmpDir, url)
if err != nil {
return nil, fmt.Errorf("error retrieving %s: %w", url, err)
}
doc, err := loads.Spec(fileName)
if err != nil {
return nil, fmt.Errorf("error loading tempfile: %w", err)
}
specs[i] = doc.Spec()
}
return specs, nil
}
// map of meaningful operation names for storage in case we want to base a heuristic off of it (unused)
var opNames = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
func getOpConflicts(s *spec.Swagger) ([]*conflictList, error) {
conflicts := make(opConflicts)
for pathName, path := range s.Paths.Paths {
ops := []*spec.Operation{path.Get, path.Post, path.Put, path.Delete, path.Patch, path.Options, path.Head}
for i, op := range ops {
if op != nil {
if op.ID == "" {
return nil, fmt.Errorf("%s method for route %s has empty opID", opNames[i], pathName)
}
if conflicts[op.ID] == nil {
conflicts[op.ID] = new(conflictList)
}
conflicts[op.ID].conflicts = append(conflicts[op.ID].conflicts, &opConflict{
op: op,
route: pathName,
method: opNames[i],
})
}
}
}
for opName, clist := range conflicts {
if len(clist.conflicts) < 2 {
delete(conflicts, opName)
}
}
cl := make([]*conflictList, 0, len(conflicts))
for _, clist := range conflicts {
cl = append(cl, clist)
}
return cl, nil
}
// apply heuristics to resolve duplicate operation names. usually, one operation name doesn't need changed, so retain one, but if we find more than one we don't know how to rename, return an error
func resolveDuplicatOpIDs(s *spec.Swagger) error {
opConflicts, err := getOpConflicts(s)
if err != nil {
return err
}
for _, clist := range opConflicts {
for _, c := range clist.conflicts {
// attempt to apply resolution rules
routeParts := strings.Split(c.route, "/")
switch {
case strings.HasPrefix(c.route, "/non-persistent"):
c.op.ID += "NP"
case strings.HasPrefix(c.route, "/namespace"):
parts := camelcase.Split(c.op.ID)
parts[0] += "NameSpace"
c.op.ID = strings.Join(parts, "")
case strings.HasSuffix(c.route, "{version}"):
c.op.ID += "Version"
case routeParts[len(routeParts)-2] == "{instanceId}":
c.op.ID += "Instance"
default:
clist.unchanged = append(clist.unchanged, c)
if len(clist.unchanged) > 1 {
var tooMany strings.Builder
tooMany.WriteString(fmt.Sprintf("Found more than one OP conflict I couldn't handle for opID [%s]: ", c.op.ID))
for _, uc := range clist.unchanged {
tooMany.WriteString(
fmt.Sprintf("%s-%s", uc.method, uc.route),
)
}
return fmt.Errorf(tooMany.String())
}
}
}
}
return nil
}
// retrieve the swagger stored at URL to a temporary file. Necessary because loads.Spec requires a file
func getURLToFile(tmpDir string, url string) (filename string, err error) {
file, err := os.Create(filepath.Join(tmpDir, url[strings.LastIndex(url, `/`)+1:]))
if err != nil {
return "", err
}
defer func() {
if err != nil {
os.Remove(file.Name())
}
}()
var resp *http.Response
resp, err = http.Get(url)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("code %d when getting %s", resp.StatusCode, url)
}
if _, err := io.Copy(file, resp.Body); err != nil {
return "", err
}
return file.Name(), file.Close()
}
const adminPrefix = `/admin`
// remove the version suffix from the admin path so all routes can live together in the combined spec
func fixPaths(s *spec.Swagger) error {
s.Info.Version = pulsarVersion
if !strings.HasPrefix(s.BasePath, adminPrefix) {
return fmt.Errorf("basePath does not have required %q prefix: %q", adminPrefix, s.BasePath)
}
newPrefix := s.BasePath[len(adminPrefix):]
s.BasePath = adminPrefix
for pathName, path := range s.Paths.Paths {
newPathName := newPrefix + pathName
if _, ok := s.Paths.Paths[newPathName]; ok {
return fmt.Errorf("new path %q already exists", newPathName)
}
s.Paths.Paths[newPathName] = path
delete(s.Paths.Paths, pathName)
}
return nil
}
func ensureNoDuplicateDefinitions(specs []*spec.Swagger) error {
// create a map of serialized definitions to ensure that definitions of the same name are in fact the same and will preserve refs
serializedDefs := map[string][]byte{}
for _, s := range specs {
for defName, defSchema := range s.Definitions {
b, err := json.Marshal(defSchema)
if err != nil {
return fmt.Errorf("error serializing defininition schema [%s]: %w", defName, err)
}
if schemaBytes, ok := serializedDefs[defName]; ok {
if !bytes.Equal(schemaBytes, b) {
return fmt.Errorf("definitions of %q are different", defName)
}
// remove the duplicate from the spec
delete(s.Definitions, defName)
continue
}
// otherwise store the serialized definition
serializedDefs[defName] = b
}
}
return nil
}
```
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
users@infra.apache.org