mirror of
				https://github.com/netscrawler/changeAPI.git
				synced 2025-10-31 20:43:13 +00:00 
			
		
		
		
	Squashed commit of the following:
commit5b11756af6Author: netscrawler <mariarthyjamesoo@gmail.com> Date: Thu Jul 4 00:44:15 2024 +0300 v0.1 commitc99f477e35Author: netscrawler <mariarthyjamesoo@gmail.com> Date: Sun Jun 30 20:44:53 2024 +0300 v0.0.6
This commit is contained in:
		
							parent
							
								
									61d2be39f1
								
							
						
					
					
						commit
						3085eb993a
					
				
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| # Default ignored files | ||||
| /shelf/ | ||||
| /workspace.xml | ||||
| # Editor-based HTTP Client requests | ||||
| /httpRequests/ | ||||
| # Datasource local storage ignored files | ||||
| /dataSources/ | ||||
| /dataSources.local.xml | ||||
							
								
								
									
										9
									
								
								.idea/changeAPI.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/changeAPI.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <module type="WEB_MODULE" version="4"> | ||||
|   <component name="Go" enabled="true" /> | ||||
|   <component name="NewModuleRootManager"> | ||||
|     <content url="file://$MODULE_DIR$" /> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   </component> | ||||
| </module> | ||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="ProjectModuleManager"> | ||||
|     <modules> | ||||
|       <module fileurl="file://$PROJECT_DIR$/.idea/changeAPI.iml" filepath="$PROJECT_DIR$/.idea/changeAPI.iml" /> | ||||
|     </modules> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="VcsDirectoryMappings"> | ||||
|     <mapping directory="" vcs="Git" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										9
									
								
								Taskfile.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Taskfile.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| version: "3" | ||||
| 
 | ||||
| tasks: | ||||
|   runServer: | ||||
|     aliases: | ||||
|       - run | ||||
|     desc: "Run grpc service" | ||||
|     cmds: | ||||
|      - go run cmd/converter/main.go --config .\config\local.yaml | ||||
							
								
								
									
										95
									
								
								cmd/converter/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								cmd/converter/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"converter/internal/app" | ||||
| 	"converter/internal/config" | ||||
| 	"converter/internal/lib/logger/handlers/slogpretty" | ||||
| 	"fmt" | ||||
| 	"github.com/redis/go-redis/v9" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"syscall" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	envlocal = "local" | ||||
| 	envdev   = "dev" | ||||
| 	envprod  = "prod" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 
 | ||||
| 	cfg := config.MustLoad() | ||||
| 
 | ||||
| 	fmt.Println(cfg) | ||||
| 
 | ||||
| 	log := setupLogger(cfg.Env) | ||||
| 	log.Info("starting application", slog.Any("cfg", cfg)) | ||||
| 	applicaton := app.New(log, cfg.GRPC.Port, cfg.TokenTTL) | ||||
| 	go applicaton.GRPCSrv.MustRun() | ||||
| 
 | ||||
| 	// TODO: инициализировать приложение app
 | ||||
| 
 | ||||
| 	//TODO:redis
 | ||||
| 	client := redis.NewClient(&redis.Options{ | ||||
| 		Addr:     cfg.Redis.Addr, | ||||
| 		Password: cfg.Redis.Password, | ||||
| 		DB:       cfg.Redis.DB, | ||||
| 	}) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 
 | ||||
| 	err := client.Set(ctx, "key", "value1", 0).Err() | ||||
| 	if err != nil { | ||||
| 		log.Info("err") | ||||
| 	} | ||||
| 
 | ||||
| 	val, err := client.Get(ctx, "key").Result() | ||||
| 	if err != nil { | ||||
| 		log.Info("err") | ||||
| 	} | ||||
| 	log.Info("key", slog.Any("val", val)) | ||||
| 
 | ||||
| 	// TODO: запустить gRPC сервер приложения
 | ||||
| 
 | ||||
| 	// Graceful shutdown
 | ||||
| 	stop := make(chan os.Signal, 1) | ||||
| 	signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) | ||||
| 
 | ||||
