From f60ebcfb36696e036d62dea3d2a06a0f57c07a73 Mon Sep 17 00:00:00 2001 From: Gitea Date: Sun, 17 Dec 2023 10:01:49 +0300 Subject: [PATCH] add simple http --- go.mod | 1 + go.sum | 2 + internal/common/config/http.go | 5 + .../kurious/adapters/ydb_course_repository.go | 137 ++++++++++++++---- internal/kurious/app/query/listcourses.go | 11 +- internal/kurious/domain/repository.go | 15 +- internal/kurious/ports/http/course.go | 118 +++++++++++++++ internal/kurious/ports/http/server.go | 54 ++++++- .../kurious/ports/http/templates/list.tmpl | 86 +++++++++++ 9 files changed, 393 insertions(+), 36 deletions(-) create mode 100644 internal/common/config/http.go create mode 100644 internal/kurious/ports/http/course.go create mode 100644 internal/kurious/ports/http/templates/list.tmpl diff --git a/go.mod b/go.mod index a713b10..f2442db 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect diff --git a/go.sum b/go.sum index cc06d98..2d99fa4 100644 --- a/go.sum +++ b/go.sum @@ -572,6 +572,8 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= diff --git a/internal/common/config/http.go b/internal/common/config/http.go new file mode 100644 index 0000000..680a634 --- /dev/null +++ b/internal/common/config/http.go @@ -0,0 +1,5 @@ +package config + +type HTTP struct { + ListenAddr string `json:"listen_addr"` +} diff --git a/internal/kurious/adapters/ydb_course_repository.go b/internal/kurious/adapters/ydb_course_repository.go index 76e6a4a..1881114 100644 --- a/internal/kurious/adapters/ydb_course_repository.go +++ b/internal/kurious/adapters/ydb_course_repository.go @@ -5,6 +5,8 @@ import ( "fmt" "log/slog" "path" + "strings" + "text/template" "time" "git.loyso.art/frx/kurious/internal/common/config" @@ -79,39 +81,90 @@ type ydbCourseRepository struct { log *slog.Logger } -func (r *ydbCourseRepository) List(ctx context.Context, params domain.ListCoursesParams) (courses []domain.Course, err error) { +func (r *ydbCourseRepository) List( + ctx context.Context, + params domain.ListCoursesParams, +) (result domain.ListCoursesResult, err error) { const limit = 1000 const queryName = "list" - const query = ` -DECLARE $limit AS Int32; -DECLARE $id AS Text; -SELECT - id, - external_id, - source_type, - source_name, - course_thematic, - learning_type, - organization_id, - origin_link, - image_link, - name, - description, - full_price, - discount, - duration, - starts_at, - created_at, - updated_at, - deleted_at -FROM - courses -WHERE - id > $id -ORDER BY id -LIMIT $limit;` + // const query = ` + // DECLARE $limit AS Int32; + // DECLARE $id AS Text; + // SELECT + // id, + // external_id, + // source_type, + // source_name, + // course_thematic, + // learning_type, + // organization_id, + // origin_link, + // image_link, + // name, + // description, + // full_price, + // discount, + // duration, + // starts_at, + // created_at, + // updated_at, + // deleted_at + // FROM + // courses + // WHERE + // id > $id + // ORDER BY id + // LIMIT $limit;` + // + const fields = `id, external_id, source_type, source_name, course_thematic, learning_type, organization_id, origin_link, image_link, name, description, full_price, discount, duration, starts_at, created_at, updated_at, deleted_at` - courses = make([]domain.Course, 0, 4_000) + if params.Limit == 0 { + params.Limit = limit + } + + qtParams := queryTemplateParams{ + Fields: fields, + Table: "courses", + Suffix: "ORDER BY id\nLIMIT $limit", + Declares: []queryTemplateDeclaration{{ + Name: "limit", + Type: "Int32", + }, { + Name: "id", + Type: "Text", + }}, + Conditions: []string{ + "id > $id", + }, + } + + options := make([]table.ParameterOption, 0, 4) + appendParams := func(name string, value string) { + if value == "" { + return + } + + ydbvalue := types.TextValue(value) + d := queryTemplateDeclaration{ + Name: name, + Type: ydbvalue.Type().String(), + } + qtParams.Declares = append(qtParams.Declares, d) + qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg()) + options = append(options, table.ValueParam(d.Arg(), ydbvalue)) + } + appendParams("course_thematic", params.CourseThematic) + appendParams("learning_type", params.LearningType) + + var sb strings.Builder + err = template.Must(template.New("").Parse(queryTemplateSelect)).Execute(&sb, qtParams) + if err != nil { + return result, fmt.Errorf("executing template: %w", err) + } + + query := sb.String() + + courses := make([]domain.Course, 0, 1_000) readTx := table.TxControl( table.BeginTx( table.WithOnlineReadOnly(), @@ -508,3 +561,27 @@ func mapSlice[T, U any](in []T, f func(T) U) []U { return out } + +type queryTemplateDeclaration struct { + Name string + Type string +} + +func (d queryTemplateDeclaration) Arg() string { + return "$" + d.Name +} + +type queryTemplateParams struct { + Declares []queryTemplateDeclaration + Fields string + Table string + Conditions []string + Suffix string +} + +const queryTemplateSelect = ` +{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}}\n{{end}} +SELECT {{.Fields}} +FROM {{.Table}} +WHERE {{ range .Conditions }}{{.}}\n{{end}} +{{.Suffix}}` diff --git a/internal/kurious/app/query/listcourses.go b/internal/kurious/app/query/listcourses.go index 6c141ea..73f77d4 100644 --- a/internal/kurious/app/query/listcourses.go +++ b/internal/kurious/app/query/listcourses.go @@ -10,9 +10,14 @@ import ( ) type ListCourse struct { + CourseThematic string + LearningType string CategoryID string OrganizationID string Keyword string + + Limit int + Offset int } type ListCourseHandler decorator.QueryHandler[ListCourse, []domain.Course] @@ -33,9 +38,11 @@ func NewListCourseHandler( func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) ([]domain.Course, error) { courses, err := h.repo.List(ctx, domain.ListCoursesParams{ - CategoryID: query.CategoryID, + CourseThematic: query.CourseThematic, + LearningType: query.LearningType, OrganizationID: query.OrganizationID, - Keyword: query.Keyword, + Limit: query.Limit, + Offset: query.Offset, }) if err != nil { return nil, fmt.Errorf("listing courses: %w", err) diff --git a/internal/kurious/domain/repository.go b/internal/kurious/domain/repository.go index b02afd9..4136db7 100644 --- a/internal/kurious/domain/repository.go +++ b/internal/kurious/domain/repository.go @@ -8,9 +8,13 @@ import ( ) type ListCoursesParams struct { + LearningType string + CourseThematic string OrganizationID string - CategoryID string - Keyword string + + NextPageToken string + Limit int + Offset int } type CreateCourseParams struct { @@ -31,10 +35,15 @@ type CreateCourseParams struct { StartsAt time.Time } +type ListCoursesResult struct { + Courses []Course + NextPageToken string +} + //go:generate mockery --name CourseRepository type CourseRepository interface { // List courses by specifid parameters. - List(ctx context.Context, params ListCoursesParams) ([]Course, error) + List(ctx context.Context, params ListCoursesParams) (ListCoursesResult, error) // Get course by id. // Should return ErrNotFound in case course not found. Get(ctx context.Context, id string) (Course, error) diff --git a/internal/kurious/ports/http/course.go b/internal/kurious/ports/http/course.go new file mode 100644 index 0000000..ce32958 --- /dev/null +++ b/internal/kurious/ports/http/course.go @@ -0,0 +1,118 @@ +package http + +import ( + "html/template" + "log/slog" + "net/http" + "strconv" + + "git.loyso.art/frx/kurious/internal/common/errors" + "git.loyso.art/frx/kurious/internal/common/xslice" + "git.loyso.art/frx/kurious/internal/kurious/app" + "git.loyso.art/frx/kurious/internal/kurious/app/query" + "git.loyso.art/frx/kurious/internal/kurious/domain" +) + +type courseServer struct { + app app.Application + log *slog.Logger +} + +type pagination struct { + page int + perPage int +} + +func (p pagination) LimitOffset() (limit, offset int) { + if p.page < 0 { + p.page = 0 + } + + if p.perPage <= 0 { + p.perPage = defaultPerPage + } + + return p.perPage, p.page * p.perPage +} + +func parsePaginationFromQuery(r *http.Request) (out pagination, err error) { + query := r.URL.Query() + + if query.Has("page") { + out.page, err = strconv.Atoi(query.Get("page")) + if err != nil { + return out, errors.NewValidationError("page", "bad page value") + } + } + + if query.Has("per_page") { + out.perPage, err = strconv.Atoi(query.Get("per_page")) + if err != nil { + return out, errors.NewValidationError("per_page", "bad per_page value") + } + } else { + out.perPage = 50 + } + + return out, nil +} + +func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) { + out.pagination, err = parsePaginationFromQuery(r) + if err != nil { + return out, err + } + + query := r.URL.Query() + out.learningType = query.Get("category") + out.courseThematic = query.Get("type") + + return out, nil +} + +type listCoursesParams struct { + pagination + + courseThematic string + learningType string +} + +type templateCourse domain.Course + +func mapDomainCourseToTemplate(in ...domain.Course) []templateCourse { + return xslice.Map(in, func(v domain.Course) templateCourse { + return templateCourse(v) + }) +} + +func (c courseServer) List(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + params, err := parseListCoursesParams(r) + if err != nil { + handleError(ctx, err, w, c.log, "unable to parse list courses params") + return + } + + limit, offset := params.LimitOffset() + + courses, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{ + CourseThematic: params.courseThematic, + LearningType: params.learningType, + Limit: limit, + Offset: offset, + }) + if err != nil { + handleError(ctx, err, w, c.log, "unable to list courses") + } + + templateCourses := mapDomainCourseToTemplate(courses...) + + err = template.Must(template.ParseFiles("templates/list.tmpl")).Execute(w, templateCourses) + if err != nil { + handleError(ctx, err, w, c.log, "unable to execute template") + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/internal/kurious/ports/http/server.go b/internal/kurious/ports/http/server.go index 6d0cf3b..074bced 100644 --- a/internal/kurious/ports/http/server.go +++ b/internal/kurious/ports/http/server.go @@ -1,3 +1,55 @@ package http -type Server struct{} +import ( + "context" + stderrors "errors" + "log/slog" + "net/http" + + "git.loyso.art/frx/kurious/internal/common/errors" + "git.loyso.art/frx/kurious/internal/common/xcontext" + "git.loyso.art/frx/kurious/internal/kurious/app" +) + +const ( + defaultPerPage = 50 +) + +type Server struct { + app app.Application +} + +func NewServer(app app.Application) Server { + return Server{} +} + +func (s Server) Courses() courseServer { + return courseServer{ + app: s.app, + } +} + +func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) { + if err == nil { + return + } + + var errorString string + var code int + valErr := new(errors.ValidationError) + switch { + case stderrors.As(err, &valErr): + errorString = valErr.Error() + code = http.StatusBadRequest + case stderrors.Is(err, errors.ErrNotFound): + errorString = err.Error() + code = http.StatusNotFound + default: + errorString = "internal server error" + code = http.StatusInternalServerError + } + + xcontext.LogWithWarnError(ctx, log, err, msg, slog.Int("status_code", code), slog.String("response", errorString)) + + http.Error(w, errorString, code) +} diff --git a/internal/kurious/ports/http/templates/list.tmpl b/internal/kurious/ports/http/templates/list.tmpl new file mode 100644 index 0000000..54fed9e --- /dev/null +++ b/internal/kurious/ports/http/templates/list.tmpl @@ -0,0 +1,86 @@ +{{define "courses"}} + + + + Courses + + + +
+

My Product

+ +
+

Courses

+ {{range $category, $courses := .Courses}} +

{{$category}}

+

{{$category.Description}}

+ {{range $course := $courses}} +
+

{{$course.Name}}

+

{{$course.Description}}

+

Full price: {{$course.FullPrice}}

+

Discount: {{$course.Discount}}

+

Thematic: {{$course.Thematic}}

+

Learning type: {{$course.LearningType}}

+

Duration: {{$course.Duration}}

+

Starts at: {{$course.StartsAt}}

+
+ {{end}} + {{end}} + + +{{end}}