mirror of
https://github.com/netscrawler/changeAPI.git
synced 2026-09-05 08:29:36 +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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
type VunitRate struct {
|
||||
Currency string
|
||||
Rate float32
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
//}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user