make application with base logic
This commit is contained in:
@ -1 +0,0 @@
|
||||
package courses
|
||||
@ -9,8 +9,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/domain"
|
||||
"git.loyso.art/frx/kurious/pkg/utilities/slices"
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/pkg/slices"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/net/html"
|
||||
@ -21,8 +21,10 @@ const (
|
||||
baseURL = "https://www.sravni.ru/kursy"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Client
|
||||
type Client interface {
|
||||
GetMainPageState() *PageState
|
||||
GetMainPageState() (*PageState, error)
|
||||
|
||||
ListEducationalProducts(
|
||||
ctx context.Context,
|
||||
params ListEducationProductsParams,
|
||||
@ -66,8 +68,8 @@ type client struct {
|
||||
validCourseThematics querySet
|
||||
}
|
||||
|
||||
func (c *client) GetMainPageState() *PageState {
|
||||
return c.cachedMainPageInfo.Clone()
|
||||
func (c *client) GetMainPageState() (*PageState, error) {
|
||||
return c.cachedMainPageInfo.Clone(), nil
|
||||
}
|
||||
|
||||
type ListEducationProductsParams struct {
|
||||
@ -119,10 +121,10 @@ func (c *client) ListEducationalProducts(
|
||||
}
|
||||
|
||||
if !c.validLearningTypes.hasValue(params.LearningType) {
|
||||
return result, domain.NewValidationError("learning_type", "bad value")
|
||||
return result, errors.NewValidationError("learning_type", "bad value")
|
||||
}
|
||||
if !c.validCourseThematics.hasValue(params.CoursesThematics) {
|
||||
return result, domain.NewValidationError("courses_thematics", "bad value")
|
||||
return result, errors.NewValidationError("courses_thematics", "bad value")
|
||||
}
|
||||
|
||||
reqParams := ListEducationProductsRequest{
|
||||
@ -161,7 +163,7 @@ func (c *client) ListEducationalProducts(
|
||||
}
|
||||
|
||||
if resp.IsError() {
|
||||
return result, fmt.Errorf("bad status code %d: %w", resp.StatusCode(), domain.ErrUnexpectedStatus)
|
||||
return result, fmt.Errorf("bad status code %d: %w", resp.StatusCode(), errors.ErrUnexpectedStatus)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@ -202,7 +204,7 @@ func (c *client) getMainPageState(ctx context.Context) (*PageState, error) {
|
||||
|
||||
if resp.IsError() {
|
||||
c.log.ErrorContext(ctx, "unable to proceed request", slog.String("body", string(resp.Body())))
|
||||
return nil, fmt.Errorf("got %d, but expected success: %w", resp.StatusCode(), domain.ErrUnexpectedStatus)
|
||||
return nil, fmt.Errorf("got %d, but expected success: %w", resp.StatusCode(), errors.ErrUnexpectedStatus)
|
||||
}
|
||||
|
||||
traceInfo := resp.Request.TraceInfo()
|
||||
@ -3,11 +3,11 @@ package sravni
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/domain"
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrClientNotInited domain.SimpleError = "client was not inited"
|
||||
ErrClientNotInited errors.SimpleError = "client was not inited"
|
||||
)
|
||||
|
||||
type PageStateRuntimeConfig struct {
|
||||
17
internal/common/client/sravni/noop.go
Normal file
17
internal/common/client/sravni/noop.go
Normal file
@ -0,0 +1,17 @@
|
||||
package sravni
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
)
|
||||
|
||||
type NoopClient struct{}
|
||||
|
||||
func (NoopClient) GetMainPageState() (*PageState, error) {
|
||||
return nil, errors.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (ListEducationProductsResponse, error) {
|
||||
return ListEducationProductsResponse{}, errors.ErrNotImplemented
|
||||
}
|
||||
55
internal/common/config/log.go
Normal file
55
internal/common/config/log.go
Normal file
@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
type LogFormat uint8
|
||||
|
||||
const (
|
||||
LogFormatText LogFormat = iota
|
||||
LogFormatJSON
|
||||
)
|
||||
|
||||
type LogLevel uint8
|
||||
|
||||
const (
|
||||
LogLevelDebug LogLevel = iota
|
||||
LogLevelInfo
|
||||
LogLevelWarn
|
||||
LogLevelError
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
Level LogLevel
|
||||
Format LogFormat
|
||||
}
|
||||
|
||||
func NewSLogger(config LogConfig) *slog.Logger {
|
||||
var level slog.Level
|
||||
switch config.Level {
|
||||
case LogLevelDebug:
|
||||
level = slog.LevelDebug
|
||||
case LogLevelInfo:
|
||||
level = slog.LevelInfo
|
||||
case LogLevelWarn:
|
||||
level = slog.LevelWarn
|
||||
case LogLevelError:
|
||||
level = slog.LevelError
|
||||
}
|
||||
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: level,
|
||||
}
|
||||
|
||||
var h slog.Handler
|
||||
switch config.Format {
|
||||
case LogFormatJSON:
|
||||
h = slog.NewJSONHandler(os.Stdout, opts)
|
||||
case LogFormatText:
|
||||
h = slog.NewTextHandler(os.Stdout, opts)
|
||||
}
|
||||
|
||||
return slog.New(h)
|
||||
}
|
||||
17
internal/common/decorator/command.go
Normal file
17
internal/common/decorator/command.go
Normal file
@ -0,0 +1,17 @@
|
||||
package decorator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type CommandHandler[T any] interface {
|
||||
Handle(ctx context.Context, params T) error
|
||||
}
|
||||
|
||||
func ApplyCommandDecorators[T any](base CommandHandler[T], log *slog.Logger) CommandHandler[T] {
|
||||
return commandLoggingDecorator[T]{
|
||||
base: base,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
64
internal/common/decorator/logging.go
Normal file
64
internal/common/decorator/logging.go
Normal file
@ -0,0 +1,64 @@
|
||||
package decorator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
)
|
||||
|
||||
type commandLoggingDecorator[T any] struct {
|
||||
base CommandHandler[T]
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err error) {
|
||||
handlerName := getTypeName[T]()
|
||||
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
xcontext.LogDebug(ctx, c.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
elapsed := slog.Duration("elapsed", time.Since(start))
|
||||
if err == nil {
|
||||
xcontext.LogInfo(ctx, c.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
}
|
||||
}()
|
||||
|
||||
return c.base.Handle(ctx, cmd)
|
||||
}
|
||||
|
||||
type queryLoggingDecorator[Q, U any] struct {
|
||||
base QueryHandler[Q, U]
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entity U, err error) {
|
||||
handlerName := getTypeName[Q]()
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
xcontext.LogDebug(ctx, q.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
defer func() {
|
||||
elapsed := slog.Duration("elapsed", time.Since(start))
|
||||
if err == nil {
|
||||
xcontext.LogInfo(ctx, q.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, q.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
}
|
||||
}()
|
||||
|
||||
return q.base.Handle(ctx, query)
|
||||
|
||||
}
|
||||
|
||||
func getTypeName[T any]() string {
|
||||
var t T
|
||||
out := fmt.Sprintf("%T", t)
|
||||
return out
|
||||
}
|
||||
17
internal/common/decorator/query.go
Normal file
17
internal/common/decorator/query.go
Normal file
@ -0,0 +1,17 @@
|
||||
package decorator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type QueryHandler[Q, U any] interface {
|
||||
Handle(ctx context.Context, query Q) (entity U, err error)
|
||||
}
|
||||
|
||||
func AddQueryDecorators[Q, U any](base QueryHandler[Q, U], log *slog.Logger) QueryHandler[Q, U] {
|
||||
return queryLoggingDecorator[Q, U]{
|
||||
base: base,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package domain
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
42
internal/common/nullable/value.go
Normal file
42
internal/common/nullable/value.go
Normal file
@ -0,0 +1,42 @@
|
||||
package nullable
|
||||
|
||||
type Value[T any] struct {
|
||||
value T
|
||||
valid bool
|
||||
}
|
||||
|
||||
func NewValue[T any](value T) Value[T] {
|
||||
return Value[T]{
|
||||
value: value,
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewValuePtr[T any](value *T) Value[T] {
|
||||
if value == nil {
|
||||
return Value[T]{}
|
||||
}
|
||||
|
||||
return NewValue(*value)
|
||||
}
|
||||
|
||||
func (n Value[T]) Value() T {
|
||||
return n.value
|
||||
}
|
||||
|
||||
func (n Value[T]) Valid() bool {
|
||||
return n.valid
|
||||
}
|
||||
|
||||
func (n Value[T]) ValutPtr() *T {
|
||||
if n.valid {
|
||||
return &n.value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Value[T]) Set(value T) {
|
||||
n.valid = true
|
||||
n.value = value
|
||||
}
|
||||
40
internal/common/xcontext/log.go
Normal file
40
internal/common/xcontext/log.go
Normal file
@ -0,0 +1,40 @@
|
||||
package xcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type ctxLogKey struct{}
|
||||
|
||||
type ctxLogAttrStore struct {
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
func WithLogFields(ctx context.Context, fields ...slog.Attr) context.Context {
|
||||
store, _ := ctx.Value(ctxLogKey{}).(ctxLogAttrStore)
|
||||
store.attrs = append(store.attrs, fields...)
|
||||
|
||||
return context.WithValue(ctx, ctxLogKey{}, store)
|
||||
}
|
||||
|
||||
func LogDebug(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelDebug, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
}
|
||||
|
||||
func LogInfo(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelInfo, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
}
|
||||
|
||||
func LogWarn(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelWarn, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
}
|
||||
|
||||
func LogError(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelError, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
}
|
||||
|
||||
func getLogFields(ctx context.Context) []slog.Attr {
|
||||
store, _ := ctx.Value(ctxLogKey{}).(ctxLogAttrStore)
|
||||
return store.attrs
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
// Package adapters aggregates all external services and it's implementations.
|
||||
package adapters
|
||||
|
||||
type Services struct{}
|
||||
|
||||
func NewServices() Services {
|
||||
return Services{}
|
||||
}
|
||||
32
internal/kurious/adapters/ydb_course_repository.go
Normal file
32
internal/kurious/adapters/ydb_course_repository.go
Normal file
@ -0,0 +1,32 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
func NewYDBCourseRepository() (*ydbCourseRepository, error) {
|
||||
return &ydbCourseRepository{}, nil
|
||||
}
|
||||
|
||||
type ydbCourseRepository struct{}
|
||||
|
||||
func (ydbCourseRepository) List(ctx context.Context, params domain.ListCoursesParams) ([]domain.Course, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (ydbCourseRepository) Get(ctx context.Context, id string) (domain.Course, error) {
|
||||
return domain.Course{}, nil
|
||||
}
|
||||
func (ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
||||
return domain.Course{}, nil
|
||||
}
|
||||
func (ydbCourseRepository) Create(context.Context, domain.CreateCourseParams) (domain.Course, error) {
|
||||
return domain.Course{}, nil
|
||||
}
|
||||
func (ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
func (ydbCourseRepository) Close() error {
|
||||
return nil
|
||||
}
|
||||
21
internal/kurious/app/app.go
Normal file
21
internal/kurious/app/app.go
Normal file
@ -0,0 +1,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
)
|
||||
|
||||
type Commands struct {
|
||||
InsertCourse command.CreateCourseHandler
|
||||
DeleteCourse command.DeleteCourseHandler
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
GetCourse query.GetCourseHandler
|
||||
ListCourses query.ListCourseHandler
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Commands Commands
|
||||
Queries Queries
|
||||
}
|
||||
54
internal/kurious/app/command/createcourse.go
Normal file
54
internal/kurious/app/command/createcourse.go
Normal file
@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type CreateCourse struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExternalID nullable.Value[string]
|
||||
SourceType domain.SourceType
|
||||
SourceName nullable.Value[string]
|
||||
OrganizationID string
|
||||
OriginLink string
|
||||
ImageLink string
|
||||
FullPrice float64
|
||||
Discount float64
|
||||
Duration time.Duration
|
||||
StartsAt time.Time
|
||||
}
|
||||
|
||||
type CreateCourseHandler decorator.CommandHandler[CreateCourse]
|
||||
|
||||
type createCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
}
|
||||
|
||||
func NewCreateCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) CreateCourseHandler {
|
||||
h := createCourseHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.ApplyCommandDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h createCourseHandler) Handle(ctx context.Context, cmd CreateCourse) error {
|
||||
_, err := h.repo.Create(ctx, domain.CreateCourseParams(cmd))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating course: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
40
internal/kurious/app/command/deletecourse.go
Normal file
40
internal/kurious/app/command/deletecourse.go
Normal file
@ -0,0 +1,40 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type DeleteCourse struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type DeleteCourseHandler decorator.CommandHandler[DeleteCourse]
|
||||
|
||||
type deleteCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
}
|
||||
|
||||
func NewDeleteCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) DeleteCourseHandler {
|
||||
h := deleteCourseHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.ApplyCommandDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h deleteCourseHandler) Handle(ctx context.Context, cmd DeleteCourse) error {
|
||||
err := h.repo.Delete(ctx, cmd.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
39
internal/kurious/app/query/getcourse.go
Normal file
39
internal/kurious/app/query/getcourse.go
Normal file
@ -0,0 +1,39 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type GetCourse struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course]
|
||||
|
||||
type getCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
}
|
||||
|
||||
func NewGetCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) GetCourseHandler {
|
||||
h := getCourseHandler{
|
||||
repo: repo,
|
||||
}
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h getCourseHandler) Handle(ctx context.Context, query GetCourse) (domain.Course, error) {
|
||||
course, err := h.repo.Get(ctx, query.ID)
|
||||
if err != nil {
|
||||
return domain.Course{}, fmt.Errorf("getting course: %w", err)
|
||||
}
|
||||
|
||||
return course, nil
|
||||
}
|
||||
45
internal/kurious/app/query/listcourses.go
Normal file
45
internal/kurious/app/query/listcourses.go
Normal file
@ -0,0 +1,45 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListCourse struct {
|
||||
CategoryID string
|
||||
OrganizationID string
|
||||
Keyword string
|
||||
}
|
||||
|
||||
type ListCourseHandler decorator.QueryHandler[ListCourse, []domain.Course]
|
||||
|
||||
type listCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
}
|
||||
|
||||
func NewListCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) ListCourseHandler {
|
||||
h := listCourseHandler{
|
||||
repo: repo,
|
||||
}
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) ([]domain.Course, error) {
|
||||
courses, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||
CategoryID: query.CategoryID,
|
||||
OrganizationID: query.OrganizationID,
|
||||
Keyword: query.Keyword,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing courses: %w", err)
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
}
|
||||
42
internal/kurious/domain/course.go
Normal file
42
internal/kurious/domain/course.go
Normal file
@ -0,0 +1,42 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
)
|
||||
|
||||
// Course is a main entity of this project.
|
||||
type Course struct {
|
||||
// ID is our unique identifier
|
||||
ID string
|
||||
// ExternalID if exists
|
||||
ExternalID nullable.Value[string]
|
||||
SourceType SourceType
|
||||
SourceName nullable.Value[string]
|
||||
// OrganizationID that provides course.
|
||||
OrganizationID string
|
||||
// Link to the course
|
||||
OriginLink string
|
||||
ImageLink string
|
||||
|
||||
Name string
|
||||
// Description of the course. Might be html encoded value.
|
||||
// Maybe it's worth to add flag about it.
|
||||
Description string
|
||||
// FullPrice is a course full price without discount.
|
||||
FullPrice float64
|
||||
// Discount for the course.
|
||||
Discount float64
|
||||
Keywords []string
|
||||
|
||||
// Duration for the course. It will be splitted in values like:
|
||||
// full month / full day / full hour.
|
||||
Duration time.Duration
|
||||
// StartsAt points to time when the course will start.
|
||||
StartsAt time.Time
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt nullable.Value[time.Time]
|
||||
}
|
||||
19
internal/kurious/domain/kurious.go
Normal file
19
internal/kurious/domain/kurious.go
Normal file
@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
// SourceType defines the method this course was added.
|
||||
type SourceType uint8
|
||||
|
||||
const (
|
||||
// SourceTypeUnset should be treated as invalid value.
|
||||
SourceTypeUnset SourceType = iota
|
||||
// SourceTypeManual defines this course was not parsed and
|
||||
// has been added manually.
|
||||
SourceTypeManual
|
||||
// SourceTypeParsed defines this course was parsed
|
||||
SourceTypeParsed
|
||||
)
|
||||
|
||||
type Category struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
22
internal/kurious/domain/organization.go
Normal file
22
internal/kurious/domain/organization.go
Normal file
@ -0,0 +1,22 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
)
|
||||
|
||||
// Organization is an entity that can have coursed.
|
||||
type Organization struct {
|
||||
ID string
|
||||
ExternalID nullable.Value[string]
|
||||
|
||||
Alias string
|
||||
Name string
|
||||
Site string
|
||||
LogoLink string
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt nullable.Value[time.Time]
|
||||
}
|
||||
65
internal/kurious/domain/repository.go
Normal file
65
internal/kurious/domain/repository.go
Normal file
@ -0,0 +1,65 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
)
|
||||
|
||||
type ListCoursesParams struct {
|
||||
OrganizationID string
|
||||
CategoryID string
|
||||
Keyword string
|
||||
}
|
||||
|
||||
type CreateCourseParams struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExternalID nullable.Value[string]
|
||||
SourceType SourceType
|
||||
SourceName nullable.Value[string]
|
||||
OrganizationID string
|
||||
OriginLink string
|
||||
ImageLink string
|
||||
FullPrice float64
|
||||
Discount float64
|
||||
Duration time.Duration
|
||||
StartsAt time.Time
|
||||
}
|
||||
|
||||
//go:generate mockery --name CourseRepository
|
||||
type CourseRepository interface {
|
||||
// List courses by specifid parameters.
|
||||
List(ctx context.Context, params ListCoursesParams) ([]Course, error)
|
||||
// Get course by id.
|
||||
// Should return ErrNotFound in case course not found.
|
||||
Get(ctx context.Context, id string) (Course, error)
|
||||
// GetByExternalID finds course by external id.
|
||||
// Should return ErrNotFound in case course not found.
|
||||
GetByExternalID(ctx context.Context, id string) (Course, error)
|
||||
|
||||
// Create course, but might fail in case of
|
||||
// unique constraint violation.
|
||||
Create(context.Context, CreateCourseParams) (Course, error)
|
||||
// Delete course by id.
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type CreateOrganizationParams struct {
|
||||
ID string
|
||||
ExternalID nullable.Value[string]
|
||||
|
||||
Alias string
|
||||
Name string
|
||||
Site string
|
||||
LogoLink string
|
||||
}
|
||||
|
||||
//go:generate mockery --name OrganizationRepository
|
||||
type OrganizationRepository interface {
|
||||
Get(ctx context.Context) (Organization, error)
|
||||
Create(context.Context, CreateOrganizationParams) (Organization, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
59
internal/kurious/service/service.go
Normal file
59
internal/kurious/service/service.go
Normal file
@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
)
|
||||
|
||||
type ApplicationConfig struct {
|
||||
LogConfig config.LogConfig
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
app.Application
|
||||
|
||||
log *slog.Logger
|
||||
closers []io.Closer
|
||||
}
|
||||
|
||||
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
||||
log := config.NewSLogger(cfg.LogConfig)
|
||||
courseadapter, err := adapters.NewYDBCourseRepository()
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("making ydb course repository: %w", err)
|
||||
}
|
||||
|
||||
application := app.Application{
|
||||
Commands: app.Commands{
|
||||
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
||||
DeleteCourse: command.NewDeleteCourseHandler(courseadapter, log),
|
||||
},
|
||||
Queries: app.Queries{
|
||||
GetCourse: query.NewGetCourseHandler(courseadapter, log),
|
||||
ListCourses: query.NewListCourseHandler(courseadapter, log),
|
||||
},
|
||||
}
|
||||
|
||||
out := Application{Application: application}
|
||||
out.closers = append(out.closers, courseadapter)
|
||||
out.log = log
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (app Application) Close() {
|
||||
for _, closer := range app.closers {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
app.log.Error("unable to close closer", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user