setup parser
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/pkg/slices"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/net/html"
|
||||
@ -76,6 +77,7 @@ type ListEducationProductsParams struct {
|
||||
LearningType string
|
||||
CoursesThematics string
|
||||
|
||||
SortBy string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
@ -116,6 +118,8 @@ func (c *client) ListEducationalProducts(
|
||||
const defaultLimit = 1
|
||||
const defaultSortProp = "advertising.position"
|
||||
const defaultSortDirection = "asc"
|
||||
// TODO: find out should it be settable
|
||||
const productName = "learning-courses"
|
||||
if err = c.checkClientInited(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
@ -123,33 +127,28 @@ func (c *client) ListEducationalProducts(
|
||||
if !c.validLearningTypes.hasValue(params.LearningType) {
|
||||
return result, errors.NewValidationError("learning_type", "bad value")
|
||||
}
|
||||
if !c.validCourseThematics.hasValue(params.CoursesThematics) {
|
||||
if params.CoursesThematics != "" && !c.validCourseThematics.hasValue(params.CoursesThematics) {
|
||||
return result, errors.NewValidationError("courses_thematics", "bad value")
|
||||
}
|
||||
|
||||
reqParams := ListEducationProductsRequest{
|
||||
LearningType: []string{
|
||||
params.LearningType,
|
||||
},
|
||||
CoursesThematics: []string{
|
||||
params.CoursesThematics,
|
||||
},
|
||||
LearningType: valueAsArray(params.LearningType),
|
||||
CoursesThematics: valueAsArray(params.CoursesThematics),
|
||||
ProductName: productName,
|
||||
Fields: defaultProductFields,
|
||||
SortProperty: defaultSortProp, // mayber sort by price?
|
||||
SortDirection: defaultSortDirection,
|
||||
NotSubIsWebinar: strconv.FormatBool(true),
|
||||
NotB2B: strconv.FormatBool(true),
|
||||
IsMix: false, // not sure why, but for better parsing
|
||||
MixRepeated: true, // looks like this option should force to exclude duplicates
|
||||
AdvertisingOnly: false, // If true, it will show only paid items.
|
||||
Location: "", // TODO: get and fill location?
|
||||
Fingerprint: "", // not sure it should be set.
|
||||
OfferTypes: []string{}, // for more precise filter but not needed.
|
||||
|
||||
Fields: defaultProductFields,
|
||||
SortProperty: defaultSortProp, // mayber sort by price?
|
||||
SortDirection: defaultSortDirection,
|
||||
NotSubIsWebinar: strconv.FormatBool(true),
|
||||
NotB2B: strconv.FormatBool(true),
|
||||
IsMix: true, // not sure why, but for better parsing
|
||||
MixRepeated: true, // looks like this option should force to exclude duplicates
|
||||
AdvertisingOnly: false, // If true, it will show only paid items.
|
||||
Location: "", // TODO: get and fill location?
|
||||
Fingerprint: "", // not sure it should be set.
|
||||
ProductName: "", // looks like it does not affects anything
|
||||
OfferTypes: nil, // for more precise filter but not needed.
|
||||
|
||||
Limit: defaultLimit,
|
||||
Offset: 0,
|
||||
Limit: xdefault.WithFallback(params.Limit, defaultLimit),
|
||||
Offset: params.Offset,
|
||||
}
|
||||
|
||||
req := c.http.R().
|
||||
@ -353,17 +352,6 @@ func (qs querySet) exactSubset(values ...string) ([]string, error) {
|
||||
|
||||
}
|
||||
|
||||
// func (qs querySet) subset(values ...string) []string {
|
||||
// out := make([]string, 0, len(values))
|
||||
// for _, value := range values {
|
||||
// if qs.hasValue(value) {
|
||||
// out = append(out, value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return out
|
||||
// }
|
||||
|
||||
func newQuerySet(values ...string) querySet {
|
||||
qs := querySet{
|
||||
values: make([]string, len(values)),
|
||||
@ -377,3 +365,11 @@ func newQuerySet(values ...string) querySet {
|
||||
|
||||
return qs
|
||||
}
|
||||
|
||||
func valueAsArray(value string) []string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{value}
|
||||
}
|
||||
|
||||
@ -102,9 +102,9 @@ func (p *PageState) Clone() *PageState {
|
||||
type CourseDiscount struct {
|
||||
PromoCode string `json:"promoCode"`
|
||||
PromoCodeType string `json:"promoCodeType"`
|
||||
Percent int `json:"percent"`
|
||||
Percent any `json:"percent"`
|
||||
EndDate time.Time `json:"endDate"`
|
||||
EndTime any `json:"endTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
type CourseAdvertising struct {
|
||||
@ -140,17 +140,17 @@ type Course struct {
|
||||
Discount CourseDiscount `json:"discount"`
|
||||
Link string `json:"link"`
|
||||
Learningtype []string `json:"learningtype"`
|
||||
DateStart any `json:"dateStart"`
|
||||
TimeStart any `json:"timeStart"`
|
||||
TimeAllHour any `json:"timeAllHour"`
|
||||
TimeAllDay any `json:"timeAllDay"`
|
||||
TimeAllMonth int `json:"timeAllMonth"`
|
||||
DateStart *time.Time `json:"dateStart"`
|
||||
TimeStart *time.Time `json:"timeStart"`
|
||||
TimeAllHour *float64 `json:"timeAllHour"`
|
||||
TimeAllDay *float64 `json:"timeAllDay"`
|
||||
TimeAllMonth *float64 `json:"timeAllMonth"`
|
||||
IsTermApproximately bool `json:"isTermApproximately"`
|
||||
DictionaryFormatFilterNew []string `json:"dictionaryFormatFilterNew"`
|
||||
DictionaryLevelFilterNew []string `json:"dictionaryLevelFilterNew"`
|
||||
Price int `json:"price"`
|
||||
PriceAll int `json:"priceAll"`
|
||||
PriceInstallment int `json:"priceInstallment"`
|
||||
Price float64 `json:"price"`
|
||||
PriceAll float64 `json:"priceAll"`
|
||||
PriceInstallment float64 `json:"priceInstallment"`
|
||||
CourseImage string `json:"courseImage"`
|
||||
WithoutDiscountPrice int `json:"withoutDiscountPrice"`
|
||||
Advertising CourseAdvertising `json:"advertising"`
|
||||
|
||||
12
internal/common/generator/ids.go
Normal file
12
internal/common/generator/ids.go
Normal file
@ -0,0 +1,12 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func RandomInt64ID() string {
|
||||
data := [8]byte{}
|
||||
_, _ = rand.Read(data[:])
|
||||
return hex.EncodeToString(data[:])
|
||||
}
|
||||
@ -34,6 +34,10 @@ func LogError(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.A
|
||||
log.LogAttrs(ctx, slog.LevelError, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
}
|
||||
|
||||
func LogWithError(ctx context.Context, log *slog.Logger, err error, msg string, attrs ...slog.Attr) {
|
||||
LogError(ctx, log, msg, append(attrs, slog.Any("err", err))...)
|
||||
}
|
||||
|
||||
func getLogFields(ctx context.Context) []slog.Attr {
|
||||
store, _ := ctx.Value(ctxLogKey{}).(ctxLogAttrStore)
|
||||
return store.attrs
|
||||
|
||||
37
internal/common/xlog/slog.go
Normal file
37
internal/common/xlog/slog.go
Normal file
@ -0,0 +1,37 @@
|
||||
package xlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func LevelAsSLogLevel(level Level) slog.Level {
|
||||
switch level {
|
||||
case LevelDebug:
|
||||
return slog.LevelDebug
|
||||
case LevelInfo:
|
||||
return slog.LevelInfo
|
||||
case LevelWarn:
|
||||
return slog.LevelWarn
|
||||
case LevelError:
|
||||
return slog.LevelError
|
||||
default:
|
||||
panic("unsupported level " + level.String())
|
||||
}
|
||||
}
|
||||
|
||||
func StringerAttr(name string, value fmt.Stringer) slog.Attr {
|
||||
return slog.Any(name, StringerValue(value))
|
||||
}
|
||||
|
||||
func StringerValue(s fmt.Stringer) slog.LogValuer {
|
||||
return stringerValue{s}
|
||||
}
|
||||
|
||||
type stringerValue struct {
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
func (s stringerValue) LogValue() slog.Value {
|
||||
return slog.StringValue(s.String())
|
||||
}
|
||||
67
internal/common/xlog/xlog.go
Normal file
67
internal/common/xlog/xlog.go
Normal file
@ -0,0 +1,67 @@
|
||||
package xlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Level uint8
|
||||
|
||||
const (
|
||||
LevelDebug Level = iota
|
||||
LevelInfo
|
||||
LevelWarn
|
||||
LevelError
|
||||
)
|
||||
|
||||
func (l *Level) UnmarshalText(data []byte) error {
|
||||
switch levelstr := string(bytes.ToLower(data)); levelstr {
|
||||
case "debug":
|
||||
*l = LevelDebug
|
||||
case "info":
|
||||
*l = LevelInfo
|
||||
case "warn":
|
||||
*l = LevelWarn
|
||||
case "error":
|
||||
*l = LevelError
|
||||
default:
|
||||
return errors.New("unsupported level " + levelstr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case LevelDebug:
|
||||
return "debug"
|
||||
case LevelInfo:
|
||||
return "info"
|
||||
case LevelWarn:
|
||||
return "warn"
|
||||
case LevelError:
|
||||
return "error"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type Format uint8
|
||||
|
||||
const (
|
||||
FormatText Format = iota
|
||||
FormatJSON
|
||||
)
|
||||
|
||||
func (f *Format) UnmarshalText(data []byte) error {
|
||||
switch formatstr := string(bytes.ToLower(data)); formatstr {
|
||||
case "debug":
|
||||
*f = FormatText
|
||||
case "info":
|
||||
*f = FormatJSON
|
||||
default:
|
||||
return errors.New("unsupported format " + formatstr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
36
internal/common/xslice/filter.go
Normal file
36
internal/common/xslice/filter.go
Normal file
@ -0,0 +1,36 @@
|
||||
package xslice
|
||||
|
||||
func Filter[T any](values []T, f func(T) bool) []T {
|
||||
out := make([]T, 0, len(values))
|
||||
for _, value := range values {
|
||||
if f(value) {
|
||||
out = append(out, value)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func FilterInplace[T any](values []T, match func(T) bool) (newlen int) {
|
||||
next := 0
|
||||
for i := 0; i < len(values); i++ {
|
||||
value := values[i]
|
||||
if !match(value) {
|
||||
continue
|
||||
}
|
||||
|
||||
if next != i {
|
||||
values[next] = value
|
||||
}
|
||||
|
||||
next++
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
func Not[T any](f func(T) bool) func(T) bool {
|
||||
return func(t T) bool {
|
||||
return !f(t)
|
||||
}
|
||||
}
|
||||
52
internal/common/xslice/filter_test.go
Normal file
52
internal/common/xslice/filter_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package xslice_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
)
|
||||
|
||||
func TestFilterInplace(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
in []int
|
||||
check func(int) bool
|
||||
expLen int
|
||||
}{{
|
||||
name: "empty",
|
||||
in: nil,
|
||||
check: nil,
|
||||
expLen: 0,
|
||||
}, {
|
||||
name: "all match",
|
||||
in: []int{1, 2, 3},
|
||||
check: func(int) bool {
|
||||
return true
|
||||
},
|
||||
expLen: 3,
|
||||
}, {
|
||||
name: "all not match",
|
||||
in: []int{1, 2, 3},
|
||||
check: func(int) bool {
|
||||
return false
|
||||
},
|
||||
expLen: 0,
|
||||
}, {
|
||||
name: "some filtered out",
|
||||
in: []int{1, 2, 3, 4},
|
||||
check: func(v int) bool {
|
||||
return v%2 == 0
|
||||
},
|
||||
expLen: 2,
|
||||
}}
|
||||
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotLen := xslice.FilterInplace(tc.in, tc.check)
|
||||
if gotLen != tc.expLen {
|
||||
t.Errorf("exp %d got %d", tc.expLen, gotLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
internal/common/xslice/foreach.go
Normal file
7
internal/common/xslice/foreach.go
Normal file
@ -0,0 +1,7 @@
|
||||
package xslice
|
||||
|
||||
func ForEach[T any](items []T, f func(T)) {
|
||||
for _, item := range items {
|
||||
f(item)
|
||||
}
|
||||
}
|
||||
10
internal/common/xslice/map.go
Normal file
10
internal/common/xslice/map.go
Normal file
@ -0,0 +1,10 @@
|
||||
package xslice
|
||||
|
||||
func Map[T, U any](in []T, f func(T) U) []U {
|
||||
out := make([]U, len(in))
|
||||
for i, value := range in {
|
||||
out[i] = f(value)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user