Skip to content

Commit

Permalink
Merge pull request #89 from elh/unsettled
Browse files Browse the repository at this point in the history
Hydrate a calculated unsettled_centipoints field on users
  • Loading branch information
elh authored Jun 30, 2023
2 parents 2a975d7 + 07b15b2 commit e158f04
Show file tree
Hide file tree
Showing 10 changed files with 421 additions and 317 deletions.
591 changes: 301 additions & 290 deletions api/bettor/v1alpha/bettor.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/bettor/v1alpha/bettor.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/bettor/v1alpha/bettor.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ message User {
pattern: "^[a-zA-Z0-9_]+$"
}];
uint64 centipoints = 5;
uint64 unsettled_centipoints = 6; // virtual field hydrated on read
}

// A betting market.
Expand Down
1 change: 1 addition & 0 deletions docs/bettor.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ User information.
| updated_at | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| username | [string](#string) | | |
| centipoints | [uint64](#uint64) | | |
| unsettled_centipoints | [uint64](#uint64) | | virtual field hydrated on read |



Expand Down
7 changes: 7 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,13 @@ <h3 id="bettor.v1alpha.User">User</h3>
<td><p> </p></td>
</tr>

<tr>
<td>unsettled_centipoints</td>
<td><a href="#uint64">uint64</a></td>
<td></td>
<td><p>virtual field hydrated on read </p></td>
</tr>

</tbody>
</table>

Expand Down
17 changes: 1 addition & 16 deletions internal/app/bettor/discord/bettor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package discord
import (
"context"

"github.com/bufbuild/connect-go"
"github.com/bwmarrin/discordgo"
api "github.com/elh/bettor/api/bettor/v1alpha"
)

var getBettorCommand = &discordgo.ApplicationCommand{
Expand All @@ -26,20 +24,7 @@ func GetBettor(ctx context.Context, client bettorClient) Handler {
return nil, CErr("Failed to get or create new user", err)
}

resp, err := client.ListBets(ctx, &connect.Request[api.ListBetsRequest]{Msg: &api.ListBetsRequest{
Book: guildBookName(guildID),
User: bettorUser.GetName(),
ExcludeSettled: true,
}})
if err != nil {
return nil, CErr("Failed to list bets", err)
}
var unsettledCentipoints uint64
for _, b := range resp.Msg.GetBets() {
unsettledCentipoints += b.GetCentipoints()
}

msgformat, margs := formatUser(bettorUser, unsettledCentipoints)
msgformat, margs := formatUser(bettorUser, bettorUser.UnsettledCentipoints)
msgformat = "🎲 👤\n\n" + msgformat
return &discordgo.InteractionResponseData{Content: localized.Sprintf(msgformat, margs...)}, nil
}
Expand Down
45 changes: 42 additions & 3 deletions internal/app/bettor/repo/mem/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
api "github.com/elh/bettor/api/bettor/v1alpha"
"github.com/elh/bettor/internal/app/bettor/entity"
"github.com/elh/bettor/internal/app/bettor/repo"
"google.golang.org/protobuf/proto"
)

var _ repo.Repo = (*Repo)(nil)
Expand All @@ -24,11 +25,35 @@ type Repo struct {
betMtx sync.RWMutex
}

// hydrate virtual fields like unsettled_centipoints.
func (r *Repo) hydrateUser(ctx context.Context, user *api.User) (*api.User, error) {
bookID, _ := entity.UserIDs(user.GetName())
bets, _, err := r.ListBets(ctx, &repo.ListBetsArgs{
Book: entity.BookN(bookID),
User: user.GetName(),
ExcludeSettled: true,
Limit: 1000, // no pagination here
})
if err != nil {
return nil, err
}

var unsettledCentipoints uint64
for _, b := range bets {
unsettledCentipoints += b.GetCentipoints()
}
userCopy := proto.Clone(user).(*api.User)
userCopy.UnsettledCentipoints = unsettledCentipoints
return userCopy, nil
}

// CreateUser creates a new user.
func (r *Repo) CreateUser(_ context.Context, user *api.User) error {
r.userMtx.Lock()
defer r.userMtx.Unlock()

user.UnsettledCentipoints = 0 // defensive

bookID, _ := entity.UserIDs(user.GetName())
for _, u := range r.Users {
if u.GetName() == user.GetName() {
Expand Down Expand Up @@ -67,33 +92,41 @@ func (r *Repo) UpdateUser(_ context.Context, user *api.User) error {
}

// GetUser gets a user by ID.
func (r *Repo) GetUser(_ context.Context, name string) (*api.User, error) {
func (r *Repo) GetUser(ctx context.Context, name string) (*api.User, error) {
r.userMtx.RLock()
defer r.userMtx.RUnlock()
for _, u := range r.Users {
if u.GetName() == name {
u, err := r.hydrateUser(ctx, u)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return u, nil
}
}
return nil, connect.NewError(connect.CodeNotFound, errors.New("user not found"))
}

// GetUserByUsername gets a user by username.
func (r *Repo) GetUserByUsername(_ context.Context, book, username string) (*api.User, error) {
func (r *Repo) GetUserByUsername(ctx context.Context, book, username string) (*api.User, error) {
r.userMtx.RLock()
defer r.userMtx.RUnlock()
bookID := entity.BooksIDs(book)
for _, u := range r.Users {
uBookID, _ := entity.UserIDs(u.GetName())
if uBookID == bookID && u.Username == username {
u, err := r.hydrateUser(ctx, u)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return u, nil
}
}
return nil, connect.NewError(connect.CodeNotFound, errors.New("user not found"))
}

// ListUsers lists users by filters.
func (r *Repo) ListUsers(_ context.Context, args *repo.ListUsersArgs) (users []*api.User, hasMore bool, err error) {
func (r *Repo) ListUsers(ctx context.Context, args *repo.ListUsersArgs) (users []*api.User, hasMore bool, err error) {
r.userMtx.RLock()
defer r.userMtx.RUnlock()
bookID := entity.BooksIDs(args.Book)
Expand All @@ -109,6 +142,12 @@ func (r *Repo) ListUsers(_ context.Context, args *repo.ListUsersArgs) (users []*
if len(args.Users) > 0 && !containsStr(args.Users, u.GetName()) {
continue
}
// hydrate
u, err := r.hydrateUser(ctx, u)
if err != nil {
return nil, false, connect.NewError(connect.CodeInternal, errors.New("failed to compute unsettled points"))
}

out = append(out, u)
if len(out) >= args.Limit+1 {
break
Expand Down
2 changes: 2 additions & 0 deletions internal/app/bettor/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
api "github.com/elh/bettor/api/bettor/v1alpha"
)

// NOTE: same models E2E from API to repo out of laziness

// Repo is a persistence repository.
type Repo interface {
CreateUser(ctx context.Context, user *api.User) error
Expand Down
1 change: 1 addition & 0 deletions internal/app/bettor/server/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (s *Server) CreateUser(ctx context.Context, in *connect.Request[api.CreateU
user.Name = entity.UserN(bookID, uuid.NewString())
user.CreatedAt = timestamppb.Now()
user.UpdatedAt = timestamppb.Now()
user.UnsettledCentipoints = 0

if err := user.Validate(); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
Expand Down
71 changes: 63 additions & 8 deletions internal/app/bettor/server/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@ func TestGetUser(t *testing.T) {
Username: "rusty",
Centipoints: 100,
}
userHydrated := &api.User{
Name: entity.UserN("guild:1", uuid.NewString()),
Username: "linus",
Centipoints: 100,
}
unsettledBet := &api.Bet{
Name: entity.BetN("guild:1", "a"),
User: userHydrated.Name,
Centipoints: 200,
}
unsettledBet2 := &api.Bet{
Name: entity.BetN("guild:1", "b"),
User: userHydrated.Name,
Centipoints: 50,
}
hydratedUserHydrated := proto.Clone(userHydrated).(*api.User)
hydratedUserHydrated.UnsettledCentipoints += unsettledBet.Centipoints
hydratedUserHydrated.UnsettledCentipoints += unsettledBet2.Centipoints
testCases := []struct {
desc string
user string
Expand All @@ -127,6 +145,11 @@ func TestGetUser(t *testing.T) {
user: user.GetName(),
expected: user,
},
{
desc: "hydrate unsettled points",
user: userHydrated.GetName(),
expected: hydratedUserHydrated,
},
{
desc: "fails if user does not exist",
user: "does-not-exist",
Expand All @@ -141,7 +164,7 @@ func TestGetUser(t *testing.T) {
for _, tC := range testCases {
tC := tC
t.Run(tC.desc, func(t *testing.T) {
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user}}))
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user, userHydrated}, Bets: []*api.Bet{unsettledBet, unsettledBet2}}))
require.Nil(t, err)
out, err := s.GetUser(context.Background(), connect.NewRequest(&api.GetUserRequest{Name: tC.user}))
if tC.expectErr {
Expand All @@ -160,6 +183,24 @@ func TestGetUserByUsername(t *testing.T) {
Username: "rusty",
Centipoints: 100,
}
userHydrated := &api.User{
Name: entity.UserN("guild:1", uuid.NewString()),
Username: "linus",
Centipoints: 100,
}
unsettledBet := &api.Bet{
Name: entity.BetN("guild:1", "a"),
User: userHydrated.Name,
Centipoints: 200,
}
unsettledBet2 := &api.Bet{
Name: entity.BetN("guild:1", "b"),
User: userHydrated.Name,
Centipoints: 50,
}
hydratedUserHydrated := proto.Clone(userHydrated).(*api.User)
hydratedUserHydrated.UnsettledCentipoints += unsettledBet.Centipoints
hydratedUserHydrated.UnsettledCentipoints += unsettledBet2.Centipoints
testCases := []struct {
desc string
book string
Expand All @@ -173,6 +214,12 @@ func TestGetUserByUsername(t *testing.T) {
username: "rusty",
expected: user,
},
{
desc: "hydrate unsettled points",
book: entity.BookN("guild:1"),
username: "linus",
expected: hydratedUserHydrated,
},
{
desc: "fails if user does not exist",
book: entity.BookN("guild:1"),
Expand All @@ -189,7 +236,7 @@ func TestGetUserByUsername(t *testing.T) {
for _, tC := range testCases {
tC := tC
t.Run(tC.desc, func(t *testing.T) {
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user}}))
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user, userHydrated}, Bets: []*api.Bet{unsettledBet, unsettledBet2}}))
require.Nil(t, err)
out, err := s.GetUserByUsername(context.Background(), connect.NewRequest(&api.GetUserByUsernameRequest{Book: tC.book, Username: tC.username}))
if tC.expectErr {
Expand All @@ -216,11 +263,19 @@ func TestListUsers(t *testing.T) {
Username: "danny",
Centipoints: 200,
}
// has an unsettled bet
user3 := &api.User{
Name: entity.UserN(bookID, "c"),
Username: "linus",
Centipoints: 300,
}
unsettledBet := &api.Bet{
Name: entity.BetN(bookID, "a"),
User: user3.Name,
Centipoints: 200,
}
user3Hydrated := proto.Clone(user3).(*api.User)
user3Hydrated.UnsettledCentipoints += unsettledBet.Centipoints
testCases := []struct {
desc string
req *api.ListUsersRequest
Expand All @@ -231,7 +286,7 @@ func TestListUsers(t *testing.T) {
{
desc: "basic case",
req: &api.ListUsersRequest{Book: entity.BookN(bookID)},
expected: []*api.User{user1, user2, user3},
expected: []*api.User{user1, user2, user3Hydrated},
expectedCalls: 1,
},
{
Expand All @@ -243,25 +298,25 @@ func TestListUsers(t *testing.T) {
{
desc: "page size 1",
req: &api.ListUsersRequest{Book: entity.BookN(bookID), PageSize: 1},
expected: []*api.User{user1, user2, user3},
expected: []*api.User{user1, user2, user3Hydrated},
expectedCalls: 3,
},
{
desc: "page size 2",
req: &api.ListUsersRequest{Book: entity.BookN(bookID), PageSize: 2},
expected: []*api.User{user1, user2, user3},
expected: []*api.User{user1, user2, user3Hydrated},
expectedCalls: 2,
},
{
desc: "page size 3",
req: &api.ListUsersRequest{Book: entity.BookN(bookID), PageSize: 3},
expected: []*api.User{user1, user2, user3},
expected: []*api.User{user1, user2, user3Hydrated},
expectedCalls: 1,
},
{
desc: "page size 4",
req: &api.ListUsersRequest{Book: entity.BookN(bookID), PageSize: 4},
expected: []*api.User{user1, user2, user3},
expected: []*api.User{user1, user2, user3Hydrated},
expectedCalls: 1,
},
{
Expand All @@ -274,7 +329,7 @@ func TestListUsers(t *testing.T) {
for _, tC := range testCases {
tC := tC
t.Run(tC.desc, func(t *testing.T) {
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user1, user2, user3}}))
s, err := server.New(server.WithRepo(&mem.Repo{Users: []*api.User{user1, user2, user3}, Bets: []*api.Bet{unsettledBet}}))
require.Nil(t, err)
var all []*api.User
var calls int
Expand Down

0 comments on commit e158f04

Please sign in to comment.