diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3352833 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +*/ @ZigBalthazar +*/ @kehiy diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5e0aa9a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Description + + +## Related Issue + + +- Fixes #(issue number) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..cafe2c5 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,25 @@ +name: Lint and format check + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.5' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..a68fe88 --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,17 @@ +name: Semantic PR + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..70839fd --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,139 @@ +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - decorder + - dogsled + - dupword + - durationcheck + - errchkjson + - errname + - errorlint + - exhaustive + - copyloopvar + - forbidigo + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gocognit + - gocritic + - gocyclo + - godot + - gofmt + - gofumpt + - goheader + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - grouper + - importas + - lll + - loggercheck + - maintidx + - makezero + - mirror + - misspell + # - musttag + - nakedret + - nilerr + - nilnil + - nlreturn + - nestif + - noctx + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - sqlclosecheck + - stylecheck + - tagalign + - tagliatelle + - tenv + - testableexamples + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - wastedassign + - whitespace + - zerologlint + - nonamedreturns + +linters-settings: + gosimple: + checks: ["all"] + + govet: + enable-all: true + disable: + - fieldalignment + - contextcheck + + predeclared: + # Comma-separated list of predeclared identifiers to not report on. + # Default: "" + ignore: "len" + # Include method names and field names (i.e., qualified names) in checks. + # Default: false + q: true + + tagliatelle: + # Check the struct tag name case. + case: + use-field-name: false + rules: + json: snake + yaml: snake + + nonamedreturns: + # Report named error if it is assigned inside defer. + # Default: false + report-error-in-defer: false + + gocritic: + disabled-checks: + - ifElseChain + - unnamedResult + enabled-tags: + - diagnostic + - style + - performance + + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 6 + +issues: + exclude-rules: + - path: _test.go + linters: + - maintidx + - nestif + - gocognit + - forbidigo + - lll + + - path: _easyjson.go + linters: + - nestif + + - linters: + - govet + text: 'shadow: declaration of "err" shadows' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ff48cd --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Go Echo Boilerplate + +This is a golang boilerplate for projects using echo and mongo db stack on Dezh Technologies. + +## Stack + +* Mongo DB +* Redis +* HTTP/echo +* gRPC/google.grpc + +## TODOs + +- [ ] Implementing auth middleware. + +## Contributions + +All kind of contribution are welcome here. + +## License + +This repo is [unlicensed](./LICENSE). diff --git a/cmd/commands/help.go b/cmd/commands/help.go new file mode 100644 index 0000000..e95d0f0 --- /dev/null +++ b/cmd/commands/help.go @@ -0,0 +1,21 @@ +package commands + +import "fmt" + +const help = `Available commands: +Commands: + run starts the application with the specified configuration file. + help displays this help information. + version displays the version of software. + +Usage: + [options] + +Examples: + run config.yaml run the application using config.yaml. + help display this help message. +` + +func HandleHelp(_ []string) { + fmt.Print(help) //nolint +} diff --git a/cmd/commands/run.go b/cmd/commands/run.go new file mode 100644 index 0000000..c2665e2 --- /dev/null +++ b/cmd/commands/run.go @@ -0,0 +1,49 @@ +package commands + +import ( + "errors" + "os" + "os/signal" + "syscall" + + "github.com/dezh-tech/geb/cmd/daemon" + "github.com/dezh-tech/geb/config" + "github.com/dezh-tech/geb/pkg/logger" +) + +func HandleRun(args []string) { + if len(args) < 3 { + ExitOnError(errors.New("at least 1 arguments expected\nuse help command for more information")) + } + + cfg, err := config.Load(args[2]) + if err != nil { + ExitOnError(err) + } + + logger.InitGlobalLogger(&cfg.Logger) + + d, err := daemon.New(cfg) + if err != nil { + ExitOnError(err) + } + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + errCh := d.Start() + + select { + case sig := <-sigChan: + logger.Info("Initiating graceful shutdown", "signal", sig.String()) + if err := d.Stop(); err != nil { + ExitOnError(err) + } + + case err := <-errCh: + logger.Error("Initiating shutdown due to the error", "err", err) + if err := d.Stop(); err != nil { + ExitOnError(err) + } + } +} diff --git a/cmd/commands/utils.go b/cmd/commands/utils.go new file mode 100644 index 0000000..b974b1d --- /dev/null +++ b/cmd/commands/utils.go @@ -0,0 +1,11 @@ +package commands + +import ( + "log" + "os" +) + +func ExitOnError(err error) { + log.Printf("immortal error: %s\n", err.Error()) //nolint + os.Exit(1) +} diff --git a/cmd/daemon/daemon.go b/cmd/daemon/daemon.go new file mode 100644 index 0000000..02b1a3a --- /dev/null +++ b/cmd/daemon/daemon.go @@ -0,0 +1,95 @@ +package daemon + +import ( + "time" + + "github.com/dezh-tech/geb/config" + "github.com/dezh-tech/geb/delivery/grpc" + "github.com/dezh-tech/geb/delivery/http" + "github.com/dezh-tech/geb/infrastructure/database" + grpcclient "github.com/dezh-tech/geb/infrastructure/grpc_client" + "github.com/dezh-tech/geb/infrastructure/redis" + "github.com/dezh-tech/geb/pkg/logger" + userrepo "github.com/dezh-tech/geb/repository/user" + usersrv "github.com/dezh-tech/geb/service/user" +) + +type Daemon struct { + config config.Config + httpServer http.Server + grpcServer *grpc.Server + database *database.Database + redis *redis.Redis +} + +func New(cfg *config.Config) (*Daemon, error) { + db, err := database.Connect(cfg.Database) + if err != nil { + return nil, err + } + + r, err := redis.New(cfg.RedisConf) + if err != nil { + return nil, err + } + + _, err = grpcclient.New(cfg.GRPCClient.Endpoint) + if err != nil { + return nil, err + } + + userRepo := userrepo.New(db) + + hs := http.New(cfg.HTTPServer, usersrv.New(userRepo)) + gs := grpc.New(&cfg.GRPCServer, r, db, time.Now()) + + return &Daemon{ + config: *cfg, + httpServer: hs, + database: db, + redis: r, + grpcServer: gs, + }, nil +} + +func (d *Daemon) Start() chan error { + errCh := make(chan error, 2) + + logger.Info("starting daemon.") + + go func() { + if err := d.httpServer.Start(); err != nil { + errCh <- err + } + }() + + go func() { + if err := d.grpcServer.Start(); err != nil { + errCh <- err + } + }() + + logger.Info("daemon started successfully.") + + return errCh +} + +func (d *Daemon) Stop() error { + logger.Info("stopping the server.") + + if err := d.httpServer.Stop(); err != nil { + return err + } + + if err := d.grpcServer.Stop(); err != nil { + return err + } + + if err := d.database.Stop(); err != nil { + return err + } + + logger.Info("daemon stopped successfully.") + + return nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a94fb50 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "errors" + "fmt" + "os" + + goginboilerplate "github.com/dezh-tech/geb" + "github.com/dezh-tech/geb/cmd/commands" +) + +func main() { + if len(os.Args) < 2 { + commands.HandleHelp(os.Args) + commands.ExitOnError(errors.New("at least 1 arguments expected")) + } + + switch os.Args[1] { + case "run": + commands.HandleRun(os.Args) + + case "help": + commands.HandleHelp(os.Args) + os.Exit(0) + + case "version": + fmt.Println(goginboilerplate.StringVersion()) //nolint + os.Exit(0) + + default: + commands.HandleHelp(os.Args) + } +} diff --git a/config/.development.env b/config/.development.env new file mode 100644 index 0000000..586bc44 --- /dev/null +++ b/config/.development.env @@ -0,0 +1,10 @@ +# we read secret configs from environment variables. here is the full list of them. +# make sure you create a `.env` file beside your build directory and define your dev environment there. +# you have to set the `environment` field of yaml config to `"dev"`, otherwise these are going to be loaded form os env. + +# your MongoDB URI for development. you can use dev docker compose to run it. +MONGO_URI="mongodb://username:password@host1:27017,host2:27017,host3:27017/mydatabase?replicaSet=myReplicaSet&authSource=admin&readPreference=primary&ssl=true&retryWrites=true&w=majority" + + +# your Redis URI for development. you can use dev docker compose to run it. +REDIS_URI="redis://localhost:6379" diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4165c0d --- /dev/null +++ b/config/config.go @@ -0,0 +1,62 @@ +package config + +import ( + "os" + + "github.com/dezh-tech/geb/delivery/grpc" + "github.com/dezh-tech/geb/delivery/http" + "github.com/dezh-tech/geb/infrastructure/database" + grpcclient "github.com/dezh-tech/geb/infrastructure/grpc_client" + "github.com/dezh-tech/geb/infrastructure/redis" + "github.com/dezh-tech/geb/pkg/logger" + "github.com/joho/godotenv" + "gopkg.in/yaml.v3" +) + +// Config represents the configs used by relay and other concepts on system. +type Config struct { + Environment string `yaml:"environment"` + GRPCClient grpcclient.Config `yaml:"grpc_client"` + Database database.Config `yaml:"database"` + RedisConf redis.Config `yaml:"redis"` + GRPCServer grpc.Config `yaml:"grpc_server"` + HTTPServer http.Config `yaml:"http_server"` + Logger logger.Config `yaml:"logger"` +} + +// Load loads config from file and env. +func Load(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + config := &Config{} + + decoder := yaml.NewDecoder(file) + + if err := decoder.Decode(config); err != nil { + return nil, err + } + + if config.Environment != "prod" { + if err := godotenv.Load(); err != nil { + return nil, err + } + } + + config.Database.URI = os.Getenv("MONGO_URI") + config.RedisConf.URI = os.Getenv("REDIS_URI") + + if err := config.basicCheck(); err != nil { + return nil, err + } + + return config, nil +} + +// basicCheck validates the basic stuff in config. +func (c *Config) basicCheck() error { + return nil +} diff --git a/config/config_example.yml b/config/config_example.yml new file mode 100644 index 0000000..1602e90 --- /dev/null +++ b/config/config_example.yml @@ -0,0 +1,29 @@ +environment: "prod" + +grpc_server: + bind: "0.0.0.0" + port: 50050 + +http_server: + bind: "0.0.0.0" + port: 8080 + +grpc_client: + endpoint: "client:8888" + +database: + db_name: immortal + query_timeout_in_ms: 3000 + connection_timeout_in_ms: 5000 + +redis: + query_timeout_in_ms: 3000 + connection_timeout_in_ms: 5000 + +logger: + level: "info" + filename: "project.log" + max_size: 10 # in mb. + max_backups: 10 + compress: true + targets: [file, console] diff --git a/delivery/grpc/buf/buf.gen.yaml b/delivery/grpc/buf/buf.gen.yaml new file mode 100644 index 0000000..dcf1c1c --- /dev/null +++ b/delivery/grpc/buf/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v1 +plugins: + - name: go + out: ../gen + opt: paths=source_relative + - name: go-grpc + out: ../gen + opt: paths=source_relative,require_unimplemented_servers=false diff --git a/delivery/grpc/config.go b/delivery/grpc/config.go new file mode 100644 index 0000000..b6e4e10 --- /dev/null +++ b/delivery/grpc/config.go @@ -0,0 +1,6 @@ +package grpc + +type Config struct { + Bind string `yaml:"bind"` + Port uint16 `yaml:"port"` +} diff --git a/delivery/grpc/gen/health.pb.go b/delivery/grpc/gen/health.pb.go new file mode 100644 index 0000000..406d0c5 --- /dev/null +++ b/delivery/grpc/gen/health.pb.go @@ -0,0 +1,365 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc (unknown) +// source: health.proto + +package grpc_client + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Status int32 + +const ( + Status_UNKNOWN Status = 0 + Status_CONNECTED Status = 1 + Status_DISCONNECTED Status = 2 +) + +// Enum value maps for Status. +var ( + Status_name = map[int32]string{ + 0: "UNKNOWN", + 1: "CONNECTED", + 2: "DISCONNECTED", + } + Status_value = map[string]int32{ + "UNKNOWN": 0, + "CONNECTED": 1, + "DISCONNECTED": 2, + } +) + +func (x Status) Enum() *Status { + p := new(Status) + *p = x + return p +} + +func (x Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Status) Descriptor() protoreflect.EnumDescriptor { + return file_health_proto_enumTypes[0].Descriptor() +} + +func (Status) Type() protoreflect.EnumType { + return &file_health_proto_enumTypes[0] +} + +func (x Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Status.Descriptor instead. +func (Status) EnumDescriptor() ([]byte, []int) { + return file_health_proto_rawDescGZIP(), []int{0} +} + +type Service struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=service.v1.Status" json:"status,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *Service) Reset() { + *x = Service{} + if protoimpl.UnsafeEnabled { + mi := &file_health_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Service) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Service) ProtoMessage() {} + +func (x *Service) ProtoReflect() protoreflect.Message { + mi := &file_health_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Service.ProtoReflect.Descriptor instead. +func (*Service) Descriptor() ([]byte, []int) { + return file_health_proto_rawDescGZIP(), []int{0} +} + +func (x *Service) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Service) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_UNKNOWN +} + +func (x *Service) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type StatusRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StatusRequest) Reset() { + *x = StatusRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_health_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusRequest) ProtoMessage() {} + +func (x *StatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_health_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusRequest.ProtoReflect.Descriptor instead. +func (*StatusRequest) Descriptor() ([]byte, []int) { + return file_health_proto_rawDescGZIP(), []int{1} +} + +type StatusResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Services []*Service `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` + Uptime int64 `protobuf:"varint,2,opt,name=uptime,proto3" json:"uptime,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *StatusResponse) Reset() { + *x = StatusResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_health_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusResponse) ProtoMessage() {} + +func (x *StatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_health_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead. +func (*StatusResponse) Descriptor() ([]byte, []int) { + return file_health_proto_rawDescGZIP(), []int{2} +} + +func (x *StatusResponse) GetServices() []*Service { + if x != nil { + return x.Services + } + return nil +} + +func (x *StatusResponse) GetUptime() int64 { + if x != nil { + return x.Uptime + } + return 0 +} + +func (x *StatusResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +var File_health_proto protoreflect.FileDescriptor + +var file_health_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x63, 0x0a, 0x07, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0x73, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2a, 0x36, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, + 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x44, + 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x32, 0x50, 0x0a, + 0x0d, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3f, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, + 0x7a, 0x68, 0x2d, 0x74, 0x65, 0x63, 0x68, 0x2f, 0x67, 0x6f, 0x2d, 0x67, 0x69, 0x6e, 0x2d, 0x62, + 0x6f, 0x69, 0x6c, 0x65, 0x72, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_health_proto_rawDescOnce sync.Once + file_health_proto_rawDescData = file_health_proto_rawDesc +) + +func file_health_proto_rawDescGZIP() []byte { + file_health_proto_rawDescOnce.Do(func() { + file_health_proto_rawDescData = protoimpl.X.CompressGZIP(file_health_proto_rawDescData) + }) + return file_health_proto_rawDescData +} + +var file_health_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_health_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_health_proto_goTypes = []interface{}{ + (Status)(0), // 0: service.v1.Status + (*Service)(nil), // 1: service.v1.Service + (*StatusRequest)(nil), // 2: service.v1.StatusRequest + (*StatusResponse)(nil), // 3: service.v1.StatusResponse +} +var file_health_proto_depIdxs = []int32{ + 0, // 0: service.v1.Service.status:type_name -> service.v1.Status + 1, // 1: service.v1.StatusResponse.services:type_name -> service.v1.Service + 2, // 2: service.v1.HealthService.Status:input_type -> service.v1.StatusRequest + 3, // 3: service.v1.HealthService.Status:output_type -> service.v1.StatusResponse + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_health_proto_init() } +func file_health_proto_init() { + if File_health_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_health_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Service); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_health_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_health_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_health_proto_rawDesc, + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_health_proto_goTypes, + DependencyIndexes: file_health_proto_depIdxs, + EnumInfos: file_health_proto_enumTypes, + MessageInfos: file_health_proto_msgTypes, + }.Build() + File_health_proto = out.File + file_health_proto_rawDesc = nil + file_health_proto_goTypes = nil + file_health_proto_depIdxs = nil +} diff --git a/delivery/grpc/gen/health_grpc.pb.go b/delivery/grpc/gen/health_grpc.pb.go new file mode 100644 index 0000000..fff7749 --- /dev/null +++ b/delivery/grpc/gen/health_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: health.proto + +package grpc_client + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + HealthService_Status_FullMethodName = "/service.v1.HealthService/Status" +) + +// HealthServiceClient is the client API for HealthService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HealthServiceClient interface { + Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) +} + +type healthServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHealthServiceClient(cc grpc.ClientConnInterface) HealthServiceClient { + return &healthServiceClient{cc} +} + +func (c *healthServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) { + out := new(StatusResponse) + err := c.cc.Invoke(ctx, HealthService_Status_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HealthServiceServer is the server API for HealthService service. +// All implementations should embed UnimplementedHealthServiceServer +// for forward compatibility +type HealthServiceServer interface { + Status(context.Context, *StatusRequest) (*StatusResponse, error) +} + +// UnimplementedHealthServiceServer should be embedded to have forward compatible implementations. +type UnimplementedHealthServiceServer struct { +} + +func (UnimplementedHealthServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Status not implemented") +} + +// UnsafeHealthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HealthServiceServer will +// result in compilation errors. +type UnsafeHealthServiceServer interface { + mustEmbedUnimplementedHealthServiceServer() +} + +func RegisterHealthServiceServer(s grpc.ServiceRegistrar, srv HealthServiceServer) { + s.RegisterService(&HealthService_ServiceDesc, srv) +} + +func _HealthService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HealthServiceServer).Status(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HealthService_Status_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HealthServiceServer).Status(ctx, req.(*StatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HealthService_ServiceDesc is the grpc.ServiceDesc for HealthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HealthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "service.v1.HealthService", + HandlerType: (*HealthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Status", + Handler: _HealthService_Status_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "health.proto", +} diff --git a/delivery/grpc/health.go b/delivery/grpc/health.go new file mode 100644 index 0000000..20e171e --- /dev/null +++ b/delivery/grpc/health.go @@ -0,0 +1,61 @@ +package grpc + +import ( + "context" + "time" + + goginboilerplate "github.com/dezh-tech/geb" + pb "github.com/dezh-tech/geb/delivery/grpc/gen" +) + +type healthServer struct { + *Server +} + +func newHealthServer(server *Server) *healthServer { + return &healthServer{ + Server: server, + } +} + +func (s healthServer) Status(ctx context.Context, _ *pb.StatusRequest) (*pb.StatusResponse, error) { + services := make([]*pb.Service, 0) + + redisStatus := pb.Status_CONNECTED + redisMessage := "" + + if err := s.Redis.Client.Ping(ctx).Err(); err != nil { + redisStatus = pb.Status_DISCONNECTED + redisMessage = err.Error() + } + + redis := pb.Service{ + Name: "redis", + Status: redisStatus, + Message: redisMessage, + } + + services = append(services, &redis) + + mongoStatus := pb.Status_CONNECTED + mongoMessage := "" + + if err := s.DB.Client.Ping(ctx, nil); err != nil { + mongoStatus = pb.Status_DISCONNECTED + mongoMessage = err.Error() + } + + mongo := pb.Service{ + Name: "mongo", + Status: mongoStatus, + Message: mongoMessage, + } + + services = append(services, &mongo) + + return &pb.StatusResponse{ + Uptime: int64(time.Since(s.StartTime).Seconds()), + Version: goginboilerplate.StringVersion(), + Services: services, + }, nil +} diff --git a/delivery/grpc/proto/health.proto b/delivery/grpc/proto/health.proto new file mode 100644 index 0000000..d0c3deb --- /dev/null +++ b/delivery/grpc/proto/health.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package service.v1; + +option go_package = "github.com/dezh-tech/go-gin-boilerplate/infrastructure/grpc_client"; + +service HealthService { + rpc Status (StatusRequest) returns (StatusResponse); +} + +message Service { + string name = 1; + Status status = 2; + string message = 3; +} + +message StatusRequest {} + +message StatusResponse { + repeated Service services = 1; + int64 uptime = 2; + string version = 3; +} + +enum Status { + UNKNOWN = 0; + CONNECTED = 1; + DISCONNECTED = 2; +} diff --git a/delivery/grpc/server.go b/delivery/grpc/server.go new file mode 100644 index 0000000..7176808 --- /dev/null +++ b/delivery/grpc/server.go @@ -0,0 +1,67 @@ +package grpc + +import ( + "context" + "net" + "strconv" + "time" + + pb "github.com/dezh-tech/geb/delivery/grpc/gen" + "github.com/dezh-tech/geb/infrastructure/database" + "github.com/dezh-tech/geb/infrastructure/redis" + "google.golang.org/grpc" +) + +type Server struct { + ctx context.Context + cancel context.CancelFunc + config *Config + listener net.Listener + grpc *grpc.Server + StartTime time.Time + DB *database.Database + Redis *redis.Redis +} + +func New(conf *Config, r *redis.Redis, db *database.Database, st time.Time) *Server { + ctx, cancel := context.WithCancel(context.Background()) + + return &Server{ + ctx: ctx, + cancel: cancel, + config: conf, + StartTime: st, + Redis: r, + DB: db, + } +} + +func (s *Server) Start() error { + listener, err := net.Listen("tcp", net.JoinHostPort(s.config.Bind, //nolint + strconv.Itoa(int(s.config.Port)))) + if err != nil { + return err + } + + grpcServer := grpc.NewServer(grpc.ChainUnaryInterceptor()) + + healthServer := newHealthServer(s) + + pb.RegisterHealthServiceServer(grpcServer, healthServer) + + s.listener = listener + s.grpc = grpcServer + + return s.grpc.Serve(listener) +} + +func (s *Server) Stop() error { + s.cancel() + + s.grpc.Stop() + if err := s.listener.Close(); err != nil { + return err + } + + return nil +} diff --git a/delivery/http/config.go b/delivery/http/config.go new file mode 100644 index 0000000..e21de6f --- /dev/null +++ b/delivery/http/config.go @@ -0,0 +1,6 @@ +package http + +type Config struct { + Bind string `yaml:"bind"` + Port uint16 `yaml:"port"` +} diff --git a/delivery/http/middleware/auth.go b/delivery/http/middleware/auth.go new file mode 100644 index 0000000..d2367c8 --- /dev/null +++ b/delivery/http/middleware/auth.go @@ -0,0 +1,17 @@ +package middleware + +import "github.com/labstack/echo/v4" + +func Auth(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if err := next(c); err != nil { + c.Error(err) + } + + _, _, _ = c.Get("pk"), c.Get("msg"), c.Get("sig") + + // verify. + + return nil + } +} diff --git a/delivery/http/server.go b/delivery/http/server.go new file mode 100644 index 0000000..509988a --- /dev/null +++ b/delivery/http/server.go @@ -0,0 +1,42 @@ +package http + +import ( + "fmt" + + userh "github.com/dezh-tech/geb/delivery/http/user_handler" + users "github.com/dezh-tech/geb/service/user" + "github.com/labstack/echo/v4" +) + +type Server struct { + config Config + userHandler userh.Handler + Router *echo.Echo +} + +func New(config Config, userSvc users.Service) Server { + return Server{ + Router: echo.New(), + config: config, + userHandler: userh.New(userSvc), + } +} + +func (s Server) Start() error { + s.userHandler.SetRoutes(s.Router) + + address := fmt.Sprintf(":%d", s.config.Port) + if err := s.Router.Start(address); err != nil { + return err + } + + return nil +} + +func (s Server) Stop() error { + if err := s.Router.Close(); err != nil { + return err + } + + return nil +} diff --git a/delivery/http/user_handler/handler.go b/delivery/http/user_handler/handler.go new file mode 100644 index 0000000..15545dc --- /dev/null +++ b/delivery/http/user_handler/handler.go @@ -0,0 +1,13 @@ +package userhandler + +import "github.com/dezh-tech/geb/service/user" + +type Handler struct { + userSvc user.Service +} + +func New(userSvc user.Service) Handler { + return Handler{ + userSvc: userSvc, + } +} diff --git a/delivery/http/user_handler/profile.go b/delivery/http/user_handler/profile.go new file mode 100644 index 0000000..6464e39 --- /dev/null +++ b/delivery/http/user_handler/profile.go @@ -0,0 +1,19 @@ +package userhandler + +import ( + "net/http" + + "github.com/dezh-tech/geb/service/user" + "github.com/labstack/echo/v4" +) + +func (h Handler) userProfile(c echo.Context) error { + pubkey, _ := c.Get("pubkey").(string) // not safe! + + resp, err := h.userSvc.Profile(user.ProfileRequest{Pubkey: pubkey}) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, "not found") + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/delivery/http/user_handler/router.go b/delivery/http/user_handler/router.go new file mode 100644 index 0000000..383d3d1 --- /dev/null +++ b/delivery/http/user_handler/router.go @@ -0,0 +1,13 @@ +package userhandler + +import ( + "github.com/dezh-tech/geb/delivery/http/middleware" + "github.com/labstack/echo/v4" +) + +func (h Handler) SetRoutes(e *echo.Echo) { + userGroup := e.Group("/users") + + userGroup.GET("/profile", h.userProfile, + middleware.Auth) +} diff --git a/documents/.keep b/documents/.keep new file mode 100644 index 0000000..e69de29 diff --git a/entity/user.go b/entity/user.go new file mode 100644 index 0000000..22e0360 --- /dev/null +++ b/entity/user.go @@ -0,0 +1,6 @@ +package entity + +type User struct { + Name string `bson:"name"` + Pubkey string `bson:"pubkey"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f31baed --- /dev/null +++ b/go.mod @@ -0,0 +1,41 @@ +module github.com/dezh-tech/geb + +go 1.23.3 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.13.3 + github.com/redis/go-redis/v9 v9.7.0 + github.com/rs/zerolog v1.33.0 + go.mongodb.org/mongo-driver v1.17.1 + google.golang.org/grpc v1.69.2 + google.golang.org/protobuf v1.36.1 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f83f62b --- /dev/null +++ b/go.sum @@ -0,0 +1,139 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/infrastructure/database/config.go b/infrastructure/database/config.go new file mode 100644 index 0000000..0959162 --- /dev/null +++ b/infrastructure/database/config.go @@ -0,0 +1,8 @@ +package database + +type Config struct { + URI string + DBName string `yaml:"db_name"` + ConnectionTimeout int16 `yaml:"connection_timeout_in_ms"` + QueryTimeout int16 `yaml:"query_timeout_in_ms"` +} diff --git a/infrastructure/database/database.go b/infrastructure/database/database.go new file mode 100644 index 0000000..0cfea1f --- /dev/null +++ b/infrastructure/database/database.go @@ -0,0 +1,51 @@ +package database + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Database struct { + DBName string + QueryTimeout time.Duration + Client *mongo.Client +} + +func Connect(cfg Config) (*Database, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.ConnectionTimeout)*time.Millisecond) + defer cancel() + + serverAPI := options.ServerAPI(options.ServerAPIVersion1) + opts := options.Client().ApplyURI(cfg.URI). + SetServerAPIOptions(serverAPI). + SetConnectTimeout(time.Duration(cfg.ConnectionTimeout) * time.Millisecond). + SetBSONOptions(&options.BSONOptions{ + UseJSONStructTags: true, + NilSliceAsEmpty: true, + }) + + client, err := mongo.Connect(ctx, opts) + if err != nil { + return nil, err + } + + qCtx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.QueryTimeout)*time.Millisecond) + defer cancel() + + if err := client.Ping(qCtx, nil); err != nil { + return nil, err + } + + return &Database{ + Client: client, + DBName: cfg.DBName, + QueryTimeout: time.Duration(cfg.QueryTimeout) * time.Millisecond, + }, nil +} + +func (db *Database) Stop() error { + return db.Client.Disconnect(context.Background()) +} diff --git a/infrastructure/grpc_client/buf/buf.gen.yaml b/infrastructure/grpc_client/buf/buf.gen.yaml new file mode 100644 index 0000000..dcf1c1c --- /dev/null +++ b/infrastructure/grpc_client/buf/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v1 +plugins: + - name: go + out: ../gen + opt: paths=source_relative + - name: go-grpc + out: ../gen + opt: paths=source_relative,require_unimplemented_servers=false diff --git a/infrastructure/grpc_client/client.go b/infrastructure/grpc_client/client.go new file mode 100644 index 0000000..041fba6 --- /dev/null +++ b/infrastructure/grpc_client/client.go @@ -0,0 +1,35 @@ +package grpcclient + +import ( + "context" + + pb "github.com/dezh-tech/geb/infrastructure/grpc_client/gen" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Client struct { + StringService pb.GetStringServiceClient + conn *grpc.ClientConn +} + +func New(endpoint string) (*Client, error) { + conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + + return &Client{ + StringService: pb.NewGetStringServiceClient(conn), + conn: conn, + }, nil +} + +func (c *Client) GetString() (string, error) { + resp, err := c.StringService.GetString(context.Background(), &pb.GetStringRequest{}) + if err != nil { + return "", err + } + + return resp.Str, nil +} diff --git a/infrastructure/grpc_client/config.go b/infrastructure/grpc_client/config.go new file mode 100644 index 0000000..f933eaa --- /dev/null +++ b/infrastructure/grpc_client/config.go @@ -0,0 +1,5 @@ +package grpcclient + +type Config struct { + Endpoint string `yaml:"endpoint"` +} diff --git a/infrastructure/grpc_client/gen/example.pb.go b/infrastructure/grpc_client/gen/example.pb.go new file mode 100644 index 0000000..0803a6a --- /dev/null +++ b/infrastructure/grpc_client/gen/example.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc (unknown) +// source: example.proto + +package grpc_client + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetStringRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetStringRequest) Reset() { + *x = GetStringRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_example_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetStringRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStringRequest) ProtoMessage() {} + +func (x *GetStringRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStringRequest.ProtoReflect.Descriptor instead. +func (*GetStringRequest) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{0} +} + +type GetStringResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Str string `protobuf:"bytes,1,opt,name=str,proto3" json:"str,omitempty"` +} + +func (x *GetStringResponse) Reset() { + *x = GetStringResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_example_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetStringResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStringResponse) ProtoMessage() {} + +func (x *GetStringResponse) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetStringResponse.ProtoReflect.Descriptor instead. +func (*GetStringResponse) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{1} +} + +func (x *GetStringResponse) GetStr() string { + if x != nil { + return x.Str + } + return "" +} + +var File_example_proto protoreflect.FileDescriptor + +var file_example_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x12, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x25, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x74, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x73, 0x74, 0x72, 0x32, 0x5c, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x09, 0x47, 0x65, + 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x1c, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x7a, 0x68, 0x2d, 0x74, 0x65, 0x63, 0x68, 0x2f, 0x67, 0x6f, 0x2d, + 0x67, 0x69, 0x6e, 0x2d, 0x62, 0x6f, 0x69, 0x6c, 0x65, 0x72, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x2f, + 0x69, 0x6e, 0x66, 0x72, 0x61, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2f, 0x67, + 0x72, 0x70, 0x63, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_example_proto_rawDescOnce sync.Once + file_example_proto_rawDescData = file_example_proto_rawDesc +) + +func file_example_proto_rawDescGZIP() []byte { + file_example_proto_rawDescOnce.Do(func() { + file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData) + }) + return file_example_proto_rawDescData +} + +var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_example_proto_goTypes = []interface{}{ + (*GetStringRequest)(nil), // 0: service.v1.GetStringRequest + (*GetStringResponse)(nil), // 1: service.v1.GetStringResponse +} +var file_example_proto_depIdxs = []int32{ + 0, // 0: service.v1.GetStringService.GetString:input_type -> service.v1.GetStringRequest + 1, // 1: service.v1.GetStringService.GetString:output_type -> service.v1.GetStringResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_example_proto_init() } +func file_example_proto_init() { + if File_example_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_example_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetStringRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_example_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetStringResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_example_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_proto_goTypes, + DependencyIndexes: file_example_proto_depIdxs, + MessageInfos: file_example_proto_msgTypes, + }.Build() + File_example_proto = out.File + file_example_proto_rawDesc = nil + file_example_proto_goTypes = nil + file_example_proto_depIdxs = nil +} diff --git a/infrastructure/grpc_client/gen/example_grpc.pb.go b/infrastructure/grpc_client/gen/example_grpc.pb.go new file mode 100644 index 0000000..d9a7a73 --- /dev/null +++ b/infrastructure/grpc_client/gen/example_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: example.proto + +package grpc_client + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + GetStringService_GetString_FullMethodName = "/service.v1.GetStringService/GetString" +) + +// GetStringServiceClient is the client API for GetStringService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GetStringServiceClient interface { + GetString(ctx context.Context, in *GetStringRequest, opts ...grpc.CallOption) (*GetStringResponse, error) +} + +type getStringServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGetStringServiceClient(cc grpc.ClientConnInterface) GetStringServiceClient { + return &getStringServiceClient{cc} +} + +func (c *getStringServiceClient) GetString(ctx context.Context, in *GetStringRequest, opts ...grpc.CallOption) (*GetStringResponse, error) { + out := new(GetStringResponse) + err := c.cc.Invoke(ctx, GetStringService_GetString_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GetStringServiceServer is the server API for GetStringService service. +// All implementations should embed UnimplementedGetStringServiceServer +// for forward compatibility +type GetStringServiceServer interface { + GetString(context.Context, *GetStringRequest) (*GetStringResponse, error) +} + +// UnimplementedGetStringServiceServer should be embedded to have forward compatible implementations. +type UnimplementedGetStringServiceServer struct { +} + +func (UnimplementedGetStringServiceServer) GetString(context.Context, *GetStringRequest) (*GetStringResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetString not implemented") +} + +// UnsafeGetStringServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GetStringServiceServer will +// result in compilation errors. +type UnsafeGetStringServiceServer interface { + mustEmbedUnimplementedGetStringServiceServer() +} + +func RegisterGetStringServiceServer(s grpc.ServiceRegistrar, srv GetStringServiceServer) { + s.RegisterService(&GetStringService_ServiceDesc, srv) +} + +func _GetStringService_GetString_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetStringRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GetStringServiceServer).GetString(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GetStringService_GetString_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GetStringServiceServer).GetString(ctx, req.(*GetStringRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GetStringService_ServiceDesc is the grpc.ServiceDesc for GetStringService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GetStringService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "service.v1.GetStringService", + HandlerType: (*GetStringServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetString", + Handler: _GetStringService_GetString_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "example.proto", +} diff --git a/infrastructure/grpc_client/proto/example.proto b/infrastructure/grpc_client/proto/example.proto new file mode 100644 index 0000000..f3c5a71 --- /dev/null +++ b/infrastructure/grpc_client/proto/example.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package service.v1; + +option go_package = "github.com/dezh-tech/go-gin-boilerplate/infrastructure/grpc_client"; + +service GetStringService { + rpc GetString (GetStringRequest) returns (GetStringResponse); +} + +message GetStringRequest {} + +message GetStringResponse { + string str = 1; +} \ No newline at end of file diff --git a/infrastructure/redis/config.go b/infrastructure/redis/config.go new file mode 100644 index 0000000..d28c006 --- /dev/null +++ b/infrastructure/redis/config.go @@ -0,0 +1,7 @@ +package redis + +type Config struct { + URI string + ConnectionTimeout int16 `yaml:"connection_timeout_in_ms"` + QueryTimeout int16 `yaml:"query_timeout_in_ms"` +} diff --git a/infrastructure/redis/redis.go b/infrastructure/redis/redis.go new file mode 100644 index 0000000..56bc473 --- /dev/null +++ b/infrastructure/redis/redis.go @@ -0,0 +1,35 @@ +package redis + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +type Redis struct { + Client *redis.Client + QueryTimeout time.Duration +} + +func New(cfg Config) (*Redis, error) { + opts, err := redis.ParseURL(cfg.URI) + if err != nil { + return nil, err + } + + rc := redis.NewClient(opts) + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.ConnectionTimeout)*time.Millisecond) + defer cancel() + + if err := rc.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("could not connect to Redis: %w", err) + } + + return &Redis{ + Client: rc, + QueryTimeout: time.Duration(cfg.QueryTimeout) * time.Millisecond, + }, nil +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..a3ca699 --- /dev/null +++ b/makefile @@ -0,0 +1,52 @@ +PACKAGES=$(shell go list ./... | grep -v 'tests' | grep -v 'grpc/gen') + +ifneq (,$(filter $(OS),Windows_NT MINGW64)) +RM = del /q +else +RM = rm -rf +endif + +### Tools needed for development +devtools: + @echo "Installing devtools" + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install mvdan.cc/gofumpt@latest + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5 + go install github.com/pactus-project/protoc-gen-doc/cmd/protoc-gen-doc@v0.0.0-20240815105130-84e89d0170e4 + go install github.com/bufbuild/buf/cmd/buf@v1.47 + +### Testing +unit-test: + go test $(PACKAGES) + +test: + go test ./... -covermode=atomic + +test-race: + go test ./... --race + +### Formatting the code +fmt: + gofumpt -l -w . + go mod tidy + +check: + golangci-lint run --timeout=20m0s + +### Building +build: + go build -o build/project cmd/main.go + +### Proto +proto: + $(RM) infrastructure/grpc_client/gen + $(RM) delivery/grpc/gen + cd infrastructure/grpc_client/buf && buf generate --template buf.gen.yaml ../proto + cd delivery/grpc/buf && buf generate --template buf.gen.yaml ../proto + +### pre commit +pre-commit: fmt check unit-test + @echo ready to commit... + +.PHONY: build diff --git a/pkg/logger/config.go b/pkg/logger/config.go new file mode 100644 index 0000000..f15da8f --- /dev/null +++ b/pkg/logger/config.go @@ -0,0 +1,10 @@ +package logger + +type Config struct { + Filename string `yaml:"filename"` + LogLevel string `yaml:"level"` + Targets []string `yaml:"targets"` + MaxSize int `yaml:"max_size"` + MaxBackups int `yaml:"max_backups"` + Compress bool `yaml:"compress"` +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..256b61e --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,122 @@ +package logger + +import ( + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "slices" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gopkg.in/natefinch/lumberjack.v2" +) + +var globalInst *logger + +type logger struct { + writer io.Writer +} + +func InitGlobalLogger(cfg *Config) { + writers := []io.Writer{} + + if slices.Contains(cfg.Targets, "file") { + fileWriter := &lumberjack.Logger{ + Filename: cfg.Filename, + MaxSize: cfg.MaxSize, + MaxBackups: cfg.MaxBackups, + Compress: cfg.Compress, + } + writers = append(writers, fileWriter) + } + + if slices.Contains(cfg.Targets, "console") { + writers = append(writers, zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}) + } + + globalInst = &logger{ + writer: io.MultiWriter(writers...), + } + + level, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel)) + if err != nil { + level = zerolog.InfoLevel + } + zerolog.SetGlobalLevel(level) + + log.Logger = zerolog.New(globalInst.writer).With().Timestamp().Logger() +} + +func addFields(event *zerolog.Event, keyvals ...any) *zerolog.Event { + if len(keyvals)%2 != 0 { + keyvals = append(keyvals, "!MISSING-VALUE!") + } + + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + key = "!INVALID-KEY!" + } + + value := keyvals[i+1] + switch typ := value.(type) { + case fmt.Stringer: + if isNil(typ) { + event.Any(key, typ) + } else { + event.Stringer(key, typ) + } + case error: + event.AnErr(key, typ) + case []byte: + event.Str(key, hex.EncodeToString(typ)) + default: + event.Any(key, typ) + } + } + + return event +} + +func Trace(msg string, keyvals ...any) { + addFields(log.Trace(), keyvals...).Msg(msg) +} + +func Debug(msg string, keyvals ...any) { + addFields(log.Debug(), keyvals...).Msg(msg) +} + +func Info(msg string, keyvals ...any) { + addFields(log.Info(), keyvals...).Msg(msg) +} + +func Warn(msg string, keyvals ...any) { + addFields(log.Warn(), keyvals...).Msg(msg) +} + +func Error(msg string, keyvals ...any) { + addFields(log.Error(), keyvals...).Msg(msg) +} + +func Fatal(msg string, keyvals ...any) { + addFields(log.Fatal(), keyvals...).Msg(msg) +} + +func Panic(msg string, keyvals ...any) { + addFields(log.Panic(), keyvals...).Msg(msg) +} + +func isNil(i any) bool { + if i == nil { + return true + } + + if reflect.TypeOf(i).Kind() == reflect.Ptr { + return reflect.ValueOf(i).IsNil() + } + + return false +} diff --git a/pkg/utils/random.go b/pkg/utils/random.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/pkg/utils/random.go @@ -0,0 +1 @@ +package utils diff --git a/repository/.keep b/repository/.keep new file mode 100644 index 0000000..e69de29 diff --git a/repository/user/user.go b/repository/user/user.go new file mode 100644 index 0000000..12b9d74 --- /dev/null +++ b/repository/user/user.go @@ -0,0 +1,57 @@ +package user + +import ( + "context" + "errors" + + "github.com/dezh-tech/geb/entity" + "github.com/dezh-tech/geb/infrastructure/database" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type User struct { + db *database.Database +} + +func New(db *database.Database) User { + return User{ + db: db, + } +} + +func (u User) Add(usr entity.User) error { + collection := u.db.Client.Database(u.db.DBName).Collection("users") + + ctx, cancel := context.WithTimeout(context.Background(), u.db.QueryTimeout) + defer cancel() + + _, err := collection.InsertOne(ctx, bson.M{ + "name": usr.Name, + "pubkey": usr.Pubkey, + }) + if err != nil { + return err + } + + return nil +} + +func (u User) GetByPubkey(pubkey string) (entity.User, error) { + collection := u.db.Client.Database(u.db.DBName).Collection("users") + + ctx, cancel := context.WithTimeout(context.Background(), u.db.QueryTimeout) + defer cancel() + + var usr entity.User + err := collection.FindOne(ctx, bson.M{"pubkey": pubkey}).Decode(&usr) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return usr, nil + } + + return usr, err + } + + return usr, nil +} diff --git a/service/.keep b/service/.keep new file mode 100644 index 0000000..e69de29 diff --git a/service/user/profile.go b/service/user/profile.go new file mode 100644 index 0000000..d5b727e --- /dev/null +++ b/service/user/profile.go @@ -0,0 +1,20 @@ +package user + +import "errors" + +type ProfileRequest struct { + Pubkey string +} + +type ProfileResponse struct { + Name string `json:"name"` +} + +func (s Service) Profile(req ProfileRequest) (ProfileResponse, error) { + user, err := s.repo.GetByPubkey(req.Pubkey) + if err != nil { + return ProfileResponse{}, errors.New("can't get the profile") // todo::: move to errors.go + } + + return ProfileResponse{Name: user.Name}, nil +} diff --git a/service/user/service.go b/service/user/service.go new file mode 100644 index 0000000..e8d7f8f --- /dev/null +++ b/service/user/service.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/dezh-tech/geb/entity" +) + +type Repository interface { + Add(usr entity.User) error + GetByPubkey(pubkey string) (entity.User, error) +} + +type Service struct { + repo Repository +} + +func New(repo Repository) Service { + return Service{repo: repo} +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..9e37fb9 --- /dev/null +++ b/version.go @@ -0,0 +1,22 @@ +package goginboilerplate + +import "fmt" + +// These constants follow the semantic versioning 2.0.0 spec. +// see: http://semver.org +var ( + major = 0 + minor = 0 + patch = 1 + meta = "beta" +) + +func StringVersion() string { + v := fmt.Sprintf("project - %d.%d.%d", major, minor, patch) + + if meta != "" { + v = fmt.Sprintf("%s-%s", v, meta) + } + + return v +}