-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add part 2 of the tutorial: CLI Contact Manager #86
base: main
Are you sure you want to change the base?
Conversation
Includes the Contact type (in-memory representation of a contact), the Flags and some Arguments for the CLI
It implements the logic for the file handling and search using Shellfish
Responsible for converting the user prompt in for of args: List[String] into Flags and Arguments
Now all use flatMap insead of mapN
As I forgot that Scala 2 doesn't have top level definitions
It now discards any empty characters after the last newline, just like the `wc` command in Unix Also, added tests to assert that behavior
I forgot that `tempFile` does not exist anymore
Because readLines was modified, It now doesn't account the newline added by the writeLines method.
} yield expect(sizeBefore + 2 == sizeAfter) | ||
// That is, one new line of the appendLine and, one newline character of the writeLines method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you've added the dropLastIf(_.isEmpty)
how come you need this modification here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before the changes:
val aList = List("a", "b")
for
_ <- path.writeLines(contentsList)
sizeBefore <- path.readLines.map(_.size) // sizeBefore is 3, as writeLines adds a newline at the end
_ <- path.appendLine("Im a last line!") // Adds another line
sizeAfter <- path.readLines.map(_.size) // Now sizeAfter is 4
yield expect(sizeBefore + 1 == sizeAfter) // so this condition is true
Now:
val aList = List("a", "b")
for
_ <- path.writeLines(contentsList)
sizeBefore <- path.readLines.map(_.size) // sizeBefore is 2, as readLines now ignores all empty characters before the last newline
_ <- path.appendLine("Im a last line!") // Adds another line
sizeAfter <- path.readLines.map(_.size) // Now sizeAfter is 4, as we didnt get rid of the newline added by writeLines
yield expect(sizeBefore + 1 == sizeAfter) // so this condition is not true anymore, as sizeBefore is 2 and sizeAfter is 4
Changes done! Also, the tutorial is broken until #88 gots merged. |
|
||
object App extends IOApp { | ||
|
||
private val createBookPath: IO[Path] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Book?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought It like that old book that had all the contacts of interest https://en.wikipedia.org/wiki/Yellow_pages, just like a "contact book". I don't know if there is a better name to describe that 😂
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/app/App.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/cli/Cli.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/cli/Cli.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/cli/Prompt.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
private def parseContact(contact: String): Either[Exception, Contact] = | ||
contact.split('|') match { | ||
case Array(id, firstName, lastName, phoneNumber, email) => | ||
Contact(id, firstName, lastName, phoneNumber, email).asRight | ||
|
||
case _ => new Exception(s"Invalid contact format: $contact").asLeft | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Albeit IO
is not strictly required, using it the following signatures will become extremely easy.
private def parseContact(contact: String): Either[Exception, Contact] = | |
contact.split('|') match { | |
case Array(id, firstName, lastName, phoneNumber, email) => | |
Contact(id, firstName, lastName, phoneNumber, email).asRight | |
case _ => new Exception(s"Invalid contact format: $contact").asLeft | |
} | |
private def parseContact(contact: String): IO[Contact] = | |
contact.split('|') match { | |
case Array(id, firstName, lastName, phoneNumber, email) => | |
Contact(id, firstName, lastName, phoneNumber, email).pure[IO] | |
case _ => new Exception(s"Invalid contact format: $contact").raiseError | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a question, I understand the added value of switching the signature from a lines.traverse(IO.fromEither(parseContact(_)))
to a lines.traverse(parseContact)
by returning an IO
, but isn't it bad practice to use IO
on functions that don't do I/O at all? I mean, it's like using Async
on an operation that only needs ApplicativeError
to be referentially transparent 🙃
Aside from that, changes committed! Thanks
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
examples/src/main/scala/io/chrisdavenport/shellfish/contacts/core/ContactManager.scala
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you might have noticed I added a lot of comments. IMHO you duplicated a lot of code and you've used a too low-level approach (i.e. when updating a contact it's easier to parse them all from disk, alter the contact you wish to alter and then re-save everything on disk instead of using the vector's index to do an update at that position).
I've added a few suggestions, reducing the code duplication, dissecting the logic in more bite-sized reusable functions, and making them work on a List[Contacts]
instead that on a List[String]
.
P.S. I haven't reviewed the tutorial yet, mostly because I assume it relies on the actual code structure.
Now they include the changes in the code with their corresponding new explanation
I forgot that I was working in Scala 2
Suggestions committed! |
This PR mostly addresses two important things:
The first is a project that uses this library to create a small contact manager using the command line. There is both the running code in the examples and a tutorial under
docs/tutorial/contact_manager.md
explaining the hows and whys.You can use it by doing
cm help
to display how to use the command.Regarding the project, this is what is contained on each folder:
contacts/domain
: The types of the application, such asContact
,CliCommand
andFlag
, that address the in memory representation of a contact in Scala, the command to execute in the CLI and the different update flags for theupdate
command, respectively.contacts/core
: Here is theContactManager
algebra. This uses shellfish to add, remove and update the contacts on a particular file. It also has methods to search uses by their name, email and phone number.Regarding the implementation, it stores the contacts on a custom encoding that's basically every contact field divided by a
|
, and stores them line by line.contacts/cli
: This class has the functionality to interact with the user in theCli
, it prints to the stdout and takes user input to execute the commands; thePrompt
object has functionality to parse the user input in form of flags and subcommands to convert them to instructions to theCli
.contacts/app
: This is basically the entry point of the application.Any changes or doubts about the code are very welcome!