Employee Module
Tackle the requirements of the Employee module, work on the CSV adapter, and finally tie up the internal usage of employee records.
The employee module is where we’ll bridge the parsing of records with filtering and searching through employees. Proper isolation of concerns should make it possible for both types of users, those who create and those who consume employee data, to do everything they need without knowing about the requirements of the other.
Setting requirements
Our challenge here is to come up with an internal data representation that can easily be converted to CSV, all the while also allowing us to use employee entries the way we would handle any other data structure. Of course, we’ll want to test this with properties. Let’s start with the transformations from what the CSV parser hands to us. This was:
last_name, first_name, date_of_birth, email
Doe, John, 1982/10/08, john.doe@foobar.com
Ann, Mary, 1975/09/21, mary.ann@foobar.com
Now the thing to notice is that there are a couple of issues with the format of the document:
- The fields are messy and have extra leading spaces.
- The dates are in
YYYY/MM/DD
format. This is an issue because Elixir by default works with aDate
structure.
The transformation requires additional processing after the conversion from CSV, which could usually be done or handled by a framework or adapter. In the case of CSV, the specification is really lax and as such it’s our responsibility to convert from a string to the appropriate type, along with some additional validation.
We’ll define the following functionality:
- An accessor for each field (
last_name/1
,first_name/1
,date_of_birth/1
,email/1
). - A function to search employees by birthday.
- A
from_csv/1
function that takes a CSV string and returns a cleaned-up set of maps representing individual employees.
Adapting CSV data
Since we’ve already tested CSV conversion itself in the CSV Parsing lesson, we need to take the output from the parser and hammer it into shape. For our tests, this means that we won’t have to work with generating CSV data, but with generating the data that needs cleaning up.
Properties
Let’s start with the leading spaces. We know all the fields that are required, and we know that all of them except the first will start with whitespace, so a property only needs to ensure that no fields start with whitespace once handled. We can see the property in the code widget below.
We rely on the raw_employee_map()
generator, call the private adapting function, and then check that the first character is not a space in any key or value.
Generators
Let’s look at how to implement the generator. The first trick here is that instead of generating a map with the map()
generator provided by PropEr, we’ll build one from a proplist
. The issue with the default generator is that it takes types for keys and values, and doesn’t let us set specific values easily. A proplist
and a let
macro will do just fine. Let’s take a look at the generator in the code widget below.
Note: We also used the
CsvTest.field()
generator that we defined while writing the CSV parser.
We’ll add an unconditional leading whitespace in front of the field
. We also write a generator for the arbitrary date format provided for us in the file sample, which will need to be properly cleaned up as well.
The module
Running this property will fail because the Bday.Employee.adapt_csv_result/1
function doesn’t exist yet. Since it’s not going to be in our interface, though, we shouldn’t export it during regular circumstances. With traditional example tests, people would colocate their EUnit test within the bday_employee
module. To keep our tests separate from the production code, we can instead add a conditional macro based on the TEST
profile to only export the function while writing tests. Let’s take a look at how they are done:
if Mix.env() == :test do
def adapt_csv_result_shim(map), do: adapt_csv_result(map)
end
Then we can start focusing on implementing the CSV conversion itself. We’ll use a special approach by wrapping all our conversions into a {raw, ...}
tuple, hidden behind an @opaque
type. This will buy us a lot of flexibility for future changes. The implementation is in the lib/employee.ex
file in the code widget below.
Get hands-on with 1400+ tech skills courses.