Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Mount example doesn't work. what's the right way to have both v1 and v2 on a server? #190

Open
chz8494 opened this issue Mar 1, 2024 · 5 comments

Comments

@chz8494
Copy link

chz8494 commented Mar 1, 2024

Hey guys,

Very good project for swagger 3.x, really appreciated your work.
I'd like to create a server with v1 and v2 server url, so was trying to follow https://github.com/swaggest/rest/blob/master/_examples/mount/main.go example to mount endpoints under api/v1, but has following error with service.Mount("/api/v1", apiV1):

panic: reflect API schema for GET /api/v1/sum: operation already exists: get /api/v1/sum

goroutine 1 [running]:
github.com/swaggest/rest/nethttp.OpenAPIMiddleware.func1({0x1012d9600?, 0x14000373920?})
	/Users/267121010/go/pkg/mod/github.com/swaggest/rest@v0.2.61/nethttp/openapi.go:35 +0x1a8

I also tried to use

r := openapi31.NewReflector()
r.Spec.WithServers(
	openapi31.Server{
		URL: "/api/v1",
	})
s := web.NewService(r)
s.Route("/data", func(r chi.Router) {
	r.Group(func(r chi.Router) {
		r.Use(serviceTokenAuth, serviceTokenDoc, checkSize)
		r.Method(http.MethodPost, "/", nethttp.NewHandler(handler.GenericPost()))
		r.Method(http.MethodPost, "/file-upload", nethttp.NewHandler(handler.FileUploader()))
	})
})
s.Docs(“/docs”, swgui.New)

with this code I can see server url options in the swagger gui, but the actual endpoint logic is not correctly mapped to server selection. I'd expect to be able to call endpoint <url>/api/v1/data, but the server is actually only listening on <url>/data, the swagger GUI call test does show correct curl example <url>/api/v1/data though.

@vearutop
Copy link
Member

vearutop commented Mar 1, 2024

Thank you for raising this, this example is now fixed in latest master.

@chz8494
Copy link
Author

chz8494 commented Mar 2, 2024

Thank you for the quick update. I've tried it and no more errors.
But it still cannot achieve what I want. Your example

apiV1.Post("/sum", sum())
s.Mount("/api/v1", apiV1)

seems providing same function as using

s.Route("/api/v1", func(r chi.Router) {
	r.Group(func(r chi.Router) {
		r.Use(sessMW, sessDoc)

		r.Method(http.MethodGet, "/sum", nethttp.NewHandler(sum()))
	})
})

it just adds pattern /api/v1 in front of whatever defined in apiV1.

what I want to do is to have server version options selectable and make routes mapping correctly in swagger GUI.
with your new code, if I add this line in the beginning:

r := openapi3.NewReflector()
r.Spec.WithServers(
  openapi31.Server{
  	URL: "/api/v1",
  },
  openapi31.Server{
  	URL: "/api/v2",
  }
)
s := web.NewService(r)

and if choose /api/v1, the swagger GUI curl example will call to endpoint localhost/api/v1/api/v1/ instead of localhost/api/v1

@vearutop
Copy link
Member

vearutop commented Mar 3, 2024

Hi, I think you need both individual spec configuration for each versioned API and Swagger UI setup that allows selection from multiple API specs.

Please check an example https://github.com/swaggest/rest/blob/master/_examples/multi-api/main.go

// Package main implements an example where two versioned API revisions are mounted into root web service
// and are available through a service selector in Swagger UI.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/go-chi/chi/v5/middleware"
	"github.com/swaggest/openapi-go"
	"github.com/swaggest/openapi-go/openapi3"
	"github.com/swaggest/rest/nethttp"
	"github.com/swaggest/rest/web"
	swg "github.com/swaggest/swgui"
	swgui "github.com/swaggest/swgui/v5emb"
	"github.com/swaggest/usecase"
)

func main() {
	fmt.Println("Swagger UI at http://localhost:8010/api/docs.")
	if err := http.ListenAndServe("localhost:8010", service()); err != nil {
		log.Fatal(err)
	}
}

