Skip to content
New issue

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

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

Already on GitHub? Sign in 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.

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

No branches or pull requests

2 participants