Last active
October 7, 2019 23:54
-
-
Save ericelsken/0a7360f54dce4ab071dfe7de02a435aa to your computer and use it in GitHub Desktop.
User Function Options
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package user | |
import ( | |
"bytes" | |
"errors" | |
"fmt" | |
"time" | |
"github.com/google/uuid" | |
) | |
var ( | |
ErrUnsetId = errors.New("user: unset Id") | |
ErrInvalidDisplayName = errors.New("user: invalid DisplayName") | |
ErrUnsetCreatedAt = errors.New("user: unset CreatedAt") | |
) | |
type CannotSetOptionError struct { | |
OptionName string | |
} | |
func (e *CannotSetOptionError) Error() string { | |
return fmt.Sprintf("user: cannot set option %s", e.OptionName) | |
} | |
type User struct { | |
isConstructing bool | |
id uuid.UUID | |
displayName string | |
createdAt time.Time | |
} | |
func NewDisplayName(displayName string) (*User, error) { | |
id, err := uuid.NewRandom() | |
if err != nil { | |
return nil, err | |
} | |
return New( | |
Id(id), | |
DisplayName(displayName), | |
CreatedAt(time.Now()), | |
) | |
} | |
func New(options ...Option) (*User, error) { | |
u := &User{ | |
isConstructing: true, | |
} | |
if err := u.with(options...); err != nil { | |
return nil, err | |
} | |
u.isConstructing = false | |
return u, nil | |
} | |
func (u *User) With(options ...Option) (*User, error) { | |
result := &User{} | |
*result = *u | |
if err := result.with(options...); err != nil { | |
return nil, err | |
} | |
return result, nil | |
} | |
func (u *User) with(options ...Option) error { | |
for _, option := range options { | |
if err := option(u); err != nil { | |
return err | |
} | |
} | |
if err := u.validate(); err != nil { | |
return err | |
} | |
return nil | |
} | |
func (u *User) validate() error { | |
if isIdEmpty(u.id) { | |
return ErrUnsetId | |
} | |
if len(u.displayName) == 0 { | |
return ErrInvalidDisplayName | |
} | |
if u.createdAt.IsZero() { | |
return ErrUnsetCreatedAt | |
} | |
return nil | |
} | |
func (u *User) Id() uuid.UUID { | |
return u.id | |
} | |
func (u *User) DisplayName() string { | |
return u.displayName | |
} | |
func (u *User) CreatedAt() time.Time { | |
return u.createdAt | |
} | |
type Option func(u *User) error | |
func Id(id uuid.UUID) Option { | |
return func(u *User) error { | |
if !u.isConstructing { | |
return &CannotSetOptionError{OptionName: "Id"} | |
} | |
u.id = id | |
return nil | |
} | |
} | |
func DisplayName(displayName string) Option { | |
return func(u *User) error { | |
u.displayName = displayName | |
return nil | |
} | |
} | |
func CreatedAt(createdAt time.Time) Option { | |
return func(u *User) error { | |
if !u.isConstructing { | |
return &CannotSetOptionError{OptionName: "CreatedAt"} | |
} | |
u.createdAt = createdAt | |
return nil | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package user | |
import ( | |
"testing" | |
"time" | |
"github.com/google/uuid" | |
) | |
var ( | |
validId, _ = uuid.NewRandom() | |
validDisplayName = "Valid Display Name" | |
validCreatedAt = time.Now() | |
) | |
var ( | |
validOptions = []Option{ | |
Id(validId), | |
DisplayName(validDisplayName), | |
CreatedAt(validCreatedAt), | |
} | |
) | |
func TestNewDisplayName_ReturnsUserWithSetDisplayName(t *testing.T) { | |
user, err := NewDisplayName("dn") | |
if err != nil { | |
t.Fatal(err) | |
} | |
if user.DisplayName() != "dn" { | |
t.Fatal() | |
} | |
} | |
func TestNew_ReturnsUserAndNilErrorWithValidOptions(t *testing.T) { | |
user, err := New(validOptions...) | |
if err != nil { | |
t.Fatal(err) | |
} | |
if user == nil { | |
t.Fatal(user) | |
} | |
} | |
func TestNew_ReturnsInvalidErrorForAppropriateOptions(t *testing.T) { | |
cases := []struct { | |
invalidOption Option | |
err error | |
}{ | |
{Id([16]byte{}), ErrUnsetId}, | |
{DisplayName(""), ErrInvalidDisplayName}, | |
{CreatedAt(time.Time{}), ErrUnsetCreatedAt}, | |
} | |
for i, tc := range cases { | |
_, err := New(append(validOptions, tc.invalidOption)...) | |
if err != tc.err { | |
t.Errorf("%d: err = %v WANT %v", i, err, tc.err) | |
} | |
} | |
} | |
func TestUser_With_ErrorsWithAppropriateCannotSetErrors(t *testing.T) { | |
user, _ := New(validOptions...) | |
cases := []struct { | |
cannotSetOption Option | |
optionName string | |
}{ | |
{Id(validId), "Id"}, | |
{CreatedAt(time.Now()), "CreatedAt"}, | |
} | |
for i, tc := range cases { | |
result, err := user.With(tc.cannotSetOption) | |
if csError, _ := err.(*CannotSetOptionError); csError.OptionName != tc.optionName { | |
t.Errorf("%d: wrong error %v", i, err) | |
} | |
if result != nil { | |
t.Errorf("%d: result not nil", i) | |
} | |
} | |
} | |
func TestUser_With_AllowsSettingAppropriateOptions(t *testing.T) { | |
user, _ := New(validOptions...) | |
cases := []struct { | |
setOption Option | |
}{ | |
{DisplayName("name")}, | |
} | |
for i, tc := range cases { | |
result, err := user.With(tc.setOption) | |
if err != nil { | |
t.Errorf("%d: err = %v", i, err) | |
} | |
if result == user { | |
t.Fatalf("%d: result equals user", i) | |
} | |
} | |
} | |
func TestUser_Id_ReturnsTheSetId(t *testing.T) { | |
id, _ := uuid.NewRandom() | |
user, _ := New(append(validOptions, Id(id))...) | |
if !areIdsEqual(user.Id(), id) { | |
t.Fatal() | |
} | |
} | |
func TestUser_DisplayName_ReturnsTheSetDisplayName(t *testing.T) { | |
user, _ := New(append(validOptions, DisplayName(t.Name()))...) | |
if user.DisplayName() != t.Name() { | |
t.Fatal() | |
} | |
} | |
func TestUser_CreatedAt_ReturnsTheSetCreatedAt(t *testing.T) { | |
now := time.Now() | |
user, _ := New(append(validOptions, CreatedAt(now))...) | |
if !user.CreatedAt().Equal(now) { | |
t.Fatal() | |
} | |
} | |
func areIdsEqual(a, b uuid.UUID) bool { | |
return bytes.Equal([]byte(a[:]), []byte(b[:])) | |
} | |
func isIdEmpty(id uuid.UUID) bool { | |
empty := [16]byte{} | |
return bytes.Equal([]byte(id[:]), empty[:]) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment