Skip to content

Instantly share code, notes, and snippets.

@ericelsken
Last active October 7, 2019 23:54
Show Gist options
  • Save ericelsken/0a7360f54dce4ab071dfe7de02a435aa to your computer and use it in GitHub Desktop.
Save ericelsken/0a7360f54dce4ab071dfe7de02a435aa to your computer and use it in GitHub Desktop.
User Function Options
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
}
}
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