|
| 1 | +/* |
| 2 | + * Copyright (c) 2024 Typelevel |
| 3 | + * |
| 4 | + * Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 5 | + * this software and associated documentation files (the "Software"), to deal in |
| 6 | + * the Software without restriction, including without limitation the rights to |
| 7 | + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 8 | + * the Software, and to permit persons to whom the Software is furnished to do so, |
| 9 | + * subject to the following conditions: |
| 10 | + * |
| 11 | + * The above copyright notice and this permission notice shall be included in all |
| 12 | + * copies or substantial portions of the Software. |
| 13 | + * |
| 14 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 15 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
| 16 | + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| 17 | + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| 18 | + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| 19 | + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 20 | + */ |
| 21 | + |
| 22 | +package io.chrisdavenport.shellfish.contacts.core |
| 23 | + |
| 24 | +import cats.syntax.all.* |
| 25 | +import cats.effect.IO |
| 26 | +import fs2.io.file.Path |
| 27 | +import io.chrisdavenport.shellfish.syntax.path.* |
| 28 | +import io.chrisdavenport.shellfish.contacts.domain.contact.* |
| 29 | + |
| 30 | +trait ContactManager { |
| 31 | + def addContact(contact: Contact): IO[Username] |
| 32 | + def removeContact(username: Username): IO[Unit] |
| 33 | + def searchUsername(username: Username): IO[Option[Contact]] |
| 34 | + def searchName(name: Name): IO[List[Contact]] |
| 35 | + def searchEmail(email: Email): IO[List[Contact]] |
| 36 | + def searchNumber(number: PhoneNumber): IO[List[Contact]] |
| 37 | + def getAll: IO[List[Contact]] |
| 38 | + def updateContact(username: String)(modify: Contact => Contact): IO[Contact] |
| 39 | +} |
| 40 | + |
| 41 | +object ContactManager { |
| 42 | + def apply(bookPath: Path): ContactManager = new ContactManager { |
| 43 | + |
| 44 | + private def parseContact(contact: String): IO[Contact] = |
| 45 | + contact.split('|') match { |
| 46 | + case Array(id, firstName, lastName, phoneNumber, email) => |
| 47 | + Contact(id, firstName, lastName, phoneNumber, email).pure[IO] |
| 48 | + case _ => |
| 49 | + new Exception(s"Invalid contact format: $contact") |
| 50 | + .raiseError[IO, Contact] |
| 51 | + } |
| 52 | + |
| 53 | + private def encodeContact(contact: Contact): String = |
| 54 | + s"${contact.username}|${contact.firstName}|${contact.lastName}|${contact.phoneNumber}|${contact.email}" |
| 55 | + |
| 56 | + private def saveContacts(contacts: List[Contact]): IO[Unit] = |
| 57 | + bookPath.writeLines(contacts.map(encodeContact)) |
| 58 | + |
| 59 | + override def addContact(contact: Contact): IO[Username] = for { |
| 60 | + contacts <- getAll |
| 61 | + _ <- IO(contacts.contains(contact)).ifM( |
| 62 | + ContactFound(contact.username).raiseError[IO, Unit], |
| 63 | + saveContacts(contact :: contacts) |
| 64 | + ) |
| 65 | + } yield contact.username |
| 66 | + |
| 67 | + override def removeContact(username: Username): IO[Unit] = |
| 68 | + for { |
| 69 | + contacts <- getAll |
| 70 | + filteredContacts = contacts.filterNot(_.username === username) |
| 71 | + _ <- saveContacts(filteredContacts) |
| 72 | + } yield () |
| 73 | + |
| 74 | + override def searchUsername(username: Username): IO[Option[Contact]] = |
| 75 | + getAll.map(contacts => contacts.find(_.username === username)) |
| 76 | + |
| 77 | + override def searchName(name: Name): IO[List[Contact]] = |
| 78 | + getAll.map(contacts => |
| 79 | + contacts.filter(c => c.firstName === name || c.lastName === name) |
| 80 | + ) |
| 81 | + |
| 82 | + override def searchEmail(email: Email): IO[List[Contact]] = |
| 83 | + getAll.map(contacts => contacts.filter(_.email === email)) |
| 84 | + |
| 85 | + override def searchNumber(number: PhoneNumber): IO[List[Contact]] = |
| 86 | + getAll.map(contacts => contacts.filter(_.phoneNumber === number)) |
| 87 | + |
| 88 | + override def getAll: IO[List[Contact]] = for { |
| 89 | + lines <- bookPath.readLines |
| 90 | + contacts <- lines.traverse(parseContact) |
| 91 | + } yield contacts |
| 92 | + |
| 93 | + override def updateContact( |
| 94 | + username: Username |
| 95 | + )(modify: Contact => Contact): IO[Contact] = for { |
| 96 | + contacts <- getAll |
| 97 | + oldContact <- contacts.find(_.username === username) match { |
| 98 | + case None => ContactNotFound(username).raiseError[IO, Contact] |
| 99 | + case Some(contact) => contact.pure[IO] |
| 100 | + } |
| 101 | + updatedContact = modify(oldContact) |
| 102 | + updatedContacts = updatedContact :: contacts.filterNot(_ == oldContact) |
| 103 | + _ <- saveContacts(updatedContacts) |
| 104 | + } yield updatedContact |
| 105 | + } |
| 106 | +} |
0 commit comments