This commit is contained in:
netscrawler
2024-07-04 00:44:15 +03:00
parent c99f477e35
commit 5b11756af6
15 changed files with 354 additions and 23 deletions
+1
View File
@@ -16,6 +16,7 @@ func New(
tokenTTl time.Duration) *App {
//TODO: инициализировать хранилище
//TODO: init convert service
grpcApp := grpcapp.New(log, grpcPort)
return &App{
GRPCSrv: grpcApp,
+3 -2
View File
@@ -2,6 +2,7 @@ package grpcapp
import (
convertgrpc "converter/internal/grpc/cnvrt"
"converter/internal/services/converter"
"fmt"
"google.golang.org/grpc"
"log/slog"
@@ -18,8 +19,8 @@ func New(
log *slog.Logger,
port int) *App {
gRPCServer := grpc.NewServer()
convertgrpc.Register(gRPCServer)
convert := converter.New(log)
convertgrpc.Register(gRPCServer, convert)
return &App{
log: log,
+7
View File
@@ -11,6 +11,7 @@ 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 {
@@ -18,6 +19,12 @@ type GRPCConfig struct {
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 == "" {
+6
View File
@@ -0,0 +1,6 @@
package models
type VunitRate struct {
Currency string
Rate float32
}
+42 -5
View File
@@ -2,21 +2,58 @@ package cnvrt
import (
"context"
cnvrtv1 "github.com/netscrawler/protos/gen/go/changeAPI"
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) {
cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{})
func Register(gRPC *grpc.Server, convert Converter) {
cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{convert: convert})
}
func (s serverAPI) Convert(
func (s *serverAPI) Convert(
ctx context.Context,
req *cnvrtv1.ConvertRequest) (
*cnvrtv1.ConvertResponse, error) {
panic("implement me")
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
}
+177
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()))
//}
+48
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
}