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"` }