123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- package main
- import (
- "cmp"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "slices"
- "strings"
- "sync"
- )
- var (
- ContactsFile = "contacts.json"
- )
- type (
- Contacts []*Contact
- ContactsStore struct {
- sync.RWMutex
- data map[int]*Contact
- }
- )
- func NewContactsStore() (*ContactsStore, error) {
- cs := ContactsStore{
- data: make(map[int]*Contact),
- }
- if err := cs.Load(); err != nil {
- return nil, err
- }
- return &cs, nil
- }
- func (cs *ContactsStore) Get(search string) Contacts {
- contacts := cs.GetAll()
- if search == "" {
- return contacts
- }
- return slices.DeleteFunc(contacts, func(c *Contact) bool {
- return (c == nil) || ((c.First == "" || !strings.Contains(c.First, search)) &&
- (c.Last == "" || !strings.Contains(c.Last, search)) &&
- (c.Email == "" || !strings.Contains(c.Email, search)) &&
- (c.Phone == "" || !strings.Contains(c.Phone, search)))
- })
- }
- func MapValues[M ~map[K]V, K comparable, V any](m M) []V {
- sl := make([]V, 0, len(m))
- for _, v := range m {
- sl = append(sl, v)
- }
- return sl
- }
- func (cs *ContactsStore) GetAll() Contacts {
- cs.RLock()
- defer cs.RUnlock()
- contacts := MapValues(cs.data)
- slices.SortStableFunc(contacts, func(a, b *Contact) int {
- if sgn := cmp.Compare(a.First, b.First); sgn != 0 {
- return sgn
- }
- if sgn := cmp.Compare(a.Last, b.Last); sgn != 0 {
- return sgn
- }
- if sgn := cmp.Compare(a.Phone, b.Phone); sgn != 0 {
- return sgn
- }
- if sgn := cmp.Compare(a.Email, b.Email); sgn != 0 {
- return sgn
- }
- return cmp.Compare(a.ID, b.ID)
- })
- return contacts
- }
- func (cs *ContactsStore) Load() error {
- if cs == nil {
- return errors.New("cannot load into nil store")
- }
- bs, err := os.ReadFile(ContactsFile)
- if err != nil {
- return fmt.Errorf("reading file: %w", err)
- }
- contacts := make(Contacts, 0)
- if err := json.Unmarshal(bs, &contacts); err != nil {
- return fmt.Errorf("unmarshalling file: %w", err)
- }
- cs.Lock()
- defer cs.Unlock()
- for _, contact := range contacts {
- cs.data[contact.ID] = contact
- }
- return nil
- }
- func (cs *ContactsStore) getByMail(email string) *Contact {
- cs.RLock()
- defer cs.RUnlock()
- for _, c := range cs.data {
- if c.Email == email {
- return c
- }
- }
- return nil
- }
- func (cs *ContactsStore) initNextID() int {
- cs.Lock()
- defer cs.Unlock()
- max := 0
- for k := range cs.data {
- if k <= max {
- continue
- }
- max = k
- }
- cs.data[max] = nil
- return max
- }
- func (cs *ContactsStore) Save(c *Contact) error {
- if existing := cs.getByMail(c.Email); existing != nil && existing.ID != c.ID {
- return fmt.Errorf("attempting to save new entry for email %q, already under ID %d",
- c.Email, existing.ID)
- }
- if c.ID == 0 {
- c.ID = cs.initNextID()
- }
- cs.Lock()
- cs.data[c.ID] = c
- cs.Unlock()
- w, err := os.Create(ContactsFile)
- if err != nil {
- return fmt.Errorf("creating contacts file: %w", err)
- }
- defer w.Close()
- enc := json.NewEncoder(w)
- if err := enc.Encode(MapValues(cs.data)); err != nil {
- return fmt.Errorf("encoding to contacts file: %w", err)
- }
- return nil
- }
- func (*ContactsStore) New(m map[string]string) *Contact {
- return &Contact{
- First: m["first"],
- Last: m["last"],
- Phone: m["phone"],
- Email: m["email"],
- Errors: make(map[string]string),
- }
- }
- type Contact struct {
- ID int `json:"id"`
- First string `json:"first"`
- Last string `json:"last"`
- Phone string `json:"phone"`
- Email string `json:"email"`
- Errors map[string]string `json:"errors"`
- }
|