initial commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func New(options ...Option) *App {
|
||||
a := &App{
|
||||
ctx: context.Background(),
|
||||
router: chi.NewRouter(),
|
||||
grpcServer: grpc.NewServer(),
|
||||
httpServer: new(http.Server),
|
||||
}
|
||||
for _, option := range options {
|
||||
option(a)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
router *chi.Mux
|
||||
grpcServer *grpc.Server
|
||||
httpServer *http.Server
|
||||
mux *runtime.ServeMux
|
||||
postgres *sqlx.DB
|
||||
mongo *mongo.Client
|
||||
closers []closeFunc
|
||||
serviceRegistry []registerServiceFunc
|
||||
gatewayRegistry []registerGatewayFunc
|
||||
config struct {
|
||||
grpcAddr string
|
||||
httpAddr string
|
||||
postgresURI string
|
||||
mongoURI string
|
||||
withGrpc bool
|
||||
withSwagger bool
|
||||
withStatic bool
|
||||
forwardResponse forwardResponseFunc
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
defaultHttpAddr = ":8000"
|
||||
defaultGrpcAddr = ":9000"
|
||||
)
|
||||
|
||||
type forwardResponseFunc func(context.Context, http.ResponseWriter, proto.Message) error
|
||||
type closeFunc func() error
|
||||
type registerServiceFunc func(gs *grpc.Server)
|
||||
type registerGatewayFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error
|
||||
|
||||
func (a *App) RegisterServices(fn registerServiceFunc) {
|
||||
a.serviceRegistry = append(a.serviceRegistry, fn)
|
||||
}
|
||||
|
||||
func (a *App) RegisterGateways(fns ...registerGatewayFunc) {
|
||||
a.gatewayRegistry = append(a.gatewayRegistry, fns...)
|
||||
}
|
||||
|
||||
func (a *App) GRPC() *grpc.Server {
|
||||
return a.grpcServer
|
||||
}
|
||||
|
||||
func (a *App) HTTP() *http.Server {
|
||||
return a.httpServer
|
||||
}
|
||||
|
||||
func (a *App) Router() *chi.Mux {
|
||||
return a.router
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"backbone/static"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func (a *App) ListenAndServe() error {
|
||||
a.httpServer.Handler = a.router
|
||||
a.httpServer.Addr = a.getHTTPAddr()
|
||||
|
||||
muxOptions := []runtime.ServeMuxOption{
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
EmitUnpopulated: true,
|
||||
EmitDefaultValues: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
if a.config.forwardResponse != nil {
|
||||
muxOptions = append(muxOptions,
|
||||
runtime.WithForwardResponseOption(a.config.forwardResponse))
|
||||
}
|
||||
a.mux = runtime.NewServeMux(muxOptions...)
|
||||
|
||||
a.router.Handle("/api/*", a.mux)
|
||||
if a.config.withSwagger {
|
||||
a.router.Handle("/api/openapi.json", openApiServer)
|
||||
a.router.Handle("/swagger*", swaggerUIHandler)
|
||||
}
|
||||
if a.config.withStatic {
|
||||
a.router.NotFound(static.Handler)
|
||||
}
|
||||
|
||||
// register services
|
||||
for _, s := range a.serviceRegistry {
|
||||
s(a.grpcServer)
|
||||
}
|
||||
|
||||
// register gateways
|
||||
conn, err := grpc.NewClient(a.getGRPCAddr(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init local grpc client: %w", err)
|
||||
}
|
||||
for _, g := range a.gatewayRegistry {
|
||||
if err = g(a.ctx, a.mux, conn); err != nil {
|
||||
return fmt.Errorf("failed to init gateway: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
errChan := make(chan error)
|
||||
go a.startGRPC(errChan)
|
||||
go a.startHTTP(errChan)
|
||||
go a.wait(errChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-a.ctx.Done():
|
||||
return a.ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) startHTTP(errChan chan error) {
|
||||
if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errChan <- fmt.Errorf("failed to start http server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) startGRPC(errChan chan error) {
|
||||
listener, err := net.Listen("tcp", a.getGRPCAddr())
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to start grpc server: %w", err)
|
||||
}
|
||||
if err = a.grpcServer.Serve(listener); err != nil {
|
||||
errChan <- fmt.Errorf("failed to serve grpc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) getHTTPAddr() string {
|
||||
if a.config.httpAddr != "" {
|
||||
return a.config.httpAddr
|
||||
}
|
||||
if v := os.Getenv("HTTP_ADDR"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultHttpAddr
|
||||
}
|
||||
|
||||
func (a *App) getGRPCAddr() string {
|
||||
if a.config.grpcAddr != "" {
|
||||
return a.config.grpcAddr
|
||||
}
|
||||
if v := os.Getenv("GRPC_ADDR"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultGrpcAddr
|
||||
}
|
||||
|
||||
func (a *App) wait(errChan chan error) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigs
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(a.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
a.grpcServer.GracefulStop()
|
||||
|
||||
for _, closer := range a.closers {
|
||||
if err := closer(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func (a *App) initMongo() {
|
||||
if a.config.mongoURI == "" {
|
||||
return
|
||||
}
|
||||
client, err := mongo.Connect(options.Client().ApplyURI(a.config.mongoURI))
|
||||
if err != nil {
|
||||
log.Fatalln("failed to connect mongo:", err)
|
||||
}
|
||||
a.mongo = client
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"backbone/api"
|
||||
"backbone/internal/pkg/version"
|
||||
"bytes"
|
||||
"embed"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed swagger
|
||||
var swaggerFS embed.FS
|
||||
|
||||
var swaggerUIHandler = func() http.Handler {
|
||||
//sub,err := fs.Sub(swaggerFS, "swagger")
|
||||
return http.FileServerFS(swaggerFS)
|
||||
}()
|
||||
|
||||
var openApiServer = func() http.Handler {
|
||||
content := bytes.ReplaceAll(api.OpenAPI, []byte("%version%"), []byte(version.Version))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, "openapi.json", time.Now(), bytes.NewReader(content))
|
||||
})
|
||||
}()
|
||||
|
||||
//
|
||||
//func (a *App) serveOpenAPIFile(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// //w.Header().Set("Content-Type", "application/json")
|
||||
// //w.WriteHeader(http.StatusOK)
|
||||
// //_, _ = w.Write(api.OpenAPI)
|
||||
//}
|
||||
@@ -0,0 +1,45 @@
|
||||
package app
|
||||
|
||||
type Option func(*App)
|
||||
|
||||
func WithHTTPAddr(addr string) Option {
|
||||
return func(a *App) {
|
||||
a.config.httpAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
func WithGRPCAddr(addr string) Option {
|
||||
return func(a *App) {
|
||||
a.config.grpcAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
func WithPostgresDatabase(uri string) Option {
|
||||
return func(a *App) {
|
||||
a.config.postgresURI = uri
|
||||
}
|
||||
}
|
||||
|
||||
func WithMongoDatabase(uri string) Option {
|
||||
return func(a *App) {
|
||||
a.config.mongoURI = uri
|
||||
}
|
||||
}
|
||||
|
||||
func WithSwaggerUI() Option {
|
||||
return func(a *App) {
|
||||
a.config.withSwagger = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithStatic() Option {
|
||||
return func(a *App) {
|
||||
a.config.withStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithForwardResponse(fn forwardResponseFunc) Option {
|
||||
return func(a *App) {
|
||||
a.config.forwardResponse = fn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func (a *App) initPostgres() {
|
||||
if a.config.postgresURI == "" {
|
||||
return
|
||||
}
|
||||
pool, err := pgxpool.New(a.ctx, a.config.postgresURI)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to connect to postgres:", err)
|
||||
}
|
||||
a.postgres = sqlx.NewDb(stdlib.OpenDBFromPool(pool), "postgres")
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,16 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="swagger-ui.css" />
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script src="swagger-initializer.js" charset="UTF-8"> </script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
window.onload = function () {
|
||||
//<editor-fold desc="Changeable Configuration Block">
|
||||
|
||||
const HideInfoUrlPartsPlugin = () => {
|
||||
return {
|
||||
wrapComponents: {
|
||||
InfoUrl: () => () => null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/api/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl,
|
||||
HideInfoUrlPartsPlugin,
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
|
||||
//</editor-fold>
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user