diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -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
diff --git a/.idea/changeAPI.iml b/.idea/changeAPI.iml
new file mode 100644
index 0000000..728392f
--- /dev/null
+++ b/.idea/changeAPI.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..fdcfa42
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..8e4e17d
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cmd/converter/main.go b/cmd/converter/main.go
new file mode 100644
index 0000000..b0ed0d3
--- /dev/null
+++ b/cmd/converter/main.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "converter/internal/app"
+ "converter/internal/config"
+ "converter/internal/lib/logger/handlers/slogpretty"
+ "fmt"
+ "log/slog"
+ "os"
+)
+
+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)
+ applicaton.GRPCSrv.MustRun()
+ // TODO: инициализировать приложение app
+
+ // TODO: запустить gRPC сервер приложения
+}
+
+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)
+}
diff --git a/config/local.yaml b/config/local.yaml
new file mode 100644
index 0000000..1db89b1
--- /dev/null
+++ b/config/local.yaml
@@ -0,0 +1,5 @@
+env: "local"
+token_ttl: 1h
+grpc:
+ port: 44044
+ timeout: 10h
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..2bce6cc
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,25 @@
+module converter
+
+go 1.22
+
+require (
+ github.com/netscrawler/protos v0.0.5
+ google.golang.org/grpc v1.64.0
+)
+
+require (
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/fatih/color v1.17.0 // indirect
+ github.com/ilyakaznacheev/cleanenv v1.5.0 // 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
+ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // 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.33.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..aeb3402
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,38 @@
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+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/protos v0.0.0-20240629165054-1454eca14db0 h1:YKByh7dtXFBif3xemj/OFyWbAcw3j/M8tD4U4XF229A=
+github.com/netscrawler/protos v0.0.0-20240629165054-1454eca14db0/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc=
+github.com/netscrawler/protos v0.0.4 h1:D+Cb4cELT9YSIyUsyXL233Zb90ClzOR290aE4i+KI8Y=
+github.com/netscrawler/protos v0.0.4/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc=
+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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+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=
diff --git a/internal/app/app.go b/internal/app/app.go
new file mode 100644
index 0000000..a4cb23b
--- /dev/null
+++ b/internal/app/app.go
@@ -0,0 +1,23 @@
+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,
+ }
+}
diff --git a/internal/app/grpc/app.go b/internal/app/grpc/app.go
new file mode 100644
index 0000000..bdf25d2
--- /dev/null
+++ b/internal/app/grpc/app.go
@@ -0,0 +1,62 @@
+package grpcapp
+
+import (
+ convertgrpc "converter/internal/grpc/cnvrt"
+ "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()
+
+ convertgrpc.Register(gRPCServer)
+
+ 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()
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..565af1b
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,52 @@
+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"`
+}
+
+type GRPCConfig struct {
+ Port int `yaml:"port"`
+ Timeout time.Duration `yaml:"timeout"`
+}
+
+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
+}
diff --git a/internal/grpc/cnvrt/server.go b/internal/grpc/cnvrt/server.go
new file mode 100644
index 0000000..ab62b44
--- /dev/null
+++ b/internal/grpc/cnvrt/server.go
@@ -0,0 +1,22 @@
+package cnvrt
+
+import (
+ "context"
+ cnvrtv1 "github.com/netscrawler/protos/gen/go/changeAPI"
+ "google.golang.org/grpc"
+)
+
+type serverAPI struct {
+ cnvrtv1.UnimplementedConverterServer
+}
+
+func Register(gRPC *grpc.Server) {
+ cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{})
+}
+
+func (s serverAPI) Convert(
+ ctx context.Context,
+ req *cnvrtv1.ConvertRequest) (
+ *cnvrtv1.ConvertResponse, error) {
+ panic("implement me")
+}
diff --git a/internal/lib/logger/handlers/slogdiscard/slogdiscard.go b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go
new file mode 100644
index 0000000..1488376
--- /dev/null
+++ b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go
@@ -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
+}
diff --git a/internal/lib/logger/handlers/slogpretty/slogpretty.go b/internal/lib/logger/handlers/slogpretty/slogpretty.go
new file mode 100644
index 0000000..f298f2f
--- /dev/null
+++ b/internal/lib/logger/handlers/slogpretty/slogpretty.go
@@ -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,
+ }
+}
diff --git a/internal/lib/logger/sl/sl.go b/internal/lib/logger/sl/sl.go
new file mode 100644
index 0000000..b9fa762
--- /dev/null
+++ b/internal/lib/logger/sl/sl.go
@@ -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()),
+ }
+}