From 8bcb8d3f20184501a443658673326eb9cf30e17c Mon Sep 17 00:00:00 2001 From: itzaname Date: Tue, 26 Nov 2024 18:28:48 -0500 Subject: [PATCH] =?UTF-8?q?Refactor=20=F0=9F=98=AE=E2=80=8D=F0=9F=92=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + internal/controller/submissions.go | 130 ------------ internal/controller/users.go | 117 ---------- internal/datastore/gormstore/users.go | 72 ------- internal/model/user.go | 7 - pkg/cmds/root.go | 26 +++ pkg/cmds/serve.go | 42 ++++ {internal => pkg}/datastore/datastore.go | 0 {internal => pkg}/datastore/filter.go | 0 {internal => pkg}/datastore/gormstore/db.go | 0 .../datastore/gormstore/gormstore.go | 0 .../datastore/gormstore/submissions.go | 0 {internal => pkg}/model/page.go | 0 {internal => pkg}/model/submission.go | 0 pkg/service/service.go | 21 ++ pkg/service/submissions.go | 56 +++++ server.go | 200 ------------------ 17 files changed, 146 insertions(+), 526 deletions(-) create mode 100644 .gitignore delete mode 100644 internal/controller/submissions.go delete mode 100644 internal/controller/users.go delete mode 100644 internal/datastore/gormstore/users.go delete mode 100644 internal/model/user.go create mode 100644 pkg/cmds/root.go create mode 100644 pkg/cmds/serve.go rename {internal => pkg}/datastore/datastore.go (100%) rename {internal => pkg}/datastore/filter.go (100%) rename {internal => pkg}/datastore/gormstore/db.go (100%) rename {internal => pkg}/datastore/gormstore/gormstore.go (100%) rename {internal => pkg}/datastore/gormstore/submissions.go (100%) rename {internal => pkg}/model/page.go (100%) rename {internal => pkg}/model/submission.go (100%) create mode 100644 pkg/service/service.go create mode 100644 pkg/service/submissions.go delete mode 100644 server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/internal/controller/submissions.go b/internal/controller/submissions.go deleted file mode 100644 index f7ad849..0000000 --- a/internal/controller/submissions.go +++ /dev/null @@ -1,130 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "git.itzana.me/strafesnet/maps-service/internal/datastore" - "git.itzana.me/strafesnet/maps-service/internal/model" - "git.itzana.me/strafesnet/maps-service/api" - "github.com/pkg/errors" - "time" -) - -type Maps struct { - *maps.UnimplementedMapsServiceServer - Store datastore.Datastore -} - -func (m Maps) Get(ctx context.Context, params *api.GetSubmissionParams) (*api.Submission, error) { - item, err := m.Store.Maps().Get(ctx, params.SubmissionID) - if err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, "map does not exit") - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get map:").Error()) - } - - return &maps.MapResponse{ - ID: item.ID, - DisplayName: item.DisplayName, - Creator: item.Creator, - GameID: item.GameID, - Date: item.Date.Unix(), - }, nil -} - -func (m Maps) GetList(ctx context.Context, list *maps.IdList) (*maps.MapList, error) { - items, err := m.Store.Maps().GetList(ctx, list.ID) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get maps").Error()) - } - - var resp maps.MapList - for i := 0; i < len(items); i++ { - resp.Maps = append(resp.Maps, &maps.MapResponse{ - ID: items[i].ID, - DisplayName: items[i].DisplayName, - Creator: items[i].Creator, - GameID: items[i].GameID, - Date: items[i].Date.Unix(), - }) - } - - return &resp, nil -} - -func (m Maps) Update(ctx context.Context, request *maps.MapRequest) (*maps.NullResponse, error) { - updates := datastore.Optional() - updates.AddNotNil("display_name", request.DisplayName) - updates.AddNotNil("creator", request.Creator) - updates.AddNotNil("game_id", request.GameID) - if request.Date != nil { - updates.AddNotNil("date", time.Unix(request.GetDate(), 0)) - } - - if err := m.Store.Maps().Update(ctx, request.GetID(), updates); err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, "map does not exit") - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to update map:").Error()) - } - - return &maps.NullResponse{}, nil -} - -func (m Maps) Create(ctx context.Context, request *maps.MapRequest) (*maps.IdMessage, error) { - item, err := m.Store.Maps().Create(ctx, model.Map{ - ID: request.GetID(), - DisplayName: request.GetDisplayName(), - Creator: request.GetCreator(), - GameID: request.GetGameID(), - Date: time.Unix(request.GetDate(), 0), - }) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to create map:").Error()) - } - - return &maps.IdMessage{ID: item.ID}, nil -} - -func (m Maps) Delete(ctx context.Context, message *maps.IdMessage) (*maps.NullResponse, error) { - if err := m.Store.Maps().Delete(ctx, message.GetID()); err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, "map does not exit") - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to delete map:").Error()) - } - - return &maps.NullResponse{}, nil -} - -func (m Maps) List(ctx context.Context, request *maps.ListRequest) (*maps.MapList, error) { - filter := datastore.Optional() - fmt.Println(request) - if request.Filter != nil { - filter.AddNotNil("display_name", request.GetFilter().DisplayName) - filter.AddNotNil("creator", request.GetFilter().Creator) - filter.AddNotNil("game_id", request.GetFilter().GameID) - } - - items, err := m.Store.Maps().List(ctx, filter, model.Page{ - Number: request.GetPage().GetNumber(), - Size: request.GetPage().GetSize(), - }) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get maps:").Error()) - } - - var resp maps.MapList - for i := 0; i < len(items); i++ { - resp.Maps = append(resp.Maps, &maps.MapResponse{ - ID: items[i].ID, - DisplayName: items[i].DisplayName, - Creator: items[i].Creator, - GameID: items[i].GameID, - Date: items[i].Date.Unix(), - }) - } - - return &resp, nil -} diff --git a/internal/controller/users.go b/internal/controller/users.go deleted file mode 100644 index 2315636..0000000 --- a/internal/controller/users.go +++ /dev/null @@ -1,117 +0,0 @@ -package controller - -import ( - "context" - "git.itzana.me/strafesnet/maps-service/internal/datastore" - "git.itzana.me/strafesnet/maps-service/internal/model" - "git.itzana.me/strafesnet/go-grpc/users" - "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -type Users struct { - *users.UnimplementedUsersServiceServer - Store datastore.Datastore -} - -func (u Users) Get(ctx context.Context, request *users.IdMessage) (*users.UserResponse, error) { - ur, err := u.Store.Users().Get(ctx, request.ID) - if err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, err.Error()) - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get user").Error()) - } - - return &users.UserResponse{ - ID: ur.ID, - Username: ur.Username, - StateID: ur.StateID, - }, nil -} - -func (u Users) GetList(ctx context.Context, list *users.IdList) (*users.UserList, error) { - uList, err := u.Store.Users().GetList(ctx, list.ID) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get users").Error()) - } - - var resp users.UserList - for i := 0; i < len(uList); i++ { - resp.Users = append(resp.Users, &users.UserResponse{ - ID: uList[i].ID, - Username: uList[i].Username, - StateID: uList[i].StateID, - }) - } - - return &resp, nil -} - -func (u Users) Update(ctx context.Context, request *users.UserRequest) (*users.NullResponse, error) { - updates := datastore.Optional() - updates.AddNotNil("state_id", request.StateID) - updates.AddNotNil("username", request.Username) - - if err := u.Store.Users().Update(ctx, request.GetID(), updates); err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, err.Error()) - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to update user").Error()) - } - - return &users.NullResponse{}, nil -} - -func (u Users) Create(ctx context.Context, request *users.UserRequest) (*users.IdMessage, error) { - us, err := u.Store.Users().Create(ctx, model.User{ - ID: request.GetID(), - Username: request.GetUsername(), - StateID: request.GetStateID(), - }) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to create user").Error()) - } - - return &users.IdMessage{ID: us.ID}, nil -} - -func (u Users) Delete(ctx context.Context, request *users.IdMessage) (*users.NullResponse, error) { - if err := u.Store.Users().Delete(ctx, request.GetID()); err != nil { - if err == datastore.ErrNotExist { - return nil, status.Error(codes.NotFound, err.Error()) - } - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to delete user").Error()) - } - - return &users.NullResponse{}, nil -} - -func (u Users) List(ctx context.Context, request *users.ListRequest) (*users.UserList, error) { - filters := datastore.Optional() - if request.Filter != nil { - filters.AddNotNil("id", request.GetFilter().ID) - filters.AddNotNil("state_id", request.GetFilter().StateID) - filters.AddNotNil("username", request.GetFilter().Username) - } - - uList, err := u.Store.Users().List(ctx, filters, model.Page{ - Number: request.GetPage().GetNumber(), - Size: request.GetPage().GetSize(), - }) - if err != nil { - return nil, status.Error(codes.Internal, errors.Wrap(err, "failed to get filtered users").Error()) - } - - var uResp users.UserList - for i := 0; i < len(uList); i++ { - uResp.Users = append(uResp.Users, &users.UserResponse{ - ID: uList[i].ID, - Username: uList[i].Username, - StateID: uList[i].StateID, - }) - } - - return &uResp, nil -} diff --git a/internal/datastore/gormstore/users.go b/internal/datastore/gormstore/users.go deleted file mode 100644 index 417baca..0000000 --- a/internal/datastore/gormstore/users.go +++ /dev/null @@ -1,72 +0,0 @@ -package gormstore - -import ( - "context" - "git.itzana.me/strafesnet/maps-service/internal/datastore" - "git.itzana.me/strafesnet/maps-service/internal/model" - "gorm.io/gorm" -) - -type Users struct { - db *gorm.DB -} - -func (u Users) Get(ctx context.Context, id int64) (model.User, error) { - var user model.User - if err := u.db.WithContext(ctx).First(&user, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return user, datastore.ErrNotExist - } - return user, err - } - - return user, nil -} - -func (u Users) GetList(ctx context.Context, id []int64) ([]model.User, error) { - var user []model.User - if err := u.db.WithContext(ctx).Find(&user, "id IN ?", id).Error; err != nil { - return user, err - } - - return user, nil -} - -func (u Users) Create(ctx context.Context, user model.User) (model.User, error) { - if err := u.db.WithContext(ctx).Create(&user).Error; err != nil { - return user, err - } - - return user, nil -} - -func (u Users) Update(ctx context.Context, id int64, values datastore.OptionalMap) error { - if err := u.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id).Updates(values.Map()).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return datastore.ErrNotExist - } - return err - } - - return nil -} - -func (u Users) Delete(ctx context.Context, id int64) error { - if err := u.db.WithContext(ctx).Delete(&model.User{}, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return datastore.ErrNotExist - } - return err - } - - return nil -} - -func (u Users) List(ctx context.Context, filters datastore.OptionalMap, page model.Page) ([]model.User, error) { - var users []model.User - if err := u.db.WithContext(ctx).Where(filters.Map()).Offset(int((page.Number - 1) * page.Size)).Limit(int(page.Size)).Find(&users).Error; err != nil { - return nil, err - } - - return users, nil -} diff --git a/internal/model/user.go b/internal/model/user.go deleted file mode 100644 index ba6d80b..0000000 --- a/internal/model/user.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type User struct { - ID int64 - Username string `gorm:"not null"` - StateID int32 `gorm:"not null;default:0"` -} diff --git a/pkg/cmds/root.go b/pkg/cmds/root.go new file mode 100644 index 0000000..cfaa4e3 --- /dev/null +++ b/pkg/cmds/root.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "github.com/urfave/cli/v2" + "os" + "path/filepath" +) + +var ( + appName = filepath.Base(os.Args[0]) + commonFlag = []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + }, + } +) + +func NewApp() *cli.App { + app := cli.NewApp() + app.Name = appName + app.Usage = "Maps API Service" + app.Flags = commonFlag + + return app +} diff --git a/pkg/cmds/serve.go b/pkg/cmds/serve.go new file mode 100644 index 0000000..23df1e3 --- /dev/null +++ b/pkg/cmds/serve.go @@ -0,0 +1,42 @@ +package cmd + +import "github.com/urfave/cli/v2" + +func NewRunCommand() *cli.Command { + return &cli.Command{ + Name: "run", + Usage: "Run maps service", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pg-host", + Usage: "Host of postgres database", + EnvVars: []string{"PG_HOST"}, + Required: true, + }, + &cli.IntFlag{ + Name: "pg-port", + Usage: "Port of postgres database", + EnvVars: []string{"PG_PORT"}, + Required: true, + }, + &cli.StringFlag{ + Name: "pg-db", + Usage: "Name of database to connect to", + EnvVars: []string{"PG_DB"}, + Required: true, + }, + &cli.StringFlag{ + Name: "pg-user", + Usage: "User to connect with", + EnvVars: []string{"PG_USER"}, + Required: true, + }, + &cli.StringFlag{ + Name: "pg-password", + Usage: "Password to connect with", + EnvVars: []string{"PG_PASSWORD"}, + Required: true, + }, + }, + } +} diff --git a/internal/datastore/datastore.go b/pkg/datastore/datastore.go similarity index 100% rename from internal/datastore/datastore.go rename to pkg/datastore/datastore.go diff --git a/internal/datastore/filter.go b/pkg/datastore/filter.go similarity index 100% rename from internal/datastore/filter.go rename to pkg/datastore/filter.go diff --git a/internal/datastore/gormstore/db.go b/pkg/datastore/gormstore/db.go similarity index 100% rename from internal/datastore/gormstore/db.go rename to pkg/datastore/gormstore/db.go diff --git a/internal/datastore/gormstore/gormstore.go b/pkg/datastore/gormstore/gormstore.go similarity index 100% rename from internal/datastore/gormstore/gormstore.go rename to pkg/datastore/gormstore/gormstore.go diff --git a/internal/datastore/gormstore/submissions.go b/pkg/datastore/gormstore/submissions.go similarity index 100% rename from internal/datastore/gormstore/submissions.go rename to pkg/datastore/gormstore/submissions.go diff --git a/internal/model/page.go b/pkg/model/page.go similarity index 100% rename from internal/model/page.go rename to pkg/model/page.go diff --git a/internal/model/submission.go b/pkg/model/submission.go similarity index 100% rename from internal/model/submission.go rename to pkg/model/submission.go diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..0b04d34 --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,21 @@ +package controller + +import ( + "context" + "git.itzana.me/strafesnet/maps-service/pkg/api" + "git.itzana.me/strafesnet/maps-service/pkg/datastore" +) + +type MapsService struct { + db *datastore.Datastore +} + +// NewError creates *ErrorStatusCode from error returned by handler. +// +// Used for common default response. +func NewError(ctx context.Context, err error) *api.ErrorStatusCode { + return &api.ErrorStatusCode{ + StatusCode: 500, + Response: api.Error{Message: err.Error()}, + } +} diff --git a/pkg/service/submissions.go b/pkg/service/submissions.go new file mode 100644 index 0000000..e2313e2 --- /dev/null +++ b/pkg/service/submissions.go @@ -0,0 +1,56 @@ +package controller + +import ( + "context" + "git.itzana.me/strafesnet/maps-service/pkg/api" +) + +// POST /submissions +func CreateSubmission(ctx context.Context) (*api.Submission, error) { + return nil, nil +} + +// GetSubmission implements getSubmission operation. +// +// Retrieve map with ID. +// +// GET /submissions/{SubmissionID} +func GetSubmission(ctx context.Context, params api.GetSubmissionParams) (*api.Submission, error) { + return nil, nil +} + +// ListSubmissions implements listSubmissions operation. +// +// Get list of submissions. +// +// GET /submissions +func ListSubmissions(ctx context.Context, params api.ListSubmissionsParams) ([]api.Submission, error) { + return nil, nil +} + +// PatchSubmissionCompleted implements patchSubmissionCompleted operation. +// +// Retrieve map with ID. +// +// PATCH /submissions/{SubmissionID}/completed +func PatchSubmissionCompleted(ctx context.Context, params api.PatchSubmissionCompletedParams) error { + return nil +} + +// PatchSubmissionModel implements patchSubmissionModel operation. +// +// Update model following role restrictions. +// +// PATCH /submissions/{SubmissionID}/model +func PatchSubmissionModel(ctx context.Context, params api.PatchSubmissionModelParams) error { + return nil +} + +// PatchSubmissionStatus implements patchSubmissionStatus operation. +// +// Update status following role restrictions. +// +// PATCH /submissions/{SubmissionID}/status +func PatchSubmissionStatus(ctx context.Context, params api.PatchSubmissionStatusParams) error { + return nil +} diff --git a/server.go b/server.go deleted file mode 100644 index dd2dd5b..0000000 --- a/server.go +++ /dev/null @@ -1,200 +0,0 @@ -package main - -import ( - "context" - "log" - "net/http" - - "git.itzana.me/strafesnet/maps-service/api" - "git.itzana.me/strafesnet/maps-service/internal/controller/submissions" - "git.itzana.me/strafesnet/maps-service/internal/controller/users" -) - -type apiServer struct { - submissions *submissions.Submissions - users *users.Users -} - -// GetUserRank implements api.Handler. -func (m *apiServer) GetSubmission(ctx context.Context, params api.GetSubmissionParams) (*api.Submission, error) { - return m.submissions.Get(ctx,params) -} - -// NewError implements api.Handler. -func (m *apiServer) NewError(ctx context.Context, err error) *api.ErrorStatusCode { - return &api.ErrorStatusCode{ - StatusCode: 500, - Response: api.Error{Message: err.Error()}, - } -} - -// GetTimes implements api.Handler. -func (m *apiServer) ListTimes(ctx context.Context, params api.ListTimesParams) ([]api.Time, error) { - client := times.NewTimesServiceClient(m.client) - - // Call the List method using params - req := ×.ListRequest{ - Page: ×.Pagination{ - Number: params.Page.GetPage(), - Size: params.Page.GetLimit(), - }, - } - - if filter, ok := params.Filter.Get(); ok { - if id := filter.GetID(); id.IsSet() { - req.Filter.ID = &id.Value - } - if time := filter.GetTime(); time.IsSet() { - req.Filter.Time = &time.Value - } - if userID := filter.GetUserID(); userID.IsSet() { - req.Filter.UserID = &userID.Value - } - if mapID := filter.GetMapID(); mapID.IsSet() { - req.Filter.MapID = &mapID.Value - } - if styleID := filter.GetStyleID(); styleID.IsSet() { - req.Filter.StyleID = &styleID.Value - } - if modeID := filter.GetModeID(); modeID.IsSet() { - req.Filter.ModeID = &modeID.Value - } - if gameID := filter.GetGameID(); gameID.IsSet() { - req.Filter.GameID = &gameID.Value - } - } - - response, err := client.List(ctx, req) - if err != nil { - return nil, err - } - - return convertTimes(response.Times), nil -} - -// GetUser implements api.Handler. -func (m *apiServer) GetUser(ctx context.Context, params api.GetUserParams) (*api.User, error) { - client := users.NewUsersServiceClient(m.client) - - response, err := client.Get(ctx, &users.IdMessage{ - ID: params.UserID, - }) - if err != nil { - return nil, err - } - - return &api.User{ - ID: api.NewOptInt64(response.ID), - Username: api.NewOptString(response.Username), - StateID: api.NewOptInt32(response.StateID), - }, nil -} - -// ListRanks implements api.Handler. -func (m *apiServer) ListRanks(ctx context.Context, params api.ListRanksParams) ([]api.Rank, error) { - client := ranks.NewRanksServiceClient(m.client) - - req := &ranks.ListRequest{ - Page: &ranks.Pagination{ - Number: params.Page.GetPage(), - Size: params.Page.GetLimit(), - }, - } - - if filter, ok := params.Filter.Get(); ok { - if gameID, ok := filter.GetGameID().Get(); ok { - req.GameID = gameID - } - - if modeID, ok := filter.GetModeID().Get(); ok { - req.ModeID = modeID - } - - if styleID, ok := filter.GetStyleID().Get(); ok { - req.StyleID = styleID - } - - if sort, ok := filter.GetSort().Get(); ok { - req.Sort = sort - } - } - - response, err := client.List(ctx, req) - if err != nil { - return nil, err - } - - ranks := make([]api.Rank, len(response.Ranks)) - for i, r := range response.Ranks { - ranks[i] = api.Rank{ - ID: api.NewOptInt64(r.ID), - User: api.NewOptUser(api.User{ - ID: api.NewOptInt64(r.User.ID), - Username: api.NewOptString(r.User.Username), - StateID: api.NewOptInt32(r.User.StateID), - }), - StyleID: api.NewOptInt32(r.StyleID), - ModeID: api.NewOptInt32(r.ModeID), - GameID: api.NewOptInt32(r.GameID), - Rank: api.NewOptFloat64(r.Rank), - Skill: api.NewOptFloat64(r.Skill), - UpdatedAt: api.NewOptInt64(r.UpdatedAt), - } - } - - return ranks, nil -} - -func main() { - // new grpc client - conn, err := grpc.Dial("localhost:9000", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Fatal(err) - } - - svc := &apiServer{ - client: conn, - } - - srv, err := api.NewServer( - svc, - api.WithPathPrefix("/v2"), - ) - if err != nil { - log.Fatal(err) - } - - if err := http.ListenAndServe(":8080", srv); err != nil { - log.Fatal(err) - } -} - -func convertTime(t *times.TimeResponse) api.Time { - return api.Time{ - ID: api.NewOptInt64(t.ID), - Time: api.NewOptInt64(t.Time), - User: api.NewOptUser(api.User{ - ID: api.NewOptInt64(t.User.ID), - Username: api.NewOptString(t.User.Username), - StateID: api.NewOptInt32(t.User.StateID), - }), - Map: api.NewOptMap(api.Map{ - ID: api.NewOptInt64(t.Map.ID), - DisplayName: api.NewOptString(t.Map.DisplayName), - Creator: api.NewOptString(t.Map.Creator), - Date: api.NewOptInt64(t.Map.Date), - }), - Date: api.NewOptInt64(t.Date), - StyleID: api.NewOptInt32(t.StyleID), - ModeID: api.NewOptInt32(t.ModeID), - GameID: api.NewOptInt32(t.GameID), - } -} - -func convertTimes(timeList []*times.TimeResponse) []api.Time { - times := make([]api.Time, len(timeList)) - for i, t := range timeList { - times[i] = convertTime(t) - } - return times -}