Compare commits
22 Commits
master
...
15d4cdb047
| Author | SHA1 | Date | |
|---|---|---|---|
| 15d4cdb047 | |||
| 4533b90e4a | |||
| 60522a7391 | |||
| 5d5e152429 | |||
| 08be7de118 | |||
| 6042ca6822 | |||
| b0a185561d | |||
| fae5a84182 | |||
| bbe1271620 | |||
| ff4f3b1d4c | |||
| a14212702c | |||
| cc48e97d40 | |||
| bd135dd3a5 | |||
| 1767349a08 | |||
| 38e04d58c2 | |||
| 91b0dd5c4f | |||
| 3754441492 | |||
| 9ef5f2bbf8 | |||
| c814f27c54 | |||
| bfa105df95 | |||
| 90a7797a27 | |||
| 6fe250896c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,4 +4,3 @@ database
|
|||||||
bin
|
bin
|
||||||
*.xml
|
*.xml
|
||||||
Makefile
|
Makefile
|
||||||
config.toml
|
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
namespace internal.encoding.fbs;
|
namespace internal.encoding.fbs;
|
||||||
|
|
||||||
struct Dimensions {
|
|
||||||
width:float;
|
|
||||||
height:float;
|
|
||||||
length:float;
|
|
||||||
}
|
|
||||||
|
|
||||||
table GoodItem {
|
table GoodItem {
|
||||||
sku:string;
|
sku:string;
|
||||||
photo:string;
|
photo:string;
|
||||||
@ -21,8 +15,6 @@ table GoodItem {
|
|||||||
cart:long;
|
cart:long;
|
||||||
stock:short;
|
stock:short;
|
||||||
parameters:string;
|
parameters:string;
|
||||||
created_at:long;
|
|
||||||
sizes:Dimensions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table GoodItems {
|
table GoodItems {
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/cmd/cli/components"
|
|
||||||
|
|
||||||
"github.com/rodaine/table"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CategoriesCommandTree() *cli.Command {
|
|
||||||
var h categoriesHandlers
|
|
||||||
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "categories",
|
|
||||||
Usage: "Interact with stored categories",
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
newCategoriesListCommand(h),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCategoriesListCommand(h categoriesHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "list",
|
|
||||||
Usage: "List categories, stories in db",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "limit",
|
|
||||||
Usage: "limits output to selected items",
|
|
||||||
Value: 20,
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "page",
|
|
||||||
Usage: "in case of limit, selects page",
|
|
||||||
Value: 0,
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "with-total",
|
|
||||||
Usage: "prints total count of categories",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.List)
|
|
||||||
}
|
|
||||||
|
|
||||||
type categoriesHandlers struct{}
|
|
||||||
|
|
||||||
func (categoriesHandlers) List(ctx context.Context, c *cli.Command) error {
|
|
||||||
r, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
categories, err := r.Category().List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("listing categories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := int(c.Int("limit"))
|
|
||||||
page := int(c.Int("page"))
|
|
||||||
total := len(categories)
|
|
||||||
|
|
||||||
if page == 0 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit > 0 {
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
if offset > len(categories) {
|
|
||||||
offset = len(categories) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
limit = offset + limit
|
|
||||||
if limit > len(categories) {
|
|
||||||
limit = len(categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = categories[offset:limit]
|
|
||||||
}
|
|
||||||
|
|
||||||
tbl := table.New("ID", "Name")
|
|
||||||
for _, category := range categories {
|
|
||||||
if category.ID == 0 && category.Name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tbl.AddRow(category.ID, category.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
tbl.Print()
|
|
||||||
|
|
||||||
if c.Bool("with-total") {
|
|
||||||
zerolog.Ctx(ctx).Info().Int("count", total).Msg("total categories stats")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type chanIter[T any] struct {
|
|
||||||
in <-chan T
|
|
||||||
err error
|
|
||||||
next T
|
|
||||||
}
|
|
||||||
|
|
||||||
var errChannelClosed = errors.New("channel closed")
|
|
||||||
|
|
||||||
func (i *chanIter[T]) Next() (ok bool) {
|
|
||||||
if i.err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
i.next, ok = <-i.in
|
|
||||||
if !ok {
|
|
||||||
i.err = errChannelClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *chanIter[T]) Get() T {
|
|
||||||
return i.next
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *chanIter[T]) Err() error {
|
|
||||||
if errors.Is(i.err, errChannelClosed) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return i.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *chanIter[T]) Close() {
|
|
||||||
for range i.in {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getItemsIter(ctx context.Context, r entity.GoodsItemRepository) *chanIter[entity.GoodsItem] {
|
|
||||||
in, err := r.ListIter(ctx, 3)
|
|
||||||
|
|
||||||
return &chanIter[entity.GoodsItem]{
|
|
||||||
in: in,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/dimension"
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dimensionDispatcher struct {
|
|
||||||
m *dimension.Matcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickFirst[T, V any](t T, v V) T {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d dimensionDispatcher) isDimensionParam(value string) bool {
|
|
||||||
return pickFirst(d.m.Match(value)) != dimension.MatchResultMiss
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d dimensionDispatcher) applyDimensionValue(ctx context.Context, key, value string, in *entity.GoodsItemSize) (updated bool) {
|
|
||||||
matchResult, priority := d.m.Match(key)
|
|
||||||
if matchResult == dimension.MatchResultMiss {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log := zerolog.Ctx(ctx).With().Str("key", key).Str("value", value).Logger()
|
|
||||||
|
|
||||||
if strings.Contains(value, "/") {
|
|
||||||
dimensionValues := strings.Split(value, "/")
|
|
||||||
for _, dv := range dimensionValues {
|
|
||||||
updated = updated || d.applyDimensionValue(ctx, key, dv, in)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out, err := entity.ParseDimention(value, entity.DimensionLocalRU)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("unable to parse key, skipping")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
out = out.AdjustTo(entity.DimensionKindCentimeter)
|
|
||||||
|
|
||||||
switch matchResult {
|
|
||||||
case dimension.MatchResultHeight:
|
|
||||||
if !priority && in.Height.Value != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
in.Height = out
|
|
||||||
case dimension.MatchResultLength:
|
|
||||||
if !priority && in.Length.Value != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
in.Length = out
|
|
||||||
case dimension.MatchResultWidth:
|
|
||||||
if !priority && in.Width.Value != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
in.Width = out
|
|
||||||
case dimension.MatchResultDepth:
|
|
||||||
in.UnmatchedDepth = out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@ -1,353 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/cmd/cli/components"
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
"git.loyso.art/frx/eway/internal/export"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExportCommandTree() *cli.Command {
|
|
||||||
var h exportHandlers
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "export",
|
|
||||||
Usage: "Provide actions to export data from the database",
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
newExportYMLCatalogCommand(h),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newExportYMLCatalogCommand(h exportHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "yml-catalog",
|
|
||||||
Usage: "Export data as yml_catalog",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "out",
|
|
||||||
Aliases: []string{"o"},
|
|
||||||
Usage: "Output to file or stdout/stderr",
|
|
||||||
Value: "yml_catalog.xml",
|
|
||||||
TakesFile: true,
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "limit",
|
|
||||||
Aliases: []string{"l"},
|
|
||||||
Usage: "limits amount of items to save",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "pretty",
|
|
||||||
Aliases: []string{"p"},
|
|
||||||
Usage: "pretty-prints output",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.YMLCatalog)
|
|
||||||
}
|
|
||||||
|
|
||||||
type exportHandlers struct{}
|
|
||||||
|
|
||||||
func (h exportHandlers) YMLCatalog(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
const defaultCurrency = "RUR"
|
|
||||||
|
|
||||||
log := zerolog.Ctx(ctx)
|
|
||||||
|
|
||||||
path := cmd.String("out")
|
|
||||||
limit := cmd.Int("limit")
|
|
||||||
pretty := cmd.Bool("pretty")
|
|
||||||
|
|
||||||
r, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().
|
|
||||||
Str("path", path).
|
|
||||||
Int64("limit", limit).
|
|
||||||
Bool("pretty", pretty).
|
|
||||||
Msg("executing with parameters")
|
|
||||||
|
|
||||||
categories, err := r.Category().List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("listing categories: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
shop := export.Shop{
|
|
||||||
Currencies: []export.Currency{{
|
|
||||||
ID: defaultCurrency,
|
|
||||||
Rate: 1,
|
|
||||||
}},
|
|
||||||
Categories: make([]export.Category, 0, len(categories)),
|
|
||||||
}
|
|
||||||
|
|
||||||
categoryByNameIdx := make(map[string]int64, len(categories))
|
|
||||||
for _, category := range categories {
|
|
||||||
categoryByNameIdx[category.Name] = category.ID
|
|
||||||
shop.Categories = append(shop.Categories, export.Category{
|
|
||||||
ID: category.ID,
|
|
||||||
Name: category.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher, err := components.GetDimensionMatcher()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting dimension matcher: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dimensionDispatcher := dimensionDispatcher{m: matcher}
|
|
||||||
|
|
||||||
log.Info().Any("patterns", matcher.GetRegisteredPatterns()).Msg("configured patterns")
|
|
||||||
|
|
||||||
const (
|
|
||||||
reasonNoSize = "no_size"
|
|
||||||
reasonTooLarge = "too_large"
|
|
||||||
reasonNoDescription = "no_description"
|
|
||||||
reasonNoPhoto = "no_photo"
|
|
||||||
)
|
|
||||||
|
|
||||||
skippedByReasons := map[string]int{
|
|
||||||
reasonNoSize: 0,
|
|
||||||
reasonTooLarge: 0,
|
|
||||||
reasonNoDescription: 0,
|
|
||||||
reasonNoPhoto: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
addToSkip := func(condition bool, name string, log zerolog.Logger) bool {
|
|
||||||
if !condition {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("reason", name).Msg("skipping item")
|
|
||||||
skippedByReasons[name]++
|
|
||||||
|
|
||||||
return condition
|
|
||||||
}
|
|
||||||
iter := getItemsIter(ctx, r.GoodsItem())
|
|
||||||
for iter.Next() {
|
|
||||||
const maximumAllowedSizes = 160
|
|
||||||
|
|
||||||
item := iter.Get()
|
|
||||||
sublog := log.With().Int64("cart", item.Cart).Logger()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case addToSkip(item.Description == "", reasonNoDescription, sublog):
|
|
||||||
continue
|
|
||||||
case addToSkip(!item.Sizes.AllSizesSet(), reasonNoSize, sublog):
|
|
||||||
continue
|
|
||||||
case addToSkip(item.Sizes.GetSum(entity.DimensionKindCentimeter) > maximumAllowedSizes, reasonTooLarge, sublog):
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
offer := h.goodsItemAsOffer(item, categoryByNameIdx, dimensionDispatcher, sublog)
|
|
||||||
|
|
||||||
if addToSkip(len(offer.PictureURLs) == 0, reasonNoPhoto, sublog) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
shop.Offers = append(shop.Offers, offer)
|
|
||||||
}
|
|
||||||
if err = iter.Err(); err != nil {
|
|
||||||
return fmt.Errorf("iterating over items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var f io.WriteCloser
|
|
||||||
switch path {
|
|
||||||
case "stdout":
|
|
||||||
f = os.Stdout
|
|
||||||
case "stderr":
|
|
||||||
f = os.Stderr
|
|
||||||
default:
|
|
||||||
f, err = os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating file: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
errClose := f.Close()
|
|
||||||
if err == nil {
|
|
||||||
err = errClose
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Err(errClose).Msg("file closed or not")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if limit > 0 {
|
|
||||||
shop.Offers = shop.Offers[:limit]
|
|
||||||
}
|
|
||||||
|
|
||||||
container := export.YmlContainer{
|
|
||||||
YmlCatalog: export.YmlCatalog{
|
|
||||||
Shop: shop,
|
|
||||||
Date: time.Now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Write([]byte(xml.Header))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("writing header: %w", err)
|
|
||||||
}
|
|
||||||
_, err = f.Write([]byte("<!DOCTYPE yml_catalog SYSTEM \"shops.dtd\">\n"))
|
|
||||||
enc := xml.NewEncoder(f)
|
|
||||||
if pretty {
|
|
||||||
enc.Indent("", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Int("processed", len(shop.Offers)).Any("skipped", skippedByReasons).Msg("completed")
|
|
||||||
|
|
||||||
return enc.Encode(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h exportHandlers) goodsItemAsOffer(in entity.GoodsItem, categoryIDByName map[string]int64, d dimensionDispatcher, log zerolog.Logger) (out export.Offer) {
|
|
||||||
const defaultType = "vendor.model"
|
|
||||||
const defaultCurrency = "RUR"
|
|
||||||
const defaultAvailable = true
|
|
||||||
const quantityParamName = "Количество на складе «Москва»"
|
|
||||||
const basePictureURL = "https://eway.elevel.ru"
|
|
||||||
|
|
||||||
imgurl := func(path string) string {
|
|
||||||
return basePictureURL + path
|
|
||||||
}
|
|
||||||
|
|
||||||
categoryID := categoryIDByName[in.Type]
|
|
||||||
|
|
||||||
pictureURLs := make([]string, 0, len(in.PhotoURLs))
|
|
||||||
for _, url := range in.PhotoURLs {
|
|
||||||
if url == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
outurl := imgurl(url)
|
|
||||||
if outurl == basePictureURL {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pictureURLs = append(pictureURLs, imgurl(url))
|
|
||||||
}
|
|
||||||
|
|
||||||
params := make([]export.Param, 0, len(in.Parameters))
|
|
||||||
for k, v := range in.Parameters {
|
|
||||||
if k == "" || v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.isDimensionParam(k) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
params = append(params, export.Param{
|
|
||||||
Name: k,
|
|
||||||
Value: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
params = append(params, export.Param{
|
|
||||||
Name: quantityParamName,
|
|
||||||
Value: strconv.Itoa(in.Stock),
|
|
||||||
})
|
|
||||||
|
|
||||||
dimensions := h.formatSizeAsDimensions(in.Sizes, log)
|
|
||||||
out = export.Offer{
|
|
||||||
ID: in.Cart,
|
|
||||||
VendorCode: in.Articul,
|
|
||||||
Price: int(in.Price),
|
|
||||||
Model: in.Name,
|
|
||||||
Vendor: in.Producer,
|
|
||||||
TypePrefix: in.Name,
|
|
||||||
Description: in.Description,
|
|
||||||
Dimensions: dimensions,
|
|
||||||
|
|
||||||
CategoryID: categoryID,
|
|
||||||
PictureURLs: pictureURLs,
|
|
||||||
Params: params,
|
|
||||||
|
|
||||||
Type: defaultType,
|
|
||||||
CurrencyID: defaultCurrency,
|
|
||||||
Available: defaultAvailable,
|
|
||||||
ManufacturerWarrany: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// check cart id 126584
|
|
||||||
func (exportHandlers) formatSizeAsDimensions(size entity.GoodsItemSize, log zerolog.Logger) string {
|
|
||||||
const delimeter = "/"
|
|
||||||
makeFloat := func(d entity.Dimension) string {
|
|
||||||
value := d.AdjustTo(entity.DimensionKindCentimeter).Value
|
|
||||||
value = float64(int(value*100)) / 100.0
|
|
||||||
|
|
||||||
return strconv.FormatFloat(value, 'f', 1, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
knownSizes := make([]entity.Dimension, 0, 3)
|
|
||||||
|
|
||||||
dimensions := []entity.Dimension{
|
|
||||||
size.Length,
|
|
||||||
size.Width,
|
|
||||||
size.Height,
|
|
||||||
}
|
|
||||||
for i, d := range dimensions {
|
|
||||||
if d.IsZero() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
knownSizes = append(knownSizes, dimensions[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
l := makeFloat(size.Length)
|
|
||||||
w := makeFloat(size.Width)
|
|
||||||
h := makeFloat(size.Height)
|
|
||||||
switch len(knownSizes) {
|
|
||||||
case 3:
|
|
||||||
// go on
|
|
||||||
case 2:
|
|
||||||
var side string
|
|
||||||
unknownDefaultSize := makeFloat(entity.NewCentimeterDimensionOrEmpty(30))
|
|
||||||
switch {
|
|
||||||
case size.Length.IsZero():
|
|
||||||
side = "length"
|
|
||||||
l = unknownDefaultSize
|
|
||||||
case size.Width.IsZero():
|
|
||||||
side = "width"
|
|
||||||
w = unknownDefaultSize
|
|
||||||
case size.Height.IsZero():
|
|
||||||
side = "height"
|
|
||||||
h = unknownDefaultSize
|
|
||||||
}
|
|
||||||
log.Debug().Str("size", side).Msg("setting to default value")
|
|
||||||
case 1:
|
|
||||||
var side string
|
|
||||||
unknownDefaultSize := makeFloat(entity.NewCentimeterDimensionOrEmpty(30))
|
|
||||||
switch {
|
|
||||||
case !size.Length.IsZero():
|
|
||||||
side = "width"
|
|
||||||
w = unknownDefaultSize
|
|
||||||
case !size.Width.IsZero():
|
|
||||||
side = "length"
|
|
||||||
l = unknownDefaultSize
|
|
||||||
case !size.Height.IsZero():
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
log.Debug().Str("size", side).Msg("setting to default value")
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output should be the following format:
|
|
||||||
// length/width/height in centimeters
|
|
||||||
return strings.Join([]string{
|
|
||||||
l, w, h,
|
|
||||||
}, delimeter)
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/cmd/cli/components"
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type empty entity.Empty
|
|
||||||
|
|
||||||
type action func(ctx context.Context, c *cli.Command) error
|
|
||||||
|
|
||||||
func decorateAction(a action) cli.ActionFunc {
|
|
||||||
return func(ctx context.Context, c *cli.Command) error {
|
|
||||||
var data [3]byte
|
|
||||||
_, _ = rand.Read(data[:])
|
|
||||||
reqid := hex.EncodeToString(data[:])
|
|
||||||
|
|
||||||
log, err := components.GetLogger()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting logger: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log = log.With().Str("reqid", reqid).Logger()
|
|
||||||
rctx := log.WithContext(ctx)
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
defer func() {
|
|
||||||
log.Info().Float64("elapsed", time.Since(start).Seconds()).Msg("command completed")
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Info().Msg("command execution started")
|
|
||||||
return a(rctx, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdWithAction(cmd cli.Command, a action) *cli.Command {
|
|
||||||
if a == nil {
|
|
||||||
a = notImplementedAction
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Action = decorateAction(a)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func notImplementedAction(_ context.Context, _ *cli.Command) error {
|
|
||||||
return entity.ErrNotImplemented
|
|
||||||
}
|
|
||||||
@ -1,385 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/cmd/cli/components"
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
"git.loyso.art/frx/eway/internal/matcher"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ItemsCommandTree() *cli.Command {
|
|
||||||
var h itemsHandlers
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "items",
|
|
||||||
Usage: "Interact with items stored inside db",
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
newItemsGetCommand(h),
|
|
||||||
newItemsCountCommand(h),
|
|
||||||
newItemsUniqueParamsCommand(h),
|
|
||||||
newItemsAggregateParametersCommand(h),
|
|
||||||
newItemsFixSizesCommand(h),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newItemsGetCommand(h itemsHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "get",
|
|
||||||
Usage: "gets goods item by its id",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "id",
|
|
||||||
Usage: "id of the goods item. Either id or cart-id should be set",
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "cart-id",
|
|
||||||
Usage: "cart-id of the item. Either cart-id or id should be set",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.Get)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newItemsCountCommand(h itemsHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "count",
|
|
||||||
Usage: "iterates over collection and counts number of items",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "param-key-match",
|
|
||||||
Usage: "filters by parameters with AND logic",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.Count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newItemsUniqueParamsCommand(h itemsHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "unique-params",
|
|
||||||
Usage: "Show all stored unique param values",
|
|
||||||
Description: "This command iterates over each item and collect keys of params in a dict and then" +
|
|
||||||
" print it to the output. It's useful to find all unique parameters",
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.UniqueParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newItemsAggregateParametersCommand(h itemsHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "aggregate-parameters",
|
|
||||||
Usage: "Show all values of requested parameters",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "case-insensitive",
|
|
||||||
Usage: "Ignores cases of keys",
|
|
||||||
},
|
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "regex",
|
|
||||||
Usage: "Registers regex to match",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "keys-only",
|
|
||||||
Usage: "prints only keys",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.AggregateParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newItemsFixSizesCommand(h itemsHandlers) *cli.Command {
|
|
||||||
cmd := cli.Command{
|
|
||||||
Name: "fix-sizes",
|
|
||||||
Usage: "Iterates over params and sets sizes from parameters",
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmdWithAction(cmd, h.FixSizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
type itemsHandlers struct{}
|
|
||||||
|
|
||||||
func (itemsHandlers) Get(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
r, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := cmd.String("id")
|
|
||||||
cartID := cmd.Int("cart-id")
|
|
||||||
if id == "" && cartID == 0 {
|
|
||||||
return cli.Exit("oneof: id or cart-id should be set", 1)
|
|
||||||
} else if id != "" && cartID != 0 {
|
|
||||||
return cli.Exit("oneof: id or cart-id should be set", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var item entity.GoodsItem
|
|
||||||
if id != "" {
|
|
||||||
item, err = r.GoodsItem().Get(ctx, id)
|
|
||||||
} else {
|
|
||||||
item, err = r.GoodsItem().GetByCart(ctx, cartID)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
|
|
||||||
err = enc.Encode(item)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("encoding item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (itemsHandlers) Count(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
r, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filters := cmd.StringSlice("param-key-match")
|
|
||||||
m := matcher.NewRadix()
|
|
||||||
patternMapped := make(map[string]empty, len(filters))
|
|
||||||
if len(filters) == 0 {
|
|
||||||
m.Register("*")
|
|
||||||
} else {
|
|
||||||
for _, f := range filters {
|
|
||||||
m.Register(f)
|
|
||||||
}
|
|
||||||
for _, pattern := range m.Patterns() {
|
|
||||||
patternMapped[pattern] = empty{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int
|
|
||||||
items, err := r.GoodsItem().List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting items: %w", err)
|
|
||||||
}
|
|
||||||
for _, item := range items {
|
|
||||||
seenPatterns := map[string]empty{}
|
|
||||||
|
|
||||||
for k := range item.Parameters {
|
|
||||||
pattern := m.MatchByPattern(k)
|
|
||||||
if pattern == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seenPatterns[pattern]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenPatterns[pattern] = empty{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(seenPatterns) == len(patternMapped) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (itemsHandlers) UniqueParameters(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
repository, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
knownParams := map[string]empty{}
|
|
||||||
iter, err := repository.GoodsItem().ListIter(ctx, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting list iter: %w", err)
|
|
||||||
}
|
|
||||||
for item := range iter {
|
|
||||||
for k := range item.Parameters {
|
|
||||||
knownParams[k] = empty{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bw := bufio.NewWriter(cmd.Writer)
|
|
||||||
for paramName := range knownParams {
|
|
||||||
_, err = bw.WriteString(paramName + "\n")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to write: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (itemsHandlers) AggregateParameters(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
repository, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
log, err := components.GetLogger()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting logger: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := cmd.Args().Slice()
|
|
||||||
opts := make([]matcher.RadixOpt, 0, 1)
|
|
||||||
if cmd.Bool("case-insensitive") {
|
|
||||||
opts = append(opts, matcher.RadixCaseInsensitive())
|
|
||||||
}
|
|
||||||
|
|
||||||
m := matcher.NewRadix(opts...)
|
|
||||||
for _, param := range params {
|
|
||||||
log.Debug().Str("param", param).Msg("registering param")
|
|
||||||
m.Register(param)
|
|
||||||
}
|
|
||||||
for _, regexp := range cmd.StringSlice("regex") {
|
|
||||||
log.Debug().Str("regexp", regexp).Msg("registering regexp")
|
|
||||||
m.RegisterRegexp(regexp)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedValues := make(map[string]map[string]empty, len(params))
|
|
||||||
requestedValuesByPattern := make(map[string]map[string]empty, len(params))
|
|
||||||
iter := getItemsIter(ctx, repository.GoodsItem())
|
|
||||||
for iter.Next() {
|
|
||||||
item := iter.Get()
|
|
||||||
for k, v := range item.Parameters {
|
|
||||||
matchedPattern := m.MatchByPattern(k)
|
|
||||||
if matchedPattern == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
values, ok := requestedValues[k]
|
|
||||||
if !ok {
|
|
||||||
values = make(map[string]empty)
|
|
||||||
}
|
|
||||||
values[v] = empty{}
|
|
||||||
requestedValues[k] = values
|
|
||||||
|
|
||||||
values, ok = requestedValuesByPattern[matchedPattern]
|
|
||||||
if !ok {
|
|
||||||
values = map[string]empty{}
|
|
||||||
}
|
|
||||||
values[v] = empty{}
|
|
||||||
requestedValuesByPattern[matchedPattern] = values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bw := bufio.NewWriter(cmd.Writer)
|
|
||||||
_, _ = bw.WriteString("Matches:\n")
|
|
||||||
|
|
||||||
if cmd.Bool("keys-only") {
|
|
||||||
for k := range requestedValues {
|
|
||||||
_, _ = bw.WriteString(k + "\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for k, v := range requestedValues {
|
|
||||||
_, _ = bw.WriteString(k + ": ")
|
|
||||||
values := make([]string, 0, len(v))
|
|
||||||
for item := range v {
|
|
||||||
values = append(values, strconv.Quote(item))
|
|
||||||
}
|
|
||||||
valuesStr := "[" + strings.Join(values, ",") + "]\n"
|
|
||||||
_, _ = bw.WriteString(valuesStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = bw.WriteString("\nPatterns:\n")
|
|
||||||
for _, pattern := range m.Patterns() {
|
|
||||||
_, _ = bw.WriteString(pattern + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return bw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (itemsHandlers) FixSizes(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
repository, err := components.GetRepository()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher, err := components.GetDimensionMatcher()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting dimension matcher: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dimensionDispatcher := dimensionDispatcher{m: matcher}
|
|
||||||
|
|
||||||
log := zerolog.Ctx(ctx)
|
|
||||||
|
|
||||||
toUpdate := make([]entity.GoodsItem, 0, 20_000)
|
|
||||||
bus := getItemsIter(ctx, repository.GoodsItem())
|
|
||||||
for bus.Next() {
|
|
||||||
item := bus.Get()
|
|
||||||
|
|
||||||
var valueBeenUpdated bool
|
|
||||||
for key, value := range item.Parameters {
|
|
||||||
trimmedValue := strings.TrimSpace(value)
|
|
||||||
trimmedKey := strings.TrimSpace(key)
|
|
||||||
|
|
||||||
if trimmedKey != key || trimmedValue != value {
|
|
||||||
log.Warn().
|
|
||||||
Str("old_key", key).
|
|
||||||
Str("new_key", trimmedKey).
|
|
||||||
Str("old_value", value).
|
|
||||||
Str("new_value", trimmedValue).
|
|
||||||
Msg("found mismatch")
|
|
||||||
|
|
||||||
delete(item.Parameters, key)
|
|
||||||
|
|
||||||
key = trimmedKey
|
|
||||||
value = trimmedValue
|
|
||||||
|
|
||||||
item.Parameters[key] = value
|
|
||||||
valueBeenUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue := strings.HasSuffix(key, ":")
|
|
||||||
if updateValue {
|
|
||||||
key = strings.TrimSuffix(key, ":")
|
|
||||||
item.Parameters[key] = value
|
|
||||||
delete(item.Parameters, key+":")
|
|
||||||
valueBeenUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if dimensionDispatcher.applyDimensionValue(ctx, key, value, &item.Sizes) {
|
|
||||||
valueBeenUpdated = true
|
|
||||||
log.Debug().Str("key", key).Any("sizes", item.Sizes).Msg("been updated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var updated bool
|
|
||||||
oldSizes := item.Sizes
|
|
||||||
item.Sizes, updated = entity.FixupSizes(oldSizes)
|
|
||||||
if updated {
|
|
||||||
log.Info().Int64("cart", item.Cart).Any("old", oldSizes).Any("new", item.Sizes).Msg("sizes been fixed")
|
|
||||||
}
|
|
||||||
|
|
||||||
valueBeenUpdated = valueBeenUpdated || updated
|
|
||||||
|
|
||||||
if valueBeenUpdated {
|
|
||||||
toUpdate = append(toUpdate, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bus.Err() != nil {
|
|
||||||
return fmt.Errorf("iterating: %w", bus.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = repository.GoodsItem().UpsertMany(ctx, toUpdate...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("updating items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Int("count", len(toUpdate)).Msg("updated items")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/config"
|
"git.loyso.art/frx/eway/internal/config"
|
||||||
"git.loyso.art/frx/eway/internal/dimension"
|
|
||||||
"git.loyso.art/frx/eway/internal/interconnect/eway"
|
"git.loyso.art/frx/eway/internal/interconnect/eway"
|
||||||
"git.loyso.art/frx/eway/internal/storage"
|
"git.loyso.art/frx/eway/internal/storage"
|
||||||
xbadger "git.loyso.art/frx/eway/internal/storage/badger"
|
xbadger "git.loyso.art/frx/eway/internal/storage/badger"
|
||||||
@ -37,19 +36,10 @@ func GetRepository() (storage.Repository, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetLogger() (zerolog.Logger, error) {
|
func GetLogger() (zerolog.Logger, error) {
|
||||||
log, err := do.Invoke[*loggerAdapter](diInjector)
|
return do.Invoke[zerolog.Logger](diInjector)
|
||||||
if err != nil {
|
|
||||||
return zerolog.Nop(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.entity.log, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDimensionMatcher() (*dimension.Matcher, error) {
|
func SetupDI(ctx context.Context, cfgpath string) error {
|
||||||
return do.Invoke[*dimension.Matcher](diInjector)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetupDI(ctx context.Context, cfgpath string, verbose bool, logAsJSON bool) error {
|
|
||||||
cfg, err := parseSettings(cfgpath)
|
cfg, err := parseSettings(cfgpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if no settings provided allow cli to run without them.
|
// if no settings provided allow cli to run without them.
|
||||||
@ -62,56 +52,14 @@ func SetupDI(ctx context.Context, cfgpath string, verbose bool, logAsJSON bool)
|
|||||||
|
|
||||||
diInjector = do.New()
|
diInjector = do.New()
|
||||||
|
|
||||||
do.Provide(diInjector, func(i *do.Injector) (*loggerAdapter, error) {
|
do.Provide(diInjector, func(i *do.Injector) (zerolog.Logger, error) {
|
||||||
tsSet := func(wr *zerolog.ConsoleWriter) {
|
tsSet := func(wr *zerolog.ConsoleWriter) {
|
||||||
wr.TimeFormat = time.RFC3339
|
wr.TimeFormat = time.RFC3339
|
||||||
}
|
}
|
||||||
|
|
||||||
var outfile *os.File
|
log := zerolog.New(zerolog.NewConsoleWriter(tsSet)).With().Timestamp().Str("app", "converter").Logger()
|
||||||
var output io.Writer
|
|
||||||
switch cfg.Log.Output {
|
|
||||||
case "", "stdout":
|
|
||||||
output = os.Stdout
|
|
||||||
case "stderr":
|
|
||||||
output = os.Stderr
|
|
||||||
default:
|
|
||||||
outfile, err = os.OpenFile(cfg.Log.Output, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("creating file for logging: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output = zerolog.SyncWriter(outfile)
|
return log, nil
|
||||||
}
|
|
||||||
|
|
||||||
var writer io.Writer
|
|
||||||
if logAsJSON {
|
|
||||||
writer = output
|
|
||||||
} else {
|
|
||||||
writer = zerolog.NewConsoleWriter(tsSet, func(w *zerolog.ConsoleWriter) {
|
|
||||||
w.Out = output
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
log := zerolog.
|
|
||||||
New(writer).
|
|
||||||
With().
|
|
||||||
Timestamp().
|
|
||||||
Str("app", "converter").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
log = log.Level(zerolog.DebugLevel)
|
|
||||||
} else {
|
|
||||||
log = log.Level(zerolog.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := &logger{
|
|
||||||
log: log,
|
|
||||||
underlyingFile: outfile,
|
|
||||||
}
|
|
||||||
return &loggerAdapter{
|
|
||||||
entity: out,
|
|
||||||
}, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
do.Provide[eway.Client](diInjector, func(i *do.Injector) (eway.Client, error) {
|
do.Provide[eway.Client](diInjector, func(i *do.Injector) (eway.Client, error) {
|
||||||
@ -153,12 +101,6 @@ func SetupDI(ctx context.Context, cfgpath string, verbose bool, logAsJSON bool)
|
|||||||
return out, nil
|
return out, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
do.Provide[*dimension.Matcher](diInjector, func(i *do.Injector) (*dimension.Matcher, error) {
|
|
||||||
matcher := dimension.New(cfg.DimensionMatcher)
|
|
||||||
|
|
||||||
return matcher, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,10 +122,9 @@ func getDB() (*badger.DB, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type settings struct {
|
type settings struct {
|
||||||
Badger config.Badger `toml:"badger"`
|
Badger config.Badger `toml:"badger"`
|
||||||
Log config.Log `toml:"log"`
|
Log config.Log `toml:"log"`
|
||||||
Eway config.Eway `toml:"eway"`
|
Eway config.Eway `toml:"eway"`
|
||||||
DimensionMatcher config.DimensionMatcher `toml:"dimension_matcher"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(cfgpath string) (cfg settings, err error) {
|
func parseSettings(cfgpath string) (cfg settings, err error) {
|
||||||
@ -195,19 +136,6 @@ func parseSettings(cfgpath string) (cfg settings, err error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type logger struct {
|
|
||||||
log zerolog.Logger
|
|
||||||
underlyingFile *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logger) Close() error {
|
|
||||||
if l.underlyingFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.underlyingFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type entityCloserAdapter[T io.Closer] struct {
|
type entityCloserAdapter[T io.Closer] struct {
|
||||||
entity T
|
entity T
|
||||||
}
|
}
|
||||||
@ -218,4 +146,3 @@ func (a entityCloserAdapter[T]) Shutdown() error {
|
|||||||
|
|
||||||
type storageRepositoryAdapter entityCloserAdapter[storage.Repository]
|
type storageRepositoryAdapter entityCloserAdapter[storage.Repository]
|
||||||
type badgerDBAdapter entityCloserAdapter[*badger.DB]
|
type badgerDBAdapter entityCloserAdapter[*badger.DB]
|
||||||
type loggerAdapter entityCloserAdapter[*logger]
|
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
"git.loyso.art/frx/eway/internal/matcher"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dimensionDispatcher struct {
|
|
||||||
heigth matcher.Unit
|
|
||||||
width matcher.Unit
|
|
||||||
length matcher.Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d dimensionDispatcher) isDimensionParam(value string) bool {
|
|
||||||
return d.heigth.Match(value) || d.width.Match(value) || d.length.Match(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d dimensionDispatcher) dispatch(ctx context.Context, key, value string, in *entity.GoodsItemSize) (updated bool) {
|
|
||||||
if !d.isDimensionParam(key) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log := zerolog.Ctx(ctx).With().Str("key", key).Str("value", value).Logger()
|
|
||||||
if strings.Contains(value, "/") {
|
|
||||||
dimensionValues := strings.Split(value, "/")
|
|
||||||
for _, dv := range dimensionValues {
|
|
||||||
updated = updated || d.dispatch(ctx, key, dv, in)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out, err := entity.ParseDimention(value, entity.DimensionLocalRU)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("unable to parse key, skipping")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
out = out.AdjustTo(entity.DimensionKindCentimeter)
|
|
||||||
|
|
||||||
updated = true
|
|
||||||
switch {
|
|
||||||
case d.heigth.Match(key):
|
|
||||||
in.Height = out
|
|
||||||
case d.width.Match(key):
|
|
||||||
in.Width = out
|
|
||||||
case d.length.Match(key):
|
|
||||||
in.Length = out
|
|
||||||
default:
|
|
||||||
log.Error().Str("key", key).Msg("unable to find proper matcher")
|
|
||||||
updated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeDefaultDimensionDispatcher() dimensionDispatcher {
|
|
||||||
h := matcher.NewRadix(matcher.RadixCaseInsensitive())
|
|
||||||
h.Register("Высота")
|
|
||||||
h.Register("Высота/*")
|
|
||||||
|
|
||||||
w := matcher.NewRadix(matcher.RadixCaseInsensitive())
|
|
||||||
w.Register("Ширина")
|
|
||||||
w.Register("Ширина/*")
|
|
||||||
|
|
||||||
l := matcher.NewRadix(matcher.RadixCaseInsensitive())
|
|
||||||
l.Register("Длина")
|
|
||||||
l.Register("Длина/*")
|
|
||||||
l.Register("Общ. длина")
|
|
||||||
|
|
||||||
return dimensionDispatcher{
|
|
||||||
heigth: h,
|
|
||||||
width: w,
|
|
||||||
length: l,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
772
cmd/cli/main.go
772
cmd/cli/main.go
File diff suppressed because it is too large
Load Diff
15
config.toml
Normal file
15
config.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[badger]
|
||||||
|
debug = false
|
||||||
|
dir = "database/"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "info"
|
||||||
|
format = "text"
|
||||||
|
|
||||||
|
[eway]
|
||||||
|
login = "leci@yandex.ru"
|
||||||
|
password = "2a136e113854cc5d46b868919a7d6e939156ccb55ff12e87861513f7767af98be79e62407410"
|
||||||
|
_session_id = "19b98ed56cc144f47e040e68dbcd8481"
|
||||||
|
_session_user = "1490"
|
||||||
|
owner_id = "26476"
|
||||||
|
debug = false
|
||||||
17
go.mod
17
go.mod
@ -4,7 +4,6 @@ go 1.21.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0
|
github.com/brianvoe/gofakeit/v6 v6.28.0
|
||||||
github.com/dgraph-io/badger/v4 v4.2.0
|
github.com/dgraph-io/badger/v4 v4.2.0
|
||||||
github.com/dgraph-io/ristretto v0.1.1
|
github.com/dgraph-io/ristretto v0.1.1
|
||||||
@ -13,30 +12,36 @@ require (
|
|||||||
github.com/rodaine/table v1.1.1
|
github.com/rodaine/table v1.1.1
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/samber/do v1.6.0
|
github.com/samber/do v1.6.0
|
||||||
github.com/stretchr/testify v1.8.4
|
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha8
|
github.com/urfave/cli/v3 v3.0.0-alpha8
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||||
|
github.com/antchfx/htmlquery v1.3.0 // indirect
|
||||||
|
github.com/antchfx/xmlquery v1.3.18 // indirect
|
||||||
|
github.com/antchfx/xpath v1.2.4 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/gocolly/colly v1.2.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v1.0.0 // indirect
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/golang/snappy v0.0.3 // indirect
|
github.com/golang/snappy v0.0.3 // indirect
|
||||||
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/klauspost/compress v1.12.3 // indirect
|
github.com/klauspost/compress v1.12.3 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
|
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||||
go.opencensus.io v0.22.5 // indirect
|
go.opencensus.io v0.22.5 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.13.0 // indirect
|
||||||
|
google.golang.org/appengine v1.4.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
32
go.sum
32
go.sum
@ -6,6 +6,13 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc
|
|||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
|
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
||||||
|
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
||||||
|
github.com/antchfx/xmlquery v1.3.18 h1:FSQ3wMuphnPPGJOFhvc+cRQ2CT/rUj4cyQXkJcjOwz0=
|
||||||
|
github.com/antchfx/xmlquery v1.3.18/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA=
|
||||||
|
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
|
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
|
||||||
|
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
@ -13,7 +20,6 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -27,12 +33,17 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
|
|||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
||||||
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||||
|
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -50,14 +61,12 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||||
|
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
|
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
|
||||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
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-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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
@ -76,16 +85,21 @@ github.com/rodaine/table v1.1.1/go.mod h1:iqTRptjn+EVcrVBYtNMlJ2wrJZa3MpULUmcXFp
|
|||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||||
github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU=
|
github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU=
|
||||||
github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k=
|
github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||||
|
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha8 h1:H+qxFPoCkGzdF8KUMs2fEOZl5io/1QySgUiGfar8occ=
|
github.com/urfave/cli/v3 v3.0.0-alpha8 h1:H+qxFPoCkGzdF8KUMs2fEOZl5io/1QySgUiGfar8occ=
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha8/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
|
github.com/urfave/cli/v3 v3.0.0-alpha8/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||||
@ -119,6 +133,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
@ -145,6 +160,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -153,6 +169,7 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
|||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
@ -160,8 +177,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -179,6 +198,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
@ -189,8 +209,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@ -7,5 +7,4 @@ type Eway struct {
|
|||||||
SessionUser string `toml:"session_user"`
|
SessionUser string `toml:"session_user"`
|
||||||
OwnerID string `toml:"owner_id"`
|
OwnerID string `toml:"owner_id"`
|
||||||
Debug bool `toml:"debug"`
|
Debug bool `toml:"debug"`
|
||||||
WorkersPool int `toml:"workers_pool"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,5 +52,4 @@ func (l *LogFormat) UnmarshalText(data []byte) (err error) {
|
|||||||
type Log struct {
|
type Log struct {
|
||||||
Level string `json:"level" toml:"level"`
|
Level string `json:"level" toml:"level"`
|
||||||
Format string `json:"format" toml:"format"`
|
Format string `json:"format" toml:"format"`
|
||||||
Output string `json:"output" toml:"output"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MatcherPredicateType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
MatcherPredicateTypeUnknown MatcherPredicateType = iota
|
|
||||||
MatcherPredicateTypePattern
|
|
||||||
MatcherPredicateTypeRegexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t *MatcherPredicateType) UnmarshalText(data []byte) error {
|
|
||||||
switch dataStr := strings.ToLower(string(data)); dataStr {
|
|
||||||
case "":
|
|
||||||
*t = MatcherPredicateTypeUnknown
|
|
||||||
case "pattern":
|
|
||||||
*t = MatcherPredicateTypePattern
|
|
||||||
case "regexp":
|
|
||||||
*t = MatcherPredicateTypeRegexp
|
|
||||||
default:
|
|
||||||
return errors.New("unsupported type " + dataStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MatcherPredicate struct {
|
|
||||||
Value string `toml:"value"`
|
|
||||||
Type MatcherPredicateType `toml:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DimensionMatcher struct {
|
|
||||||
CaseInsensitive bool `toml:"case_insensitive"`
|
|
||||||
|
|
||||||
Length []MatcherPredicate `toml:"length"`
|
|
||||||
Width []MatcherPredicate `toml:"width"`
|
|
||||||
Height []MatcherPredicate `toml:"height"`
|
|
||||||
}
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
package dimension
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.loyso.art/frx/eway/internal/config"
|
|
||||||
"git.loyso.art/frx/eway/internal/matcher"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MatchResult uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
MatchResultMiss MatchResult = iota
|
|
||||||
MatchResultLength
|
|
||||||
MatchResultHeight
|
|
||||||
MatchResultWidth
|
|
||||||
MatchResultDepth
|
|
||||||
)
|
|
||||||
|
|
||||||
type Matcher struct {
|
|
||||||
length matcher.Unit
|
|
||||||
width matcher.Unit
|
|
||||||
height matcher.Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(cfg config.DimensionMatcher) *Matcher {
|
|
||||||
return &Matcher{
|
|
||||||
length: makeMatcherByConig(cfg.CaseInsensitive, cfg.Length...),
|
|
||||||
width: makeMatcherByConig(cfg.CaseInsensitive, cfg.Width...),
|
|
||||||
height: makeMatcherByConig(cfg.CaseInsensitive, cfg.Height...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeMatcherByConig(insensitive bool, cfgs ...config.MatcherPredicate) matcher.Unit {
|
|
||||||
opts := make([]matcher.RadixOpt, 0, 1)
|
|
||||||
if insensitive {
|
|
||||||
opts = append(opts, matcher.RadixCaseInsensitive())
|
|
||||||
}
|
|
||||||
m := matcher.NewRadix(opts...)
|
|
||||||
|
|
||||||
for _, cfg := range cfgs {
|
|
||||||
switch cfg.Type {
|
|
||||||
case config.MatcherPredicateTypePattern:
|
|
||||||
m.Register(cfg.Value)
|
|
||||||
case config.MatcherPredicateTypeRegexp:
|
|
||||||
m.RegisterRegexp(cfg.Value)
|
|
||||||
default:
|
|
||||||
panic("unsupported matcher type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Matcher) Match(value string) (r MatchResult, priority bool) {
|
|
||||||
switch {
|
|
||||||
case value == "Высота":
|
|
||||||
priority = true
|
|
||||||
fallthrough
|
|
||||||
case m.height.Match(value):
|
|
||||||
return MatchResultHeight, priority
|
|
||||||
case value == "Глубина":
|
|
||||||
priority = true
|
|
||||||
return MatchResultDepth, priority
|
|
||||||
case value == "Длина":
|
|
||||||
priority = true
|
|
||||||
fallthrough
|
|
||||||
case m.length.Match(value):
|
|
||||||
return MatchResultLength, priority
|
|
||||||
case value == "Ширина":
|
|
||||||
priority = true
|
|
||||||
fallthrough
|
|
||||||
case m.width.Match(value):
|
|
||||||
return MatchResultWidth, priority
|
|
||||||
}
|
|
||||||
|
|
||||||
return MatchResultMiss, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Matcher) GetRegisteredPatterns() map[string][]string {
|
|
||||||
out := map[string][]string{
|
|
||||||
"length": nil,
|
|
||||||
"width": nil,
|
|
||||||
"height": nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.height != nil {
|
|
||||||
out["height"] = m.height.Patterns()
|
|
||||||
}
|
|
||||||
if m.width != nil {
|
|
||||||
out["width"] = m.width.Patterns()
|
|
||||||
}
|
|
||||||
if m.length != nil {
|
|
||||||
out["length"] = m.length.Patterns()
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
|
||||||
|
|
||||||
package fbs
|
|
||||||
|
|
||||||
import (
|
|
||||||
flatbuffers "github.com/google/flatbuffers/go"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dimensions struct {
|
|
||||||
_tab flatbuffers.Struct
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *Dimensions) Init(buf []byte, i flatbuffers.UOffsetT) {
|
|
||||||
rcv._tab.Bytes = buf
|
|
||||||
rcv._tab.Pos = i
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *Dimensions) Table() flatbuffers.Table {
|
|
||||||
return rcv._tab.Table
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *Dimensions) Width() float32 {
|
|
||||||
return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(0))
|
|
||||||
}
|
|
||||||
func (rcv *Dimensions) MutateWidth(n float32) bool {
|
|
||||||
return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(0), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *Dimensions) Height() float32 {
|
|
||||||
return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(4))
|
|
||||||
}
|
|
||||||
func (rcv *Dimensions) MutateHeight(n float32) bool {
|
|
||||||
return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(4), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *Dimensions) Length() float32 {
|
|
||||||
return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(8))
|
|
||||||
}
|
|
||||||
func (rcv *Dimensions) MutateLength(n float32) bool {
|
|
||||||
return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(8), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateDimensions(builder *flatbuffers.Builder, width float32, height float32, length float32) flatbuffers.UOffsetT {
|
|
||||||
builder.Prep(4, 12)
|
|
||||||
builder.PrependFloat32(length)
|
|
||||||
builder.PrependFloat32(height)
|
|
||||||
builder.PrependFloat32(width)
|
|
||||||
return builder.Offset()
|
|
||||||
}
|
|
||||||
@ -177,33 +177,8 @@ func (rcv *GoodItem) Parameters() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rcv *GoodItem) CreatedAt() int64 {
|
|
||||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
|
|
||||||
if o != 0 {
|
|
||||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *GoodItem) MutateCreatedAt(n int64) bool {
|
|
||||||
return rcv._tab.MutateInt64Slot(32, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rcv *GoodItem) Sizes(obj *Dimensions) *Dimensions {
|
|
||||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(34))
|
|
||||||
if o != 0 {
|
|
||||||
x := o + rcv._tab.Pos
|
|
||||||
if obj == nil {
|
|
||||||
obj = new(Dimensions)
|
|
||||||
}
|
|
||||||
obj.Init(rcv._tab.Bytes, x)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GoodItemStart(builder *flatbuffers.Builder) {
|
func GoodItemStart(builder *flatbuffers.Builder) {
|
||||||
builder.StartObject(16)
|
builder.StartObject(14)
|
||||||
}
|
}
|
||||||
func GoodItemAddSku(builder *flatbuffers.Builder, sku flatbuffers.UOffsetT) {
|
func GoodItemAddSku(builder *flatbuffers.Builder, sku flatbuffers.UOffsetT) {
|
||||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(sku), 0)
|
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(sku), 0)
|
||||||
@ -247,12 +222,6 @@ func GoodItemAddStock(builder *flatbuffers.Builder, stock int16) {
|
|||||||
func GoodItemAddParameters(builder *flatbuffers.Builder, parameters flatbuffers.UOffsetT) {
|
func GoodItemAddParameters(builder *flatbuffers.Builder, parameters flatbuffers.UOffsetT) {
|
||||||
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(parameters), 0)
|
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(parameters), 0)
|
||||||
}
|
}
|
||||||
func GoodItemAddCreatedAt(builder *flatbuffers.Builder, createdAt int64) {
|
|
||||||
builder.PrependInt64Slot(14, createdAt, 0)
|
|
||||||
}
|
|
||||||
func GoodItemAddSizes(builder *flatbuffers.Builder, sizes flatbuffers.UOffsetT) {
|
|
||||||
builder.PrependStructSlot(15, flatbuffers.UOffsetT(sizes), 0)
|
|
||||||
}
|
|
||||||
func GoodItemEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
func GoodItemEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||||
return builder.EndObject()
|
return builder.EndObject()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
"git.loyso.art/frx/eway/internal/entity"
|
||||||
|
|
||||||
@ -82,13 +81,6 @@ func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatb
|
|||||||
|
|
||||||
producer := builder.CreateString(in.Producer)
|
producer := builder.CreateString(in.Producer)
|
||||||
|
|
||||||
var w, h, l float32
|
|
||||||
if in.Sizes != (entity.GoodsItemSize{}) {
|
|
||||||
w = float32(in.Sizes.Width.AdjustTo(entity.DimensionKindCentimeter).Value)
|
|
||||||
h = float32(in.Sizes.Height.AdjustTo(entity.DimensionKindCentimeter).Value)
|
|
||||||
l = float32(in.Sizes.Length.AdjustTo(entity.DimensionKindCentimeter).Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
GoodItemStart(builder)
|
GoodItemStart(builder)
|
||||||
GoodItemAddSku(builder, sku)
|
GoodItemAddSku(builder, sku)
|
||||||
GoodItemAddPhoto(builder, photo)
|
GoodItemAddPhoto(builder, photo)
|
||||||
@ -106,8 +98,6 @@ func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatb
|
|||||||
GoodItemAddCart(builder, int64(in.Cart))
|
GoodItemAddCart(builder, int64(in.Cart))
|
||||||
GoodItemAddStock(builder, int16(in.Stock))
|
GoodItemAddStock(builder, int16(in.Stock))
|
||||||
GoodItemAddParameters(builder, parameters)
|
GoodItemAddParameters(builder, parameters)
|
||||||
GoodItemAddCreatedAt(builder, in.CreatedAt.Unix())
|
|
||||||
GoodItemAddSizes(builder, CreateDimensions(builder, w, h, l))
|
|
||||||
|
|
||||||
return GoodItemEnd(builder)
|
return GoodItemEnd(builder)
|
||||||
}
|
}
|
||||||
@ -115,10 +105,7 @@ func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatb
|
|||||||
func ParseGoodsItem(data []byte) (item entity.GoodsItem, err error) {
|
func ParseGoodsItem(data []byte) (item entity.GoodsItem, err error) {
|
||||||
itemFBS := GetRootAsGoodItem(data, 0)
|
itemFBS := GetRootAsGoodItem(data, 0)
|
||||||
item.Articul = string(itemFBS.Sku())
|
item.Articul = string(itemFBS.Sku())
|
||||||
photoURLs := string(itemFBS.Photo())
|
item.PhotoURLs = strings.Split(string(itemFBS.Photo()), ";")
|
||||||
if len(photoURLs) > 0 {
|
|
||||||
item.PhotoURLs = strings.Split(photoURLs, ";")
|
|
||||||
}
|
|
||||||
item.Name = string(itemFBS.Name())
|
item.Name = string(itemFBS.Name())
|
||||||
|
|
||||||
description, err := base64.RawStdEncoding.DecodeString(string(itemFBS.Description()))
|
description, err := base64.RawStdEncoding.DecodeString(string(itemFBS.Description()))
|
||||||
@ -147,25 +134,6 @@ func ParseGoodsItem(data []byte) (item entity.GoodsItem, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var w, h, l float64
|
|
||||||
sizes := itemFBS.Sizes(nil)
|
|
||||||
if sizes != nil {
|
|
||||||
w = float64(sizes.Width())
|
|
||||||
h = float64(sizes.Height())
|
|
||||||
l = float64(sizes.Length())
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Sizes = entity.GoodsItemSize{
|
|
||||||
Width: entity.NewCentimeterDimensionOrEmpty(w),
|
|
||||||
Height: entity.NewCentimeterDimensionOrEmpty(h),
|
|
||||||
Length: entity.NewCentimeterDimensionOrEmpty(l),
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAt := itemFBS.CreatedAt()
|
|
||||||
if createdAt > 0 {
|
|
||||||
item.CreatedAt = time.Unix(createdAt, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultLocale = DimensionLocalRU
|
|
||||||
|
|
||||||
type DimensionLocale uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
DimensionLocalUnspecified DimensionLocale = iota
|
|
||||||
DimensionLocalRU
|
|
||||||
|
|
||||||
dimensionLocalEnd
|
|
||||||
)
|
|
||||||
|
|
||||||
type DimensionKind uint8
|
|
||||||
|
|
||||||
func (k DimensionKind) GetPos() float64 {
|
|
||||||
switch k {
|
|
||||||
case DimensionKindMilimeter:
|
|
||||||
return 1
|
|
||||||
case DimensionKindCentimeter:
|
|
||||||
return 10
|
|
||||||
case DimensionKindMeter:
|
|
||||||
return 1000
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k DimensionKind) String() string {
|
|
||||||
m := getLocaleKindToStringMap()[DefaultLocale]
|
|
||||||
return m[k]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DimensionKindUnspecified DimensionKind = iota
|
|
||||||
DimensionKindMilimeter
|
|
||||||
DimensionKindCentimeter
|
|
||||||
DimensionKindMeter
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dimension struct {
|
|
||||||
Value float64
|
|
||||||
Kind DimensionKind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Dimension) IsZero() bool {
|
|
||||||
return d.Value == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Dimension) MarshalText() ([]byte, error) {
|
|
||||||
if d.Value == 0 && d.Kind == DimensionKindUnspecified {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
value := strconv.FormatFloat(d.Value, 'f', 4, 64) + " " + d.Kind.String()
|
|
||||||
return []byte(value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dimension) UnmarshalText(data []byte) (err error) {
|
|
||||||
*d, err = ParseDimention(string(data), DefaultLocale)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Dimension) AdjustTo(kind DimensionKind) Dimension {
|
|
||||||
from := d.Kind.GetPos()
|
|
||||||
to := kind.GetPos()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case from < to:
|
|
||||||
mult := to / from
|
|
||||||
return Dimension{
|
|
||||||
Kind: kind,
|
|
||||||
Value: d.Value / float64(mult),
|
|
||||||
}
|
|
||||||
case from > to:
|
|
||||||
mult := from / to
|
|
||||||
return Dimension{
|
|
||||||
Kind: kind,
|
|
||||||
Value: d.Value * float64(mult),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseDimention(value string, locale DimensionLocale) (Dimension, error) {
|
|
||||||
switch locale {
|
|
||||||
case DimensionLocalRU:
|
|
||||||
default:
|
|
||||||
return Dimension{}, SimpleError("unknown locale for parse")
|
|
||||||
}
|
|
||||||
|
|
||||||
dimensionStrToKind := getLocaleToKindMap()[locale]
|
|
||||||
lastSpaceIdx := strings.LastIndex(value, " ")
|
|
||||||
if lastSpaceIdx == -1 {
|
|
||||||
return Dimension{}, SimpleError("expected 2 values after split for value " + value)
|
|
||||||
}
|
|
||||||
|
|
||||||
var splitted [2]string
|
|
||||||
splitted[0] = strings.ReplaceAll(value[:lastSpaceIdx], " ", "")
|
|
||||||
splitted[1] = value[lastSpaceIdx+1:]
|
|
||||||
|
|
||||||
var out Dimension
|
|
||||||
var ok bool
|
|
||||||
out.Kind, ok = dimensionStrToKind[splitted[1]]
|
|
||||||
if !ok {
|
|
||||||
return Dimension{}, SimpleError("dimension map not found for kind " + splitted[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
out.Value, err = strconv.ParseFloat(splitted[0], 64)
|
|
||||||
if err != nil {
|
|
||||||
return Dimension{}, fmt.Errorf("parsing value: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCentimeterDimensionOrEmpty(value float64) Dimension {
|
|
||||||
if value == 0 {
|
|
||||||
return Dimension{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Dimension{
|
|
||||||
Value: value,
|
|
||||||
Kind: DimensionKindCentimeter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLocaleToKindMap() map[DimensionLocale]map[string]DimensionKind {
|
|
||||||
return map[DimensionLocale]map[string]DimensionKind{
|
|
||||||
DimensionLocalRU: {
|
|
||||||
"мм": DimensionKindMilimeter,
|
|
||||||
"см": DimensionKindCentimeter,
|
|
||||||
"м": DimensionKindMeter,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLocaleKindToStringMap() map[DimensionLocale]map[DimensionKind]string {
|
|
||||||
return map[DimensionLocale]map[DimensionKind]string{
|
|
||||||
DimensionLocalRU: {
|
|
||||||
DimensionKindMilimeter: "мм",
|
|
||||||
DimensionKindCentimeter: "см",
|
|
||||||
DimensionKindMeter: "м",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLocaleMap(t *testing.T) {
|
|
||||||
kindToStr := getLocaleKindToStringMap()
|
|
||||||
strToKind := getLocaleToKindMap()
|
|
||||||
|
|
||||||
assert := assert.New(t)
|
|
||||||
for locale := DimensionLocalUnspecified + 1; locale < dimensionLocalEnd; locale++ {
|
|
||||||
localeKinds, ok := kindToStr[locale]
|
|
||||||
assert.True(ok)
|
|
||||||
localeStrs, ok := strToKind[locale]
|
|
||||||
assert.True(ok)
|
|
||||||
|
|
||||||
assert.Equal(len(localeKinds), len(localeStrs))
|
|
||||||
|
|
||||||
for kindKey, kindValue := range localeKinds {
|
|
||||||
strKey := kindValue
|
|
||||||
strValue, ok := localeStrs[strKey]
|
|
||||||
assert.True(ok)
|
|
||||||
|
|
||||||
assert.Equal(kindKey, strValue)
|
|
||||||
assert.Equal(strKey, kindValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
package entity_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDimension_AdjustTo(t *testing.T) {
|
|
||||||
// Test adjusting from smaller dimension to larger one
|
|
||||||
d := entity.Dimension{Value: 5.0, Kind: entity.DimensionKindCentimeter}
|
|
||||||
expected := entity.Dimension{Value: 0.05, Kind: entity.DimensionKindMeter}
|
|
||||||
actual := d.AdjustTo(entity.DimensionKindMeter)
|
|
||||||
|
|
||||||
assert.EqualValues(t, expected.Value, actual.Value)
|
|
||||||
assert.Equal(t, expected.Kind, actual.Kind)
|
|
||||||
|
|
||||||
// Test adjusting from larger dimension to smaller one
|
|
||||||
d = entity.Dimension{Value: 0.05, Kind: entity.DimensionKindMeter}
|
|
||||||
expected = entity.Dimension{Value: 50.0, Kind: entity.DimensionKindMilimeter}
|
|
||||||
actual = d.AdjustTo(entity.DimensionKindMilimeter)
|
|
||||||
|
|
||||||
assert.EqualValues(t, expected.Value, actual.Value)
|
|
||||||
assert.Equal(t, expected.Kind, actual.Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDimension_Success(t *testing.T) {
|
|
||||||
// Test parsing a valid dimension string with RU locale
|
|
||||||
input := "10 см"
|
|
||||||
expected := entity.Dimension{Value: 10.0, Kind: entity.DimensionKindCentimeter}
|
|
||||||
|
|
||||||
actual, err := entity.ParseDimention(input, entity.DimensionLocalRU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.EqualValues(t, expected.Value, actual.Value)
|
|
||||||
assert.Equal(t, expected.Kind, actual.Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDimensionComplex_Success(t *testing.T) {
|
|
||||||
// Test parsing a valid dimension string with RU locale
|
|
||||||
input := "10 256.20 см"
|
|
||||||
expected := entity.Dimension{Value: 10256.20, Kind: entity.DimensionKindCentimeter}
|
|
||||||
|
|
||||||
actual, err := entity.ParseDimention(input, entity.DimensionLocalRU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.EqualValues(t, expected.Value, actual.Value)
|
|
||||||
assert.Equal(t, expected.Kind, actual.Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDimension_InvalidInputFormat(t *testing.T) {
|
|
||||||
// Test parsing an invalid dimension string with RU locale
|
|
||||||
input := "invalid value 2"
|
|
||||||
expectedErr := errors.New("expected 2 values after split for value invalid value 2")
|
|
||||||
|
|
||||||
_, err := entity.ParseDimention(input, entity.DimensionLocalRU)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.EqualError(t, err, expectedErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDimension_InvalidLocale(t *testing.T) {
|
|
||||||
// Test parsing a dimension string with an unsupported locale
|
|
||||||
input := "10 мм"
|
|
||||||
expectedErr := errors.New("unknown locale for parse")
|
|
||||||
|
|
||||||
_, err := entity.ParseDimention(input, 3) // An invalid locale value is used here for demonstration purposes
|
|
||||||
assert.EqualError(t, err, expectedErr.Error())
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
type Empty struct{}
|
|
||||||
@ -4,84 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GoodsItemSize struct {
|
|
||||||
Width Dimension
|
|
||||||
Height Dimension
|
|
||||||
Length Dimension
|
|
||||||
|
|
||||||
UnmatchedDepth Dimension
|
|
||||||
}
|
|
||||||
|
|
||||||
func FixupSizes(s GoodsItemSize) (GoodsItemSize, bool) {
|
|
||||||
// Nothing to substitute
|
|
||||||
if s.UnmatchedDepth.IsZero() {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int
|
|
||||||
for _, d := range []Dimension{
|
|
||||||
s.Width,
|
|
||||||
s.Height,
|
|
||||||
s.Length,
|
|
||||||
} {
|
|
||||||
if d.IsZero() {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can only replace one dimension
|
|
||||||
if count != 1 {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case s.Width.IsZero():
|
|
||||||
s.Width = s.UnmatchedDepth
|
|
||||||
case s.Height.IsZero():
|
|
||||||
s.Height = s.UnmatchedDepth
|
|
||||||
case s.Length.IsZero():
|
|
||||||
s.Length = s.UnmatchedDepth
|
|
||||||
}
|
|
||||||
|
|
||||||
s.UnmatchedDepth = Dimension{}
|
|
||||||
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s GoodsItemSize) AllSizesSet() bool {
|
|
||||||
var count int
|
|
||||||
for _, d := range []Dimension{
|
|
||||||
s.Width,
|
|
||||||
s.Height,
|
|
||||||
s.Length,
|
|
||||||
s.UnmatchedDepth,
|
|
||||||
} {
|
|
||||||
if d.IsZero() {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count >= 3
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s GoodsItemSize) GetSum(kind DimensionKind) float64 {
|
|
||||||
var value float64
|
|
||||||
sum := func(ds ...Dimension) {
|
|
||||||
for _, d := range ds {
|
|
||||||
value += d.AdjustTo(kind).Value
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sum(s.Height, s.Length, s.Length)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
type GoodsItem struct {
|
type GoodsItem struct {
|
||||||
Articul string `json:"sku"`
|
Articul string `json:"sku"`
|
||||||
PhotoURLs []string `json:"photo"`
|
PhotoURLs []string `json:"photo"`
|
||||||
@ -96,9 +21,7 @@ type GoodsItem struct {
|
|||||||
TariffPrice float64 `json:"tariff_price"`
|
TariffPrice float64 `json:"tariff_price"`
|
||||||
Cart int64 `json:"cart"`
|
Cart int64 `json:"cart"`
|
||||||
Stock int `json:"stock"`
|
Stock int `json:"stock"`
|
||||||
Sizes GoodsItemSize `json:"sizes"`
|
|
||||||
Parameters map[string]string `json:"parameters"`
|
Parameters map[string]string `json:"parameters"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GoodsItemRaw struct {
|
type GoodsItemRaw struct {
|
||||||
|
|||||||
@ -23,8 +23,7 @@ type Offer struct {
|
|||||||
TypePrefix string `xml:"typePrefix"`
|
TypePrefix string `xml:"typePrefix"`
|
||||||
Description string `xml:"description"`
|
Description string `xml:"description"`
|
||||||
ManufacturerWarrany bool `xml:"manufacturer_warranty"`
|
ManufacturerWarrany bool `xml:"manufacturer_warranty"`
|
||||||
Dimensions string `xml:"dimensions"`
|
Params []Param `xml:"param"`
|
||||||
Params []Param `xml:"param,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Currency struct {
|
type Currency struct {
|
||||||
|
|||||||
@ -11,15 +11,14 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/config"
|
"git.loyso.art/frx/eway/internal/config"
|
||||||
"git.loyso.art/frx/eway/internal/crypto"
|
"git.loyso.art/frx/eway/internal/crypto"
|
||||||
"git.loyso.art/frx/eway/internal/entity"
|
"git.loyso.art/frx/eway/internal/entity"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/gocolly/colly"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,50 +35,33 @@ type client struct {
|
|||||||
http *resty.Client
|
http *resty.Client
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
|
|
||||||
htmlParseSema chan struct{}
|
ownerID string
|
||||||
releaseSemaDelay time.Duration
|
|
||||||
getProductInfoBus chan getProductInfoRequest
|
|
||||||
ownerID string
|
|
||||||
workersPool int
|
|
||||||
workerswg *sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config config.Eway
|
type Config config.Eway
|
||||||
|
|
||||||
func New(ctx context.Context, cfg Config, log zerolog.Logger) (*client, error) {
|
func New(ctx context.Context, cfg Config, log zerolog.Logger) (client, error) {
|
||||||
httpclient := resty.New().
|
httpclient := resty.New().
|
||||||
SetDebug(cfg.Debug).
|
SetDebug(cfg.Debug).
|
||||||
SetBaseURL("https://eway.elevel.ru/api")
|
SetBaseURL("https://eway.elevel.ru/api")
|
||||||
|
|
||||||
c := client{
|
c := client{
|
||||||
http: httpclient,
|
http: httpclient,
|
||||||
log: log.With().Str("client", "eway").Logger(),
|
log: log.With().Str("client", "eway").Logger(),
|
||||||
htmlParseSema: make(chan struct{}, 2),
|
|
||||||
getProductInfoBus: make(chan getProductInfoRequest),
|
|
||||||
releaseSemaDelay: time.Second / 2,
|
|
||||||
workerswg: &sync.WaitGroup{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < cfg.WorkersPool; i++ {
|
|
||||||
c.workerswg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer c.workerswg.Done()
|
|
||||||
c.productInfoWorker(ctx, c.getProductInfoBus)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.SessionID == "" || cfg.SessionUser == "" {
|
if cfg.SessionID == "" || cfg.SessionUser == "" {
|
||||||
if cfg.Login == "" || cfg.Password == "" {
|
if cfg.Login == "" || cfg.Password == "" {
|
||||||
return nil, entity.SimpleError("no auth method provided")
|
return client{}, entity.SimpleError("no auth method provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptedPassword, err := crypto.Decrypt(cfg.Password)
|
decryptedPassword, err := crypto.Decrypt(cfg.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decrypting password: %w", err)
|
return client{}, fmt.Errorf("decrypting password: %w", err)
|
||||||
}
|
}
|
||||||
err = c.login(ctx, cfg.Login, decryptedPassword)
|
err = c.login(ctx, cfg.Login, decryptedPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return client{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("login successful")
|
log.Info().Msg("login successful")
|
||||||
@ -101,10 +83,10 @@ func New(ctx context.Context, cfg Config, log zerolog.Logger) (*client, error) {
|
|||||||
|
|
||||||
c.http.SetCookies(cookies)
|
c.http.SetCookies(cookies)
|
||||||
} else {
|
} else {
|
||||||
return nil, entity.SimpleError("bad configuration: either session_id and session_user should be set or login and password")
|
return client{}, entity.SimpleError("bad configuration: either session_id and session_user should be set or login and password")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetGoodsNewParams struct {
|
type GetGoodsNewParams struct {
|
||||||
@ -124,6 +106,8 @@ type getGoodsNewResponse struct {
|
|||||||
Replacement bool `json:"replacement"`
|
Replacement bool `json:"replacement"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type goodRemnant [4]int
|
||||||
|
|
||||||
func parseGoodItem(items []any) (out entity.GoodsItemRaw) {
|
func parseGoodItem(items []any) (out entity.GoodsItemRaw) {
|
||||||
valueOf := reflect.ValueOf(&out).Elem()
|
valueOf := reflect.ValueOf(&out).Elem()
|
||||||
typeOf := valueOf.Type()
|
typeOf := valueOf.Type()
|
||||||
@ -178,7 +162,7 @@ func mapResponseByOrder(response getGoodsNewResponse) (items []entity.GoodsItemR
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) GetGoodsRemnants(
|
func (c client) GetGoodsRemnants(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
productIDs []int,
|
productIDs []int,
|
||||||
) (out entity.MappedGoodsRemnants, err error) {
|
) (out entity.MappedGoodsRemnants, err error) {
|
||||||
@ -191,12 +175,12 @@ func (c *client) GetGoodsRemnants(
|
|||||||
productsStr = append(productsStr, strconv.Itoa(sku))
|
productsStr = append(productsStr, strconv.Itoa(sku))
|
||||||
}
|
}
|
||||||
|
|
||||||
req := c.http.R().
|
resp, err := c.http.R().
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"products": strings.Join(productsStr, ","),
|
"products": strings.Join(productsStr, ","),
|
||||||
}).
|
}).
|
||||||
SetDoNotParseResponse(true)
|
SetDoNotParseResponse(true).
|
||||||
resp, err := c.do(ctx, "GetGoodsRemnants", req, resty.MethodPost, "/goods_remnants")
|
Post("/goods_remnants")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting goods new: %w", err)
|
return nil, fmt.Errorf("getting goods new: %w", err)
|
||||||
}
|
}
|
||||||
@ -225,41 +209,27 @@ func (c *client) GetGoodsRemnants(
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) GetGoodsNew(
|
func (c client) GetGoodsNew(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
params GetGoodsNewParams,
|
params GetGoodsNewParams,
|
||||||
) (items []entity.GoodsItemRaw, total int, err error) {
|
) (items []entity.GoodsItemRaw, total int, err error) {
|
||||||
var response getGoodsNewResponse
|
var response getGoodsNewResponse
|
||||||
formData := map[string]string{
|
resp, err := c.http.R().
|
||||||
"draw": strconv.Itoa(params.Draw),
|
SetFormData(map[string]string{
|
||||||
"start": strconv.Itoa(params.Start),
|
"draw": strconv.Itoa(params.Draw),
|
||||||
"length": strconv.Itoa(params.Length),
|
"start": strconv.Itoa(params.Start),
|
||||||
"order[0][column]": "14",
|
"length": strconv.Itoa(params.Length),
|
||||||
"order[0][dir]": "desc",
|
"order[0][column]": "14",
|
||||||
"search[value]": "",
|
"order[0][dir]": "desc",
|
||||||
"search[regex]": "false",
|
"search[value]": "",
|
||||||
}
|
"search[regex]": "false",
|
||||||
if params.SearchInStocks {
|
"search_in_stocks": "on",
|
||||||
stocksNum := strconv.Itoa(params.RemmantsAtleast)
|
"remnants_atleast": "5",
|
||||||
formData["search_in_stocks"] = "on"
|
}).
|
||||||
formData["remnants_atleast"] = stocksNum
|
|
||||||
}
|
|
||||||
|
|
||||||
c.log.Debug().
|
|
||||||
Int("remnants", params.RemmantsAtleast).
|
|
||||||
Bool("search_in_stocks", params.SearchInStocks).
|
|
||||||
Int("draw", params.Draw).
|
|
||||||
Int("start", params.Start).
|
|
||||||
Int("length", params.Length).
|
|
||||||
Msg("sending request")
|
|
||||||
|
|
||||||
req := c.http.R().
|
|
||||||
SetFormData(formData).
|
|
||||||
SetQueryParam("category_id", "0").
|
SetQueryParam("category_id", "0").
|
||||||
SetQueryParam("own", c.ownerID). // user id?
|
SetQueryParam("own", c.ownerID). // user id?
|
||||||
SetDoNotParseResponse(true)
|
SetDoNotParseResponse(true).
|
||||||
|
Post("/goods_new")
|
||||||
resp, err := c.do(ctx, "GetGoodsNew", req, resty.MethodPost, "/goods_new")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, fmt.Errorf("getting goods new: %w", err)
|
return nil, -1, fmt.Errorf("getting goods new: %w", err)
|
||||||
}
|
}
|
||||||
@ -269,6 +239,7 @@ func (c *client) GetGoodsNew(
|
|||||||
c.log.Error().Err(err).Msg("unable to close body")
|
c.log.Error().Err(err).Msg("unable to close body")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if resp.IsError() {
|
if resp.IsError() {
|
||||||
return nil, -1, errors.New("request was not successful")
|
return nil, -1, errors.New("request was not successful")
|
||||||
}
|
}
|
||||||
@ -281,15 +252,13 @@ func (c *client) GetGoodsNew(
|
|||||||
return mapResponseByOrder(response), response.RecordsTotal, nil
|
return mapResponseByOrder(response), response.RecordsTotal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) login(ctx context.Context, user, pass string) error {
|
func (c client) login(ctx context.Context, user, pass string) error {
|
||||||
req := c.http.R().
|
resp, err := c.http.R().
|
||||||
SetDoNotParseResponse(true).
|
SetDoNotParseResponse(true).
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"username": user,
|
"username": user,
|
||||||
"password": pass,
|
"password": pass,
|
||||||
})
|
}).Post("https://eway.elevel.ru/")
|
||||||
|
|
||||||
resp, err := c.do(ctx, "login", req, resty.MethodPost, "https://eway.elevel.ru/")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sending request: %w", err)
|
return fmt.Errorf("sending request: %w", err)
|
||||||
}
|
}
|
||||||
@ -303,160 +272,63 @@ func (c *client) login(ctx context.Context, user, pass string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) do(ctx context.Context, name string, req *resty.Request, method string, url string) (resp *resty.Response, err error) {
|
type ProductInfo struct {
|
||||||
resp, err = req.
|
ImageLinks []string
|
||||||
EnableTrace().
|
Parameters map[string]string
|
||||||
Execute(method, url)
|
|
||||||
|
|
||||||
traceInfo := resp.Request.TraceInfo()
|
|
||||||
c.log.Debug().
|
|
||||||
Str("name", name).
|
|
||||||
Str("path", url).
|
|
||||||
Str("method", method).
|
|
||||||
Float64("elapsed", traceInfo.TotalTime.Seconds()).
|
|
||||||
Float64("response_time", traceInfo.ResponseTime.Seconds()).
|
|
||||||
Int("attempt", traceInfo.RequestAttempt).
|
|
||||||
Bool("success", resp.IsSuccess()).
|
|
||||||
Msg("request processed")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("executing request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) GetProductInfo(ctx context.Context, cart int64) (pi entity.GoodsItemInfo, err error) {
|
type parameterSelector struct {
|
||||||
if c.workersPool == 0 {
|
Name string `selector:"div"`
|
||||||
return c.getProductInfo(ctx, cart)
|
Value string `selector:"div.text-right"`
|
||||||
}
|
|
||||||
|
|
||||||
responseBus := make(chan taskResult[entity.GoodsItemInfo], 1)
|
|
||||||
c.getProductInfoBus <- getProductInfoRequest{
|
|
||||||
cartID: cart,
|
|
||||||
response: responseBus,
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case response := <-responseBus:
|
|
||||||
return response.value, response.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return pi, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) getProductInfo(ctx context.Context, cartID int64) (pi entity.GoodsItemInfo, err error) {
|
func (c client) GetProductInfo(ctx context.Context, cart int64) (pi entity.GoodsItemInfo, err error) {
|
||||||
reqpath := "https://eway.elevel.ru/product/" + strconv.Itoa(int(cartID)) + "/"
|
collector := colly.NewCollector(
|
||||||
|
colly.AllowedDomains("eway.elevel.ru"),
|
||||||
req := c.http.R().SetDoNotParseResponse(true).AddRetryCondition(func(r *resty.Response, err error) bool {
|
colly.AllowURLRevisit(),
|
||||||
if r.Request.Attempt > 3 {
|
)
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(err.Error(), "pipe")
|
|
||||||
})
|
|
||||||
|
|
||||||
pi.Parameters = map[string]string{}
|
pi.Parameters = map[string]string{}
|
||||||
resp, err := c.do(ctx, "getProductInfo", req, resty.MethodGet, reqpath)
|
|
||||||
if err != nil {
|
start := time.Now()
|
||||||
return pi, fmt.Errorf("getting product info: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
defer func() {
|
||||||
errClose := resp.RawBody().Close()
|
elapsed := time.Since(start).Seconds()
|
||||||
if errClose == nil {
|
c.log.Info().Float64("elapsed", elapsed).Msg("request processed")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
err = errClose
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.log.Warn().Err(errClose).Msg("unable to close body")
|
|
||||||
}()
|
}()
|
||||||
if resp.IsError() {
|
|
||||||
return pi, errors.New("request was not successful")
|
|
||||||
}
|
|
||||||
|
|
||||||
doc, err := goquery.NewDocumentFromReader(resp.RawBody())
|
collector.OnHTML("body > div.page-container > div.page-content > div.content-wrapper > div.content > div.row > div.col-md-4 > div > div > div:nth-child(6)", func(e *colly.HTMLElement) {
|
||||||
if err != nil {
|
e.ForEach("div.display-flex", func(i int, h *colly.HTMLElement) {
|
||||||
return pi, fmt.Errorf("makind new document: %w", err)
|
var s parameterSelector
|
||||||
}
|
err = h.Unmarshal(&s)
|
||||||
|
if err != nil {
|
||||||
cleanText := func(t string) string {
|
c.log.Warn().Err(err).Msg("unable to unmarshal")
|
||||||
return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(t), ":"))
|
return
|
||||||
}
|
|
||||||
|
|
||||||
const parametersSelector = "body > div.page-container > div.page-content > div.content-wrapper > div.content > div.row > div.col-md-4 > div > div > div:nth-child(6)"
|
|
||||||
const parametersInnerNode = "div.display-flex"
|
|
||||||
doc.
|
|
||||||
Find(parametersSelector).
|
|
||||||
Find(parametersInnerNode).
|
|
||||||
Each(func(i int, s *goquery.Selection) {
|
|
||||||
name := cleanText(s.Find("div").Eq(0).Text())
|
|
||||||
value := cleanText(s.Find("div.text-right").Text())
|
|
||||||
pi.Parameters[name] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
const galleryPanelSelector = "div.gallery_panel"
|
|
||||||
const galleryImageSelector = "div.gallery_thumbnail > img"
|
|
||||||
doc.
|
|
||||||
Find(galleryPanelSelector).
|
|
||||||
Find(galleryImageSelector).
|
|
||||||
Each(func(i int, s *goquery.Selection) {
|
|
||||||
imageURL, ok := s.Attr("data-src")
|
|
||||||
if !ok {
|
|
||||||
imageURL, ok = s.Attr("src")
|
|
||||||
}
|
}
|
||||||
if !ok || len(imageURL) == 0 {
|
|
||||||
|
if s.Name == "" || s.Value == "" {
|
||||||
|
c.log.Warn().Msg("got empty key or value, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.Parameters[s.Name] = s.Value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
collector.OnHTML("div.gallery_panel", func(h *colly.HTMLElement) {
|
||||||
|
h.ForEach("div.gallery_thumbnail > img", func(i int, h *colly.HTMLElement) {
|
||||||
|
imageURL := h.Attr("src")
|
||||||
|
|
||||||
|
if imageURL == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pi.PhotoURLs = append(pi.PhotoURLs, imageURL)
|
pi.PhotoURLs = append(pi.PhotoURLs, imageURL)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
err = collector.Visit("https://eway.elevel.ru/product/" + strconv.Itoa(int(cart)) + "/")
|
||||||
|
if err != nil {
|
||||||
|
return pi, fmt.Errorf("visiting site: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return pi, nil
|
return pi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type getProductInfoRequest struct {
|
|
||||||
cartID int64
|
|
||||||
response chan taskResult[entity.GoodsItemInfo]
|
|
||||||
}
|
|
||||||
|
|
||||||
type taskResult[T any] struct {
|
|
||||||
value T
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *client) productInfoWorker(
|
|
||||||
ctx context.Context,
|
|
||||||
in <-chan getProductInfoRequest,
|
|
||||||
) {
|
|
||||||
var req getProductInfoRequest
|
|
||||||
var ok bool
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case req, ok = <-in:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
c.htmlParseSema <- struct{}{}
|
|
||||||
defer func() {
|
|
||||||
go func() {
|
|
||||||
time.Sleep(c.releaseSemaDelay)
|
|
||||||
<-c.htmlParseSema
|
|
||||||
}()
|
|
||||||
}()
|
|
||||||
|
|
||||||
pi, err := c.getProductInfo(ctx, req.cartID)
|
|
||||||
req.response <- taskResult[entity.GoodsItemInfo]{
|
|
||||||
value: pi,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
package matcher
|
|
||||||
|
|
||||||
type Unit interface {
|
|
||||||
MatchByPattern(value string) (pattern string)
|
|
||||||
Match(value string) bool
|
|
||||||
|
|
||||||
// Move this to init stage because some of matchers
|
|
||||||
// might not be applicable to it
|
|
||||||
RegisterRegexp(regexpPattern string)
|
|
||||||
Register(pattern string)
|
|
||||||
|
|
||||||
Patterns() []string
|
|
||||||
}
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
package matcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const radixWildcard = '*'
|
|
||||||
|
|
||||||
type radixNode struct {
|
|
||||||
exact bool
|
|
||||||
next map[rune]*radixNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type radixMatcher struct {
|
|
||||||
root *radixNode
|
|
||||||
|
|
||||||
caseInsensitive bool
|
|
||||||
saved []string
|
|
||||||
regexps []*regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
type RadixOpt func(m *radixMatcher)
|
|
||||||
|
|
||||||
func RadixCaseInsensitive() RadixOpt {
|
|
||||||
return func(m *radixMatcher) {
|
|
||||||
m.caseInsensitive = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRadix(opts ...RadixOpt) *radixMatcher {
|
|
||||||
m := &radixMatcher{
|
|
||||||
root: &radixNode{
|
|
||||||
next: make(map[rune]*radixNode),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) MatchByPattern(value string) (pattern string) {
|
|
||||||
originValue := value
|
|
||||||
if r.caseInsensitive {
|
|
||||||
value = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
node := r.root
|
|
||||||
lastIdx := len([]rune(value)) - 1
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
for i, v := range value {
|
|
||||||
var ok bool
|
|
||||||
if _, ok := node.next[radixWildcard]; ok {
|
|
||||||
_, _ = sb.WriteRune(radixWildcard)
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
node, ok = node.next[v]
|
|
||||||
if !ok {
|
|
||||||
return r.findByRegexp(originValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteRune(v)
|
|
||||||
|
|
||||||
if i != lastIdx {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !node.exact {
|
|
||||||
return r.findByRegexp(originValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.findByRegexp(originValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) Match(value string) bool {
|
|
||||||
originValue := value
|
|
||||||
if r.caseInsensitive {
|
|
||||||
value = strings.ToLower(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
node := r.root
|
|
||||||
lastIdx := len([]rune(value)) - 1
|
|
||||||
for i, v := range value {
|
|
||||||
var ok bool
|
|
||||||
if _, ok = node.next[radixWildcard]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
node, ok = node.next[v]
|
|
||||||
if !ok {
|
|
||||||
return r.findByRegexp(originValue) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == lastIdx {
|
|
||||||
return node.exact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.findByRegexp(originValue) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) findByRegexp(value string) (regexpPattern string) {
|
|
||||||
for _, rx := range r.regexps {
|
|
||||||
if rx.MatchString(value) {
|
|
||||||
return rx.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) RegisterRegexp(regexpPattern string) {
|
|
||||||
pattern := regexp.MustCompile(regexpPattern)
|
|
||||||
r.regexps = append(r.regexps, pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) Register(pattern string) {
|
|
||||||
if len(pattern) == 0 {
|
|
||||||
panic("unable to handle empty pattern")
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.caseInsensitive {
|
|
||||||
pattern = strings.ToLower(pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.saved = append(r.saved, pattern)
|
|
||||||
|
|
||||||
node := r.root
|
|
||||||
lastIdx := len([]rune(pattern)) - 1
|
|
||||||
for i, v := range pattern {
|
|
||||||
nextNode, ok := node.next[v]
|
|
||||||
if !ok {
|
|
||||||
nextNode = &radixNode{
|
|
||||||
next: make(map[rune]*radixNode),
|
|
||||||
}
|
|
||||||
node.next[v] = nextNode
|
|
||||||
}
|
|
||||||
node = nextNode
|
|
||||||
|
|
||||||
if v == '*' {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if i != lastIdx {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
node.exact = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *radixMatcher) Patterns() []string {
|
|
||||||
ownIdx := len(r.saved)
|
|
||||||
out := make([]string, len(r.saved)+len(r.regexps))
|
|
||||||
copy(out, r.saved[:ownIdx])
|
|
||||||
|
|
||||||
for i := 0; i < len(r.regexps); i++ {
|
|
||||||
idx := i + ownIdx
|
|
||||||
out[idx] = r.regexps[i].String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
package matcher_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/matcher"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRadixMatcherWithPattern(t *testing.T) {
|
|
||||||
m := matcher.NewRadix()
|
|
||||||
m.Register("aloha")
|
|
||||||
m.Register("hawaii")
|
|
||||||
m.Register("te*")
|
|
||||||
m.Register("Ширина")
|
|
||||||
|
|
||||||
var tt = []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
pattern string
|
|
||||||
}{{
|
|
||||||
name: "should match exact",
|
|
||||||
in: "aloha",
|
|
||||||
pattern: "aloha",
|
|
||||||
}, {
|
|
||||||
name: "should match exact 2",
|
|
||||||
in: "hawaii",
|
|
||||||
pattern: "hawaii",
|
|
||||||
}, {
|
|
||||||
name: "should match pattern",
|
|
||||||
in: "test",
|
|
||||||
pattern: "te*",
|
|
||||||
}, {
|
|
||||||
name: "should not match",
|
|
||||||
in: "alohe",
|
|
||||||
}, {
|
|
||||||
name: "should not match 2",
|
|
||||||
in: "whoa",
|
|
||||||
}, {
|
|
||||||
name: "should not match 3",
|
|
||||||
in: "alohaya",
|
|
||||||
}, {
|
|
||||||
name: "should match exact 3",
|
|
||||||
in: "Ширина",
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
pattern := m.MatchByPattern(tc.in)
|
|
||||||
if pattern != tc.pattern {
|
|
||||||
t.Errorf("expected %s got %s", tc.pattern, pattern)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRadixMatcher(t *testing.T) {
|
|
||||||
m := matcher.NewRadix()
|
|
||||||
m.Register("aloha")
|
|
||||||
m.Register("hawaii")
|
|
||||||
m.Register("te*")
|
|
||||||
|
|
||||||
var tt = []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
match bool
|
|
||||||
}{{
|
|
||||||
name: "should match exact",
|
|
||||||
in: "aloha",
|
|
||||||
match: true,
|
|
||||||
}, {
|
|
||||||
name: "should match exact 2",
|
|
||||||
in: "hawaii",
|
|
||||||
match: true,
|
|
||||||
}, {
|
|
||||||
name: "should match pattern",
|
|
||||||
in: "test",
|
|
||||||
match: true,
|
|
||||||
}, {
|
|
||||||
name: "should not match",
|
|
||||||
in: "alohe",
|
|
||||||
}, {
|
|
||||||
name: "should not match 2",
|
|
||||||
in: "whoa",
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
match := m.Match(tc.in)
|
|
||||||
if match != tc.match {
|
|
||||||
t.Errorf("expected %t got %t", tc.match, match)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRadixMatcherWildcard(t *testing.T) {
|
|
||||||
m := matcher.NewRadix()
|
|
||||||
m.Register("*")
|
|
||||||
|
|
||||||
var tt = []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
match bool
|
|
||||||
}{{
|
|
||||||
name: "should match exact",
|
|
||||||
in: "aloha",
|
|
||||||
match: true,
|
|
||||||
}, {
|
|
||||||
name: "should match exact 2",
|
|
||||||
in: "hawaii",
|
|
||||||
match: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range tt {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
match := m.Match(tc.in)
|
|
||||||
if match != tc.match {
|
|
||||||
t.Errorf("expected %t got %t", tc.match, match)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -35,11 +35,10 @@ func Open(ctx context.Context, path string, debug bool, log zerolog.Logger) (*ba
|
|||||||
log: log.With().Str("db", "badger").Logger(),
|
log: log.With().Str("db", "badger").Logger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
level := badger.WARNING
|
level := badger.INFO
|
||||||
if debug {
|
if debug {
|
||||||
level = badger.DEBUG
|
level = badger.DEBUG
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := badger.DefaultOptions(path).
|
opts := badger.DefaultOptions(path).
|
||||||
WithLogger(bl).
|
WithLogger(bl).
|
||||||
WithLoggingLevel(level).
|
WithLoggingLevel(level).
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"git.loyso.art/frx/eway/internal/encoding/fbs"
|
"git.loyso.art/frx/eway/internal/encoding/fbs"
|
||||||
@ -249,20 +248,18 @@ func (c *goodsItemClient) upsertByBatch(ctx context.Context, items []entity.Good
|
|||||||
batch := c.db.NewWriteBatch()
|
batch := c.db.NewWriteBatch()
|
||||||
defer batch.Cancel()
|
defer batch.Cancel()
|
||||||
|
|
||||||
createdAt := time.Now()
|
|
||||||
err := func() error {
|
err := func() error {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
item.CreatedAt = createdAt
|
key := c.prefixedStr(item.Articul)
|
||||||
value, err := c.s.Serialize(item)
|
value, err := c.s.Serialize(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("serializing item: %w", err)
|
return fmt.Errorf("serializing item: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
key := c.prefixedStr(item.Articul)
|
|
||||||
idxValue := make([]byte, len(key))
|
idxValue := make([]byte, len(key))
|
||||||
copy(idxValue, key)
|
copy(idxValue, key)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user