func service() *web.Service {
	// Creating root service, to host versioned APIs.
	s := web.NewService(openapi3.NewReflector())
	s.OpenAPISchema().SetTitle("Security and Mount Example")

	// Each versioned API is exposed with its own OpenAPI schema.
	v1r := openapi3.NewReflector()
	v1r.SpecEns().WithServers(openapi3.Server{URL: "/api/v1/"}).WithInfo(openapi3.Info{Title: "My API of version 1"})
	apiV1 := web.NewService(v1r)

	v2r := openapi3.NewReflector()
	v2r.SpecEns().WithServers(openapi3.Server{URL: "/api/v2/"})
	apiV2 := web.NewService(v2r)

	// Versioned APIs may or may not have their own middlewares and wraps.
	apiV1.Wrap(
		middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"}),
		nethttp.HTTPBasicSecurityMiddleware(s.OpenAPICollector, "Admin", "Admin access"),
		nethttp.OpenAPIAnnotationsMiddleware(s.OpenAPICollector, func(oc openapi.OperationContext) error {
			oc.SetTags(append(oc.Tags(), "V1")...)
			return nil
		}),
	)
	apiV1.Post("/sum", sum())
	apiV1.Post("/mul", mul())
	// Once all API use cases are added, schema can be served too.
	apiV1.Method(http.MethodGet, "/openapi.json", specHandler(apiV1.OpenAPICollector.SpecSchema()))

	apiV2.Post("/summarization", sum())
	apiV2.Post("/multiplication", mul())
	apiV2.Method(http.MethodGet, "/openapi.json", specHandler(apiV2.OpenAPICollector.SpecSchema()))

	// Prepared versioned API services are mounted with their base URLs into root service.
	s.Mount("/api/v1", apiV1)
	s.Mount("/api/v2", apiV2)

	// Root docs needs a bit of hackery to expose versioned APIs as separate services.
	s.Docs("/api/docs", swgui.NewWithConfig(swg.Config{
		ShowTopBar: true,
		SettingsUI: map[string]string{
			// When "urls" are configured, Swagger UI ignores "url" and switches to multi API mode.
			"urls": `[
	{"url": "/api/v1/openapi.json", "name": "APIv1"}, 
	{"url": "/api/v2/openapi.json", "name": "APIv2"}
]`,
			`"urls.primaryName"`: `"APIv2"`, // Using APIv2 as default.
		},
	}))

	// Blanket handler, for example to serve static content.
	s.Mount("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("blanket handler got a request: " + r.URL.String()))
	}))

	return s
}

func specHandler(s openapi.SpecSchema) http.Handler {
	j, err := json.Marshal(s)
	if err != nil {
		panic(err)
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(j)
	})
}

func mul() usecase.Interactor {
	return usecase.NewInteractor(func(ctx context.Context, input []int, output *int) error {
		*output = 1

		for _, v := range input {
			*output *= v
		}

		return nil
	})
}

func sum() usecase.Interactor {
	return usecase.NewInteractor(func(ctx context.Context, input []int, output *int) error {
		for _, v := range input {
			*output += v
		}

		return nil
	})
}

@chz8494
Copy link
Author

chz8494 commented Mar 3, 2024

@vearutop Thank you for the example code update, it does solved most of the problem, I can now use banner to switch json profile and gui reflects endpoint pattern correctly.
but this solution seems not work with grouped route auth middleware, e.g:

s.Route("/data", func(r chi.Router) {
	r.Group(func(r chi.Router) {
		r.Use(sessMW, sessDoc)

		r.Method(http.MethodGet, "/sum", nethttp.NewHandler(sum()))
	})
})

the api is functioning correctly and accepting auth, but GUI doesn't have auth options/icon displayed, openapi.json doesn't have security related content either.
I also tried with s.With(sessMW, sessDoc), not work either.

and the example code

apiV1.Wrap(
		middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"}),
		nethttp.HTTPBasicSecurityMiddleware(s.OpenAPICollector, "Admin", "Admin access"),
		nethttp.OpenAPIAnnotationsMiddleware(s.OpenAPICollector, func(oc openapi.OperationContext) error {
			oc.SetTags(append(oc.Tags(), "V1")...)
			return nil
		}),
	)

causes the whole apiV1 page becomes BasicAuth

@chz8494
Copy link
Author

chz8494 commented Mar 4, 2024

Ok, I kind of got it work by

apiV1.Route("/data", func(r chi.Router) {
	r.Group(func(r chi.Router) {
		r.Use(serviceTokenAuth, serviceTokenDoc, checkSize)
		r.Method(http.MethodGet, "/sum", nethttp.NewHandler(sum()))
	})
})
// Swagger GUI to have authorization schema and input
apiV1.OpenAPISchema().SetAPIKeySecurity("apiKey", "Authorization", oapi.InHeader, "API Key.")
// to add authorization schema under route group so that Swagger GUI example curl can call
for _, pi := range v1r.Spec.Paths.MapOfPathItemValues {
  pi.Post.Security = []map[string][]string{
    {
      "apiKey": []string{},
    },
  }
}
apiV1.Method(http.MethodGet, "/docs/openapi.json", specHandler(apiV1.OpenAPICollector.SpecSchema()))

but it's not ideal, I'd prefer apiV1.OpenAPICollector.SpecSchema() to pick up correct security values by itself.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants