contacts_model.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package main
  2. import (
  3. "cmp"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "slices"
  9. "strings"
  10. "sync"
  11. )
  12. var (
  13. ContactsFile = "contacts.json"
  14. )
  15. type (
  16. Contacts []*Contact
  17. ContactsStore struct {
  18. sync.RWMutex
  19. data map[int]*Contact
  20. }
  21. )
  22. func NewContactsStore() (*ContactsStore, error) {
  23. cs := ContactsStore{
  24. data: make(map[int]*Contact),
  25. }
  26. if err := cs.Load(); err != nil {
  27. return nil, err
  28. }
  29. return &cs, nil
  30. }
  31. func (cs *ContactsStore) Get(search string) Contacts {
  32. contacts := cs.GetAll()
  33. if search == "" {
  34. return contacts
  35. }
  36. return slices.DeleteFunc(contacts, func(c *Contact) bool {
  37. return (c == nil) || ((c.First == "" || !strings.Contains(c.First, search)) &&
  38. (c.Last == "" || !strings.Contains(c.Last, search)) &&
  39. (c.Email == "" || !strings.Contains(c.Email, search)) &&
  40. (c.Phone == "" || !strings.Contains(c.Phone, search)))
  41. })
  42. }
  43. func MapValues[M ~map[K]V, K comparable, V any](m M) []V {
  44. sl := make([]V, 0, len(m))
  45. for _, v := range m {
  46. sl = append(sl, v)
  47. }
  48. return sl
  49. }
  50. func (cs *ContactsStore) GetAll() Contacts {
  51. cs.RLock()
  52. defer cs.RUnlock()
  53. contacts := MapValues(cs.data)
  54. slices.SortStableFunc(contacts, func(a, b *Contact) int {
  55. if sgn := cmp.Compare(a.First, b.First); sgn != 0 {
  56. return sgn
  57. }
  58. if sgn := cmp.Compare(a.Last, b.Last); sgn != 0 {
  59. return sgn
  60. }
  61. if sgn := cmp.Compare(a.Phone, b.Phone); sgn != 0 {
  62. return sgn
  63. }
  64. if sgn := cmp.Compare(a.Email, b.Email); sgn != 0 {
  65. return sgn
  66. }
  67. return cmp.Compare(a.ID, b.ID)
  68. })
  69. return contacts
  70. }
  71. func (cs *ContactsStore) Load() error {
  72. if cs == nil {
  73. return errors.New("cannot load into nil store")
  74. }
  75. bs, err := os.ReadFile(ContactsFile)
  76. if err != nil {
  77. return fmt.Errorf("reading file: %w", err)
  78. }
  79. contacts := make(Contacts, 0)
  80. if err := json.Unmarshal(bs, &contacts); err != nil {
  81. return fmt.Errorf("unmarshalling file: %w", err)
  82. }
  83. cs.Lock()
  84. defer cs.Unlock()
  85. for _, contact := range contacts {
  86. cs.data[contact.ID] = contact
  87. }
  88. return nil
  89. }
  90. func (cs *ContactsStore) getByMail(email string) *Contact {
  91. cs.RLock()
  92. defer cs.RUnlock()
  93. for _, c := range cs.data {
  94. if c.Email == email {
  95. return c
  96. }
  97. }
  98. return nil
  99. }
  100. func (cs *ContactsStore) initNextID() int {
  101. cs.Lock()
  102. defer cs.Unlock()
  103. max := 0
  104. for k := range cs.data {
  105. if k <= max {
  106. continue
  107. }
  108. max = k
  109. }
  110. cs.data[max] = nil
  111. return max
  112. }
  113. func (cs *ContactsStore) Save(c *Contact) error {
  114. if existing := cs.getByMail(c.Email); existing != nil && existing.ID != c.ID {
  115. return fmt.Errorf("attempting to save new entry for email %q, already under ID %d",
  116. c.Email, existing.ID)
  117. }
  118. if c.ID == 0 {
  119. c.ID = cs.initNextID()
  120. }
  121. cs.Lock()
  122. cs.data[c.ID] = c
  123. cs.Unlock()
  124. w, err := os.Create(ContactsFile)
  125. if err != nil {
  126. return fmt.Errorf("creating contacts file: %w", err)
  127. }
  128. defer w.Close()
  129. enc := json.NewEncoder(w)
  130. if err := enc.Encode(MapValues(cs.data)); err != nil {
  131. return fmt.Errorf("encoding to contacts file: %w", err)
  132. }
  133. return nil
  134. }
  135. func (*ContactsStore) New(m map[string]string) *Contact {
  136. return &Contact{
  137. First: m["first"],
  138. Last: m["last"],
  139. Phone: m["phone"],
  140. Email: m["email"],
  141. Errors: make(map[string]string),
  142. }
  143. }
  144. type Contact struct {
  145. ID int `json:"id"`
  146. First string `json:"first"`
  147. Last string `json:"last"`
  148. Phone string `json:"phone"`
  149. Email string `json:"email"`
  150. Errors map[string]string `json:"errors"`
  151. }