// Package guestbook keeps the messages, checks new ones, and saves them to a // JSON file. It knows nothing about the web. package guestbook import ( "encoding/json" "errors" "fmt" "os" "slices" "strings" "sync" "unicode/utf8" ) const MaxNameLength = 40 const MaxTextLength = 500 // Message is one entry. The fields are exported because encoding/json and // html/template can only see exported fields. type Message struct { Name string Text string } // NewMessage trims and checks the values. Its error is safe to show a visitor. func NewMessage(name string, text string) (Message, error) { name = strings.TrimSpace(name) text = strings.TrimSpace(text) if name == "" { return Message{}, errors.New("Please enter your name.") } if text == "" { return Message{}, errors.New("Please enter a message.") } if utf8.RuneCountInString(name) > MaxNameLength { return Message{}, fmt.Errorf("The name can have at most %d characters.", MaxNameLength) } if utf8.RuneCountInString(text) > MaxTextLength { return Message{}, fmt.Errorf("The message can have at most %d characters.", MaxTextLength) } return Message{Name: name, Text: text}, nil } // Book holds the messages. Make one with Open. Its methods may be called by // many requests at the same time. type Book struct { mu sync.Mutex filename string messages []Message } // Open loads the book from a file. A file that does not exist yet gives an empty // book. A file that cannot be read or understood is an error. func Open(filename string) (*Book, error) { data, err := os.ReadFile(filename) if errors.Is(err, os.ErrNotExist) { return &Book{filename: filename, messages: []Message{}}, nil } if err != nil { return nil, err } var messages []Message err = json.Unmarshal(data, &messages) if err != nil { return nil, fmt.Errorf("%s is not valid: %v", filename, err) } return &Book{filename: filename, messages: messages}, nil } // Messages returns a copy of the messages, oldest first. func (b *Book) Messages() []Message { b.mu.Lock() defer b.mu.Unlock() return slices.Clone(b.messages) } // Add saves a message. If saving fails, the book stays as it was. func (b *Book) Add(message Message) error { b.mu.Lock() defer b.mu.Unlock() updated := append(slices.Clone(b.messages), message) err := save(b.filename, updated) if err != nil { return err } b.messages = updated return nil } // save writes a temporary file, then renames it over the real one. func save(filename string, messages []Message) error { data, err := json.MarshalIndent(messages, "", " ") if err != nil { return err } temporary := filename + ".tmp" err = os.WriteFile(temporary, data, 0644) if err != nil { return err } return os.Rename(temporary, filename) }