| 	sign := <-stop | ||||
| 	log.Info("stoping application", slog.String("signal", sign.String())) | ||||
| 	applicaton.GRPCSrv.Stop() | ||||
| 	log.Info("application stopped") | ||||
| } | ||||
| 
 | ||||
| func setupLogger(env string) *slog.Logger { | ||||
| 	var log *slog.Logger | ||||
| 
 | ||||
| 	switch env { | ||||
| 	case envlocal: | ||||
| 		log = setupPrettySlog() | ||||
| 	case envdev: | ||||
| 		log = slog.New( | ||||
| 			slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), | ||||
| 		) | ||||
| 	case envprod: | ||||
| 		log = slog.New( | ||||
| 			slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}), | ||||
| 		) | ||||
| 	} | ||||
| 	return log | ||||
| } | ||||
| 
 | ||||
| func setupPrettySlog() *slog.Logger { | ||||
| 	opts := slogpretty.PrettyHandlerOptions{ | ||||
| 		SlogOpts: &slog.HandlerOptions{ | ||||
| 			Level: slog.LevelDebug, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	handler := opts.NewPrettyHandler(os.Stdout) | ||||
| 
 | ||||
| 	return slog.New(handler) | ||||
| } | ||||
							
								
								
									
										9
									
								
								config/local.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								config/local.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| env: "local" | ||||
| token_ttl: 1h | ||||
| grpc: | ||||
|   port: 44044 | ||||
|   timeout: 10h | ||||
| redis: | ||||
|   addr: "localhost:6379" | ||||
|   password: "1234" | ||||
|   db: 0 | ||||
							
								
								
									
										28
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| module converter | ||||
| 
 | ||||
| go 1.22 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/fatih/color v1.17.0 | ||||
| 	github.com/ilyakaznacheev/cleanenv v1.5.0 | ||||
| 	github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4 | ||||
| 	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 | ||||
| 	google.golang.org/grpc v1.64.0 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.2.1 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.2.0 // indirect | ||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||
| 	github.com/joho/godotenv v1.5.1 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/redis/go-redis/v9 v9.5.3 // indirect | ||||
| 	golang.org/x/net v0.22.0 // indirect | ||||
| 	golang.org/x/sys v0.18.0 // indirect | ||||
| 	golang.org/x/text v0.14.0 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect | ||||
| 	google.golang.org/protobuf v1.34.2 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect | ||||
| ) | ||||
							
								
								
									
										45
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= | ||||
| github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= | ||||
| github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||
| 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/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= | ||||
| github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= | ||||
| 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/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= | ||||
| github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4 h1:V/zWems1vjc1Ob0p0APeL+tKiSHeOW4Kc1yo3xP0IMQ= | ||||
| github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4/go.mod h1:+Ms2tgnXO0rzzVXLn61kltLnEOA/CuSKSv62L+cacdQ= | ||||
| github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= | ||||
| github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= | ||||
| golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= | ||||
| golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= | ||||
| golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= | ||||
| golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= | ||||
| 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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= | ||||
| golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= | ||||
| google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= | ||||
| google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= | ||||
| google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= | ||||
| google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= | ||||
| olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= | ||||
							
								
								
									
										24
									
								
								internal/app/app.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/app/app.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| package app | ||||
| 
 | ||||
| import ( | ||||
| 	grpcapp "converter/internal/app/grpc" | ||||
| 	"log/slog" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type App struct { | ||||
| 	GRPCSrv *grpcapp.App | ||||
| } | ||||
| 
 | ||||
| func New( | ||||
| 	log *slog.Logger, | ||||
| 	grpcPort int, | ||||
| 	tokenTTl time.Duration) *App { | ||||
| 	//TODO: инициализировать хранилище
 | ||||
| 	//TODO: init convert service
 | ||||
| 
 | ||||
| 	grpcApp := grpcapp.New(log, grpcPort) | ||||
| 	return &App{ | ||||
| 		GRPCSrv: grpcApp, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										63
									
								
								internal/app/grpc/app.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/app/grpc/app.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | ||||
| package grpcapp | ||||
| 
 | ||||
| import ( | ||||
| 	convertgrpc "converter/internal/grpc/cnvrt" | ||||
| 	"converter/internal/services/converter" | ||||
| 	"fmt" | ||||
| 	"google.golang.org/grpc" | ||||
| 	"log/slog" | ||||
| 	"net" | ||||
| ) | ||||
| 
 | ||||
| type App struct { | ||||
| 	log        *slog.Logger | ||||
| 	gRPCServer *grpc.Server | ||||
| 	port       int | ||||
| } | ||||
| 
 | ||||
| func New( | ||||
| 	log *slog.Logger, | ||||
| 	port int) *App { | ||||
| 	gRPCServer := grpc.NewServer() | ||||
| 	convert := converter.New(log) | ||||
| 	convertgrpc.Register(gRPCServer, convert) | ||||
| 
 | ||||
| 	return &App{ | ||||
| 		log:        log, | ||||
| 		gRPCServer: gRPCServer, | ||||
| 		port:       port, | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func (a *App) MustRun() { | ||||
| 	if err := a.Run(); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (a *App) Run() error { | ||||
| 	const op = "grpcapp.Run" | ||||
| 	log := a.log.With( | ||||
| 		slog.String("op", op), | ||||
| 		slog.Int("port", a.port)) | ||||
| 
 | ||||
| 	l, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("%s: %w", op, err) | ||||
| 	} | ||||
| 	log.Info("gRPC server is running", slog.String("addr", l.Addr().String())) | ||||
| 
 | ||||
| 	if err := a.gRPCServer.Serve(l); err != nil { | ||||
| 		return fmt.Errorf("%s: %w", op, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (a *App) Stop() { | ||||
| 	const op = "grpcapp.Stop" | ||||
| 
 | ||||
| 	a.log.With(slog.String("op", op)). | ||||
| 		Info("stopping gRPC server", slog.Int("port", a.port)) | ||||
| 	a.gRPCServer.GracefulStop() | ||||
| } | ||||
							
								
								
									
										59
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"github.com/ilyakaznacheev/cleanenv" | ||||
| 	"os" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Config struct { | ||||
| 	Env      string        `yaml:"env" env-default:"local"` | ||||
| 	TokenTTL time.Duration `yaml:"token_ttl" env-required:"true"` | ||||
| 	GRPC     GRPCConfig    `yaml:"grpc"` | ||||
| 	Redis    RedisConfig   `yaml:"redis"` | ||||
| } | ||||
| 
 | ||||
| type GRPCConfig struct { | ||||
| 	Port    int           `yaml:"port"` | ||||
| 	Timeout time.Duration `yaml:"timeout"` | ||||
| } | ||||
| 
 | ||||
| type RedisConfig struct { | ||||
| 	Addr     string `yaml:"addr"` | ||||
| 	Password string `yaml:"password"` | ||||
| 	DB       int    `yaml:"db"` | ||||
| } | ||||
| 
 | ||||
| func MustLoad() *Config { | ||||
| 	path := fetchConfigPath() | ||||
| 	if path == "" { | ||||
| 		panic("config path is empty") | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := os.Stat(path); os.IsNotExist(err) { | ||||
| 		panic("config file not found" + path) | ||||
| 	} | ||||
| 
 | ||||
| 	var cfg Config | ||||
| 
 | ||||
| 	if err := cleanenv.ReadConfig(path, &cfg); err != nil { | ||||
| 		panic("failed to read config" + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	return &cfg | ||||
| } | ||||
| 
 | ||||
| func fetchConfigPath() string { | ||||
| 	var res string | ||||
| 
 | ||||
| 	flag.StringVar(&res, "config", "", "path to config file") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	if res == "" { | ||||
| 		res = os.Getenv("CONFIG_PATH") | ||||
| 	} | ||||
| 
 | ||||
| 	return res | ||||
| } | ||||
							
								
								
									
										6
									
								
								internal/domain/models/vunitRate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/domain/models/vunitRate.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| package models | ||||
| 
 | ||||
| type VunitRate struct { | ||||
| 	Currency string | ||||
| 	Rate     float32 | ||||
| } | ||||
							
								
								
									
										59
									
								
								internal/grpc/cnvrt/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/grpc/cnvrt/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| package cnvrt | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	cnvrtv1 "github.com/netscrawler/protoss/gen/go/changeAPI" | ||||
| 	"google.golang.org/grpc" | ||||
| 	"google.golang.org/grpc/codes" | ||||
| 	"google.golang.org/grpc/status" | ||||
| ) | ||||
| 
 | ||||
| type Converter interface { | ||||
| 	Convert(ctx context.Context, | ||||
| 		amount uint32, | ||||
| 		targetCurrency string, | ||||
| 	) (convertedAmount uint32, rate float32, err error) | ||||
| } | ||||
| 
 | ||||
| type serverAPI struct { | ||||
| 	cnvrtv1.UnimplementedConverterServer | ||||
| 	convert Converter | ||||
| } | ||||
| 
 | ||||
| func Register(gRPC *grpc.Server, convert Converter) { | ||||
| 	cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{convert: convert}) | ||||
| } | ||||
| 
 | ||||
| func (s *serverAPI) Convert( | ||||
| 	ctx context.Context, | ||||
| 	req *cnvrtv1.ConvertRequest) ( | ||||
| 	*cnvrtv1.ConvertResponse, error) { | ||||
| 	if !isValidCurrency(req.GetTargetCurrency()) { | ||||
| 		return nil, status.Error(codes.InvalidArgument, "Invalid target currency") | ||||
| 	} | ||||
| 
 | ||||
| 	convertedAmount, rate, err := s.convert.Convert(ctx, req.GetAmount(), req.GetTargetCurrency()) | ||||
| 	if err != nil { | ||||
| 		//todo error handler
 | ||||
| 		return nil, status.Error(codes.Internal, "Internal error") | ||||
| 	} | ||||
| 	return &cnvrtv1.ConvertResponse{ | ||||
| 		BaseAmount:        req.GetAmount(), | ||||
| 		ConvertedAmount:   convertedAmount, | ||||
| 		ConvertedCurrency: req.GetTargetCurrency(), | ||||
| 		Rate:              rate, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func isValidCurrency(currency string) bool { | ||||
| 
 | ||||
| 	currencies := map[string]bool{ | ||||
| 		"AUD": true, "GBP": true, "BYR": true, "DKK": true, "USD": true, "EUR": true, | ||||
| 		"ISK": true, "KZT": true, "CAD": true, "NOK": true, "XDR": true, "SGD": true, | ||||
| 		"TRL": true, "UAH": true, "SEK": true, "CHF": true, "JPY": true, | ||||
| 	} | ||||
| 	if currencies[currency] { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
							
								
								
									
										36
									
								
								internal/lib/logger/handlers/slogdiscard/slogdiscard.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								internal/lib/logger/handlers/slogdiscard/slogdiscard.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| package slogdiscard | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"golang.org/x/exp/slog" | ||||
| ) | ||||
| 
 | ||||
| func NewDiscardLogger() *slog.Logger { | ||||
| 	return slog.New(NewDiscardHandler()) | ||||
| } | ||||
| 
 | ||||
| type DiscardHandler struct{} | ||||
| 
 | ||||
| func NewDiscardHandler() *DiscardHandler { | ||||
| 	return &DiscardHandler{} | ||||
| } | ||||
| 
 | ||||
| func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { | ||||
| 	// Просто игнорируем запись журнала
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { | ||||
| 	// Возвращает тот же обработчик, так как нет атрибутов для сохранения
 | ||||
| 	return h | ||||
| } | ||||
| 
 | ||||
| func (h *DiscardHandler) WithGroup(_ string) slog.Handler { | ||||
| 	// Возвращает тот же обработчик, так как нет группы для сохранения
 | ||||
| 	return h | ||||
| } | ||||
| 
 | ||||
| func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { | ||||
| 	// Всегда возвращает false, так как запись журнала игнорируется
 | ||||
| 	return false | ||||
| } | ||||
							
								
								
									
										98
									
								
								internal/lib/logger/handlers/slogpretty/slogpretty.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								internal/lib/logger/handlers/slogpretty/slogpretty.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| package slogpretty | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	stdLog "log" | ||||
| 	"log/slog" | ||||
| 
 | ||||
| 	"github.com/fatih/color" | ||||
| ) | ||||
| 
 | ||||
| type PrettyHandlerOptions struct { | ||||
| 	SlogOpts *slog.HandlerOptions | ||||
| } | ||||
| 
 | ||||
| type PrettyHandler struct { | ||||
| 	opts PrettyHandlerOptions | ||||
| 	slog.Handler | ||||
| 	l     *stdLog.Logger | ||||
| 	attrs []slog.Attr | ||||
| } | ||||
| 
 | ||||
| func (opts PrettyHandlerOptions) NewPrettyHandler( | ||||
| 	out io.Writer, | ||||
| ) *PrettyHandler { | ||||
| 	h := &PrettyHandler{ | ||||
| 		Handler: slog.NewJSONHandler(out, opts.SlogOpts), | ||||
| 		l:       stdLog.New(out, "", 0), | ||||
| 	} | ||||
| 
 | ||||
| 	return h | ||||
| } | ||||
| 
 | ||||
| func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { | ||||
| 	level := r.Level.String() + ":" | ||||
| 
 | ||||
| 	switch r.Level { | ||||
| 	case slog.LevelDebug: | ||||
| 		level = color.MagentaString(level) | ||||
| 	case slog.LevelInfo: | ||||
| 		level = color.BlueString(level) | ||||
| 	case slog.LevelWarn: | ||||
| 		level = color.YellowString(level) | ||||
| 	case slog.LevelError: | ||||
| 		level = color.RedString(level) | ||||
| 	} | ||||
| 
 | ||||
| 	fields := make(map[string]interface{}, r.NumAttrs()) | ||||
| 
 | ||||
| 	r.Attrs(func(a slog.Attr) bool { | ||||
| 		fields[a.Key] = a.Value.Any() | ||||
| 
 | ||||
| 		return true | ||||
| 	}) | ||||
| 
 | ||||
| 	for _, a := range h.attrs { | ||||
| 		fields[a.Key] = a.Value.Any() | ||||
| 	} | ||||
| 
 | ||||
| 	var b []byte | ||||
| 	var err error | ||||
| 
 | ||||
| 	if len(fields) > 0 { | ||||
| 		b, err = json.MarshalIndent(fields, "", "  ") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	timeStr := r.Time.Format("[15:05:05.000]") | ||||
| 	msg := color.CyanString(r.Message) | ||||
| 
 | ||||
| 	h.l.Println( | ||||
| 		timeStr, | ||||
| 		level, | ||||
| 		msg, | ||||
| 		color.WhiteString(string(b)), | ||||
| 	) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { | ||||
| 	return &PrettyHandler{ | ||||
| 		Handler: h.Handler, | ||||
| 		l:       h.l, | ||||
| 		attrs:   attrs, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (h *PrettyHandler) WithGroup(name string) slog.Handler { | ||||
| 	// TODO: implement
 | ||||
| 	return &PrettyHandler{ | ||||
| 		Handler: h.Handler.WithGroup(name), | ||||
| 		l:       h.l, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								internal/lib/logger/sl/sl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/lib/logger/sl/sl.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| package sl | ||||
| 
 | ||||
| import ( | ||||
| 	"log/slog" | ||||
| ) | ||||
| 
 | ||||
| func Err(err error) slog.Attr { | ||||
| 	return slog.Attr{ | ||||
| 		Key:   "error", | ||||
| 		Value: slog.StringValue(err.Error()), | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										177
									
								
								internal/lib/rateExtract/rateExtract.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								internal/lib/rateExtract/rateExtract.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,177 @@ | ||||
| package rateExtract | ||||
| 
 | ||||
| import ( | ||||
| 	"compress/gzip" | ||||
| 	"converter/internal/domain/models" | ||||
| 	"encoding/xml" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golang.org/x/text/encoding/charmap" | ||||
| ) | ||||
| 
 | ||||
| type resultType struct { | ||||
| 	XMLName xml.Name `xml:"ValCurs"` | ||||
| 	Valute  []struct { | ||||
| 		NumCode  string `xml:"NumCode"` | ||||
| 		CharCode string `xml:"CharCode"` | ||||
| 		Nominal  string `xml:"Nominal"` | ||||
| 		Name     string `xml:"Name"` | ||||
| 		Value    string `xml:"Value"` | ||||
| 	} `xml:"Valute"` | ||||
| } | ||||
| 
 | ||||
| type cacheKeyType struct { | ||||
| 	CurrencyId string | ||||
| 	Date       string | ||||
| } | ||||
| 
 | ||||
| type cacheResultType struct { | ||||
| 	Rate float64 | ||||
| } | ||||
| 
 | ||||
| var urlTemplate string = "https://www.cbr.ru/scripts/XML_daily.asp?date_req=%s" | ||||
| 
 | ||||
| var cache map[cacheKeyType]*cacheResultType | ||||
| 
 | ||||
| func GetExchangeRate(currencyId string) (models.VunitRate, error) { | ||||
| 	date := time.Now() | ||||
| 	if cache == nil { | ||||
| 		cache = map[cacheKeyType]*cacheResultType{} | ||||
| 	} | ||||
| 
 | ||||
| 	reqDate := fmt.Sprintf("%02d/%02d/%d", date.Day(), date.Month(), date.Year()) | ||||
| 
 | ||||
| 	cacheKey := cacheKeyType{CurrencyId: currencyId, Date: reqDate} | ||||
| 
 | ||||
| 	cacheResult, exists := cache[cacheKey] | ||||
| 
 | ||||
| 	if !exists { | ||||
| 
 | ||||
| 		url := fmt.Sprintf(urlTemplate, reqDate) | ||||
| 
 | ||||
| 		req, err := http.NewRequest(http.MethodGet, url, nil) | ||||
| 		if err != nil { | ||||
| 			return models.VunitRate{}, err | ||||
| 		} | ||||
| 
 | ||||
| 		req.Header.Add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") | ||||
| 		req.Header.Add("accept-encoding", "gzip, deflate, br") | ||||
| 		req.Header.Add("accept-language", "en-US,en;q=0.9,ru;q=0.8") | ||||
| 		req.Header.Add("cache-control", "max-age=0") | ||||
| 		req.Header.Add("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36") | ||||
| 
 | ||||
| 		client := &http.Client{} | ||||
| 
 | ||||
| 		resp, err := client.Do(req) | ||||
| 		if err != nil { | ||||
| 			return models.VunitRate{Currency: currencyId, Rate: 0}, err | ||||
| 		} | ||||
| 
 | ||||
| 		defer resp.Body.Close() | ||||
| 
 | ||||
| 		var reader io.ReadCloser | ||||
| 
 | ||||
| 		switch resp.Header.Get("Content-Encoding") { | ||||
| 		case "gzip": | ||||
| 			reader, err = gzip.NewReader(resp.Body) | ||||
| 			if err != nil { | ||||
| 				return models.VunitRate{Currency: currencyId, Rate: 0}, err | ||||
| 			} | ||||
| 			defer reader.Close() | ||||
| 		default: | ||||
| 			reader = resp.Body | ||||
| 		} | ||||
| 
 | ||||
| 		xml := xml.NewDecoder(reader) | ||||
| 
 | ||||
| 		xml.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { | ||||
| 			switch charset { | ||||
| 			case "windows-1251": | ||||
| 				return charmap.Windows1251.NewDecoder().Reader(input), nil | ||||
| 			default: | ||||
| 				return nil, fmt.Errorf("unknown charset: %s", charset) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		result := &resultType{} | ||||
| 
 | ||||
| 		err = xml.Decode(result) | ||||
| 		if err != nil { | ||||
| 			return models.VunitRate{Currency: currencyId, Rate: 0}, err | ||||
| 		} | ||||
| 
 | ||||
| 		for _, resultRow := range result.Valute { | ||||
| 
 | ||||
| 			if resultRow.CharCode != currencyId { | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			resultRow.Value = strings.Replace(resultRow.Value, ",", ".", 1) | ||||
| 
 | ||||
| 			rate, err := strconv.ParseFloat(resultRow.Value, 64) | ||||
| 			if err != nil { | ||||
| 				return models.VunitRate{Currency: currencyId, Rate: 0}, err | ||||
| 			} | ||||
| 
 | ||||
| 			nominal, err := strconv.ParseInt(resultRow.Nominal, 10, 64) | ||||
| 			if err != nil { | ||||
| 				return models.VunitRate{Currency: currencyId, Rate: 0}, err | ||||
| 			} | ||||
| 
 | ||||
| 			cacheResult = &cacheResultType{Rate: rate / float64(nominal)} | ||||
| 
 | ||||
| 			cache[cacheKey] = cacheResult | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 	return models.VunitRate{Currency: currencyId, Rate: float32(cacheResult.Rate)}, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| //func Convert(from string, to string, value float64, date time.Time) (float64, error) {
 | ||||
| //
 | ||||
| //	if from == to {
 | ||||
| //		return value, nil
 | ||||
| //	}
 | ||||
| //
 | ||||
| //	if value == 0 {
 | ||||
| //		return 0, nil
 | ||||
| //	}
 | ||||
| //
 | ||||
| //	result := value
 | ||||
| //
 | ||||
| //	if from != CurrencyRUB {
 | ||||
| //
 | ||||
| //		exchangeRate, err := GetExchangeRate(from, date)
 | ||||
| //		if err != nil {
 | ||||
| //			return 0, err
 | ||||
| //		}
 | ||||
| //
 | ||||
| //		result = result * exchangeRate
 | ||||
| //
 | ||||
| //	}
 | ||||
| //
 | ||||
| //	if to != CurrencyRUB {
 | ||||
| //
 | ||||
| //		exchangeRate, err := GetExchangeRate(to, date)
 | ||||
| //		if err != nil {
 | ||||
| //			return 0, err
 | ||||
| //		}
 | ||||
| //
 | ||||
| //		result = result / exchangeRate
 | ||||
| //
 | ||||
| //	}
 | ||||
| //
 | ||||
| //	return (math.Floor(result*100) / 100), nil
 | ||||
| //
 | ||||
| //}
 | ||||
| //
 | ||||
| //func main() {
 | ||||
| //	fmt.Println(GetExchangeRate(CurrencyUSD, time.Now()))
 | ||||
| //}
 | ||||
							
								
								
									
										48
									
								
								internal/services/converter/converter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								internal/services/converter/converter.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| package converter | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"converter/internal/lib/logger/sl" | ||||
| 	"converter/internal/lib/rateExtract" | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| ) | ||||
| 
 | ||||
| type Converter struct { | ||||
| 	log *slog.Logger | ||||
| } | ||||
| 
 | ||||
| // New returns a new instance of the Converter service.
 | ||||
| func New( | ||||
| 	log *slog.Logger, | ||||
| 
 | ||||
| ) *Converter { | ||||
| 	return &Converter{ | ||||
| 		log: log, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *Converter) Convert( | ||||
| 	ctx context.Context, | ||||
| 	amount uint32, | ||||
| 	currency string, | ||||
| ) (uint32, float32, error) { | ||||
| 	const op = "converter.convert" | ||||
| 	log := c.log.With( | ||||
| 		slog.String("op", op), | ||||
| 		slog.String("currency", currency), | ||||
| 		slog.Any("amount", amount), | ||||
| 	) | ||||
| 	log.Info("convertation") | ||||
| 	var convertedAmount uint32 | ||||
| 	//TODO extract rate
 | ||||
| 	rate, err := rateExtract.GetExchangeRate(currency) | ||||
| 	if err != nil { | ||||
| 		log.Error("Error extracting rate", sl.Err(err)) | ||||
| 		return 0, 0, fmt.Errorf("%s: %w", op, err) | ||||
| 	} | ||||
| 
 | ||||
| 	convertedAmount = uint32(float32(amount) * rate.Rate) | ||||
| 
 | ||||
| 	return convertedAmount, rate.Rate, nil | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user