initial commit

This commit is contained in:
Семен Каменецкий
2026-09-15 15:31:54 +03:00
parent 702f108aff
commit 09dd1f7217
65 changed files with 4640 additions and 0 deletions
+79
View File
@@ -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
}
+140
View File
@@ -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
}()
}
+19
View File
@@ -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
}
+33
View File
@@ -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)
//}
+45
View File
@@ -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
}
}
+20
View File
@@ -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

+16
View File
@@ -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;
}
+19
View File
@@ -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