Squashed commit of the following:

commit 5b11756af6
Author: netscrawler <mariarthyjamesoo@gmail.com>
Date:   Thu Jul 4 00:44:15 2024 +0300

    v0.1

commit c99f477e35
Author: netscrawler <mariarthyjamesoo@gmail.com>
Date:   Sun Jun 30 20:44:53 2024 +0300

    v0.0.6
This commit is contained in:
netscrawler 2024-07-04 00:45:11 +03:00
parent 61d2be39f1
commit 3085eb993a
19 changed files with 799 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -0,0 +1,6 @@
package models
type VunitRate struct {
Currency string
Rate float32
}

View 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
}

View 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
}

View 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,
}
}

View 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()),
}
}

View 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()))
//}

View 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
}