newsletter

This commit is contained in:
Danil Negrienko 2023-12-22 05:08:45 -05:00
parent 776a104f19
commit 42bdf9694e
13 changed files with 528 additions and 0 deletions

View File

@ -0,0 +1,21 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/newsletter.ex"
],
"test": [
"test/newsletter_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about working with files by sending out a newsletter."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"newsletter","id":"82b87d3dabcd44359ff71806e27cec05","url":"https://exercism.org/tracks/elixir/exercises/newsletter","handle":"negrienko","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
elixir/newsletter/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

75
elixir/newsletter/HELP.md Normal file
View File

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/newsletter.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.

View File

@ -0,0 +1,40 @@
# Hints
## 1. General
- Read about files in the official [Getting Started guide][getting-started-file].
- Read about files on [joyofelixir.com][joy-of-elixir-file].
- Take a look at the [documentation of the `File` module][file].
## 1. Read email addresses from a file
- There is a [built-in function][file-read] for reading the contents of a file all at once.
## 2. Open a log file for writing
- There is a [built-in function][file-open] for opening a file.
- The second argument of that function is a list of modes which allows specifying that the file should be opened for writing.
## 3. Log a sent email
- Functions for reading and writing to a file opened with [`File.open!/1`][file-open] can be found in the [`IO`][io] module.
- There is a [built-in function][io-puts] for writing a string to a file, followed by a newline.
## 4. Close the log file
- There is a [built-in function][file-close] for closing a file.
## 5. Send the newsletter
- All the necessary operations on files were already implemented in the previous steps.
- Before writing to a file, the file must be opened.
- After all write operations to a file finished, the file should be closed.
[getting-started-file]: https://elixir-lang.org/getting-started/io-and-the-file-system.html#the-file-module
[joy-of-elixir-file]: https://joyofelixir.com/11-files/
[file]: https://hexdocs.pm/elixir/File.html
[file-read]: https://hexdocs.pm/elixir/File.html#read!/1
[file-open]: https://hexdocs.pm/elixir/File.html#open!/1
[file-close]: https://hexdocs.pm/elixir/File.html#close/1
[io]: https://hexdocs.pm/elixir/IO.html
[io-puts]: https://hexdocs.pm/elixir/IO.html#puts/2

View File

@ -0,0 +1,92 @@
# Newsletter
Welcome to Newsletter on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## File
Functions for working with files are provided by the `File` module.
To read a whole file, use `File.read/1`. To write to a file, use `File.write/2`.
Every time a file is written to with `File.write/2`, a file descriptor is opened and a new Elixir [process][exercism-processes] is spawned. For this reason, writing to a file in a loop using `File.write/2` should be avoided.
Instead, a file can be opened using `File.open/2`. The second argument to `File.open/2` is a list of modes, which allows you to specify if you want to open the file for reading or for writing.
`File.open/2` returns a PID of a process that handles the file. To read and write to the file, use functions from the `IO` module and pass this PID as the IO device.
When you're finished working with the file, close it with `File.close/1`.
All the mentioned functions from the `File` module also have a `!` variant that raises an error instead of returning an error tuple (e.g. `File.read!/1`). Use that variant if you don't intend to handle errors such as missing files or lack of permissions.
[exercism-processes]: https://exercism.org/tracks/elixir/concepts/processes
## Instructions
You're a big model train enthusiast and have decided to share your passion with the world by starting a newsletter. You'll start by sending the first issue of your newsletter to your friends and acquaintances that share your hobby. You have a text file with a list of their email addresses.
## 1. Read email addresses from a file
Implement the `Newsletter.read_emails/1` function. It should take a file path. The file is a text file that contains email addresses separated by newlines. The function should return a list of the email addresses from the file.
```elixir
Newsletter.read_emails("/home/my_user/documents/model_train_friends_emails.txt")
# => ["rick@example.com", "choochoo42@example.com", "anna@example.com"]
```
## 2. Open a log file for writing
Sending an email is a task that might fail for many unpredictable reasons, like a typo in the email address or temporary network issues. To ensure that you can retry sending the emails to all your friends without sending duplicates, you need to log the email addresses that already received the email. For this, you'll need a log file.
Implement the `Newsletter.open_log/1` function. It should take a file path, open the file for writing, and return the PID of the process that handles the file.
```elixir
Newsletter.open_log("/home/my_user/documents/newsletter_issue1_log.txt")
# => #PID<0.145.0>
```
## 3. Log a sent email
Implement the `Newsletter.log_sent_email/2` function. It should take a PID of the process that handles the file and a string with the email address. It should write the email address to the file, followed by a newline.
```elixir
Newsletter.log_sent_email(pid, "joe@example.com")
# => :ok
```
## 4. Close the log file
Implement the `Newsletter.close_log/1` function. It should take a PID of the process that handles the file and close the file.
```elixir
Newsletter.close_log(pid)
# => :ok
```
## 5. Send the newsletter
Now that you have all of the building blocks of the email sending procedure, you need to combine them together in a single function.
Implement the `Newsletter.send_newsletter/3` function. It should take a path of the file with email addresses, a path of a log file, and an anonymous function that sends an email to a given email address. It should read all the email addresses from the given file and attempt to send an email to every one of them. If the anonymous function that sends the email returns `:ok`, write the email address to the log file, followed by a new line. Make sure to do it as soon as the email is sent. Afterwards, close the log file.
```elixir
Newsletter.send_newsletter(
"model_train_friends_emails.txt",
"newsletter_issue1_log.txt",
fn email -> :ok end
)
# => :ok
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View File

@ -0,0 +1,4 @@
alice@example.com
bob@example.com
charlie@example.com
dave@example.com

View File

View File

@ -0,0 +1,32 @@
defmodule Newsletter do
def read_emails(path) do
path
|> File.stream!()
|> Stream.map(&String.trim/1)
|> Enum.to_list()
end
def open_log(path) do
File.open!(path, [:write])
end
def log_sent_email(pid, email) do
IO.puts(pid, email)
end
def close_log(pid) do
File.close(pid)
end
def send_newsletter(emails_path, log_path, send_fun) do
log_pid = open_log(log_path)
emails_path
|> read_emails()
|> Enum.map(fn email ->
if send_fun.(email) == :ok, do: log_sent_email(log_pid, email)
end)
close_log(log_pid)
end
end

28
elixir/newsletter/mix.exs Normal file
View File

@ -0,0 +1,28 @@
defmodule Newsletter.MixProject do
use Mix.Project
def project do
[
app: :newsletter,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View File

@ -0,0 +1,205 @@
defmodule NewsletterTest do
use ExUnit.Case
@temp_file_path Path.join(["assets", "temp.txt"])
setup do
File.write!(@temp_file_path, "")
on_exit(fn -> File.rm!(@temp_file_path) end)
end
describe "read_emails" do
@tag task_id: 1
test "returns a list of all lines in a file" do
emails_file_path = Path.join(["assets", "emails.txt"])
assert Newsletter.read_emails(emails_file_path) == [
"alice@example.com",
"bob@example.com",
"charlie@example.com",
"dave@example.com"
]
end
@tag task_id: 1
test "returns an empty list if the file is empty" do
empty_file_path = Path.join(["assets", "empty.txt"])
assert Newsletter.read_emails(empty_file_path) == []
end
end
describe "open_log" do
@tag task_id: 2
test "returns a pid" do
file = Newsletter.open_log(@temp_file_path)
assert is_pid(file)
File.close(file)
end
@tag task_id: 2
test "opens the file for writing" do
file = Newsletter.open_log(@temp_file_path)
assert IO.write(file, "hello") == :ok
assert File.read!(@temp_file_path) == "hello"
File.close(file)
end
end
describe "log_sent_email" do
@tag task_id: 3
test "returns ok" do
file = File.open!(@temp_file_path, [:write])
assert Newsletter.log_sent_email(file, "janice@example.com") == :ok
File.close(file)
end
@tag task_id: 3
test "writes the email address to the given file" do
file = File.open!(@temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
assert File.read!(@temp_file_path) == "joe@example.com\n"
File.close(file)
end
@tag task_id: 3
test "writes many email addresses to the given file" do
file = File.open!(@temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
Newsletter.log_sent_email(file, "kathrine@example.com")
Newsletter.log_sent_email(file, "lina@example.com")
assert File.read!(@temp_file_path) ==
"joe@example.com\nkathrine@example.com\nlina@example.com\n"
File.close(file)
end
end
describe "close_log" do
@tag task_id: 4
test "returns ok" do
file = File.open!(@temp_file_path, [:write])
assert Newsletter.close_log(file) == :ok
end
@tag task_id: 4
test "closes the file" do
file = File.open!(@temp_file_path, [:read])
assert Newsletter.close_log(file) == :ok
assert IO.read(file, :all) == {:error, :terminated}
end
end
describe "send_newsletter" do
@tag task_id: 5
test "returns ok" do
send_fun = fn _ -> :ok end
assert Newsletter.send_newsletter(
Path.join(["assets", "emails.txt"]),
@temp_file_path,
send_fun
) == :ok
end
@tag task_id: 5
test "calls send function for every email from the emails file" do
send_fun = fn email -> send(self(), {:send, email}) && :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert_received {:send, "alice@example.com"}
assert_received {:send, "bob@example.com"}
assert_received {:send, "charlie@example.com"}
assert_received {:send, "dave@example.com"}
end
@tag task_id: 5
test "logs emails that were sent" do
send_fun = fn _ -> :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
charlie@example.com
dave@example.com
"""
end
@tag task_id: 5
test "does not log emails that could not be sent" do
send_fun = fn
"bob@example.com" -> :error
"charlie@example.com" -> :error
_ -> :ok
end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) == """
alice@example.com
dave@example.com
"""
end
@tag task_id: 5
test "sending the same newsletter twice resets the log" do
send_fun = fn _ -> :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
charlie@example.com
dave@example.com
"""
end
@tag task_id: 5
test "logs the email immediately after it was sent" do
send_fun = fn email ->
case email do
"alice@example.com" ->
:ok
"bob@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
"""
:ok
"charlie@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
bob@example.com
"""
:error
"dave@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
bob@example.com
"""
:ok
end
end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
dave@example.com
"""
end
end
end

View File

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)