Codecademy Practice

Contact Book - File database

Challenge: Until now we have been saving contacts in an array, but that means that when we exit the app, the data is lost. So now we are going to persist the data in a JSON file.

First, familiarize with the JSON format and the File class in Ruby.

JSON

Check the Ruby docs for JSON here.

JSON is a file format consisting of keys and values, which can be strings, numbers, booleans or other JSON objects. It is also a file extension: .json. JSON is saved as a string.

Its structure is very similar to a hash, and in Ruby you can easily switch from a hash object to a JSON string and viceversa. It’s a format often used by APIs to return their data.

This is what JSON looks like if you open a .json file (observe the double quotes):

[
  {
    "name": "Matt Damon"
    "address": "Some address"
    "phone": "12345678901"
    "email": "matt@damon.com"
    "notes": "I think he has an Oscar"
  },
  {
    "name": "Another name",
    // etc.
  }
]

Here is a video about JSON.

File

Check the Ruby docs for File here.

File is the Ruby class that allows us to read from and write to files.

Like the terminal, File handles input and output streams of binary data, and it also shares methods with the IO and StringIO classes.

However the difference is that we have to give File a path where our stream is (the path to the file), and once we write to it, we have to close the stream or flush it with flush. We don’t do any of this for terminal streams.

We can also rewind files, or we can truncate them to wipe their contents. They can be created with a lot of different modes that are described in the IO class.

Instructions:

Step by step instructions

List contacts

Create a database class that represents your persistence layer, i.e., it encapsulates the manipulation and operations on your application’s data, isolating the application from these type of changes.

It will be in charge of creating, updating, deleting and searching for contacts.

This class will have exactly the same methods as the array database class, with the same names and the same behaviour, just it will read and write to a file, rather than store an array. For now, it will just read a list of contacts form a JSON file. So it is going to be called FileDatabase.

Tests:

Create contacts:

{
  name: 'Matt Damon',
  address: 'Some address',
  phone: '1234567',
  email: 'matt@damon.com',
  notes: 'I think he has an Oscar'
}

Rest of methods:

Swap array database with file database

Swap the array database with the file database in each of the actions, one at a time. You can do one action class per commit.

When you swap one database with the other, you still have to make sure that all the actions behave in the same way no matter which database you pass in.

In order to ensure that, we use RSpec “Shared Examples”, which is a feature that allows us to test exactly that.

To use shared examples, you just have to slightly modify your actual action tests, so that you test the action with each database and they share the same tests. For example, for the Creator:

RSpec.shared_examples 'a Creator' do |database_class, argument|
  describe '#run' do
    let(:database) { argument ? database_class.new(argument) : database_class.new }
    let(:creator) { Creator.new(ui, database) }
    # rest of lets

    after do
      # Make sure to delete the contents of the test file
      # created with `TempFile.new` after every test
    end

    # all the tests
  end
end

RSpec.describe 'With Array Database' do
  it_behaves_like 'a Creator', [ArrayDatabase, nil]
end

First do that change and check that everything works. This should print:

With Array Database
  behaves like a Creator
    #run
      ALL YOUR TESTS HERE

Then you can add the describe for the other database:

RSpec.describe 'With File Database' do
  it_behaves_like 'a Creator', [FileDatabase, Tempfile.new('TEST_FILE')]
end

After adding the last describe, your tests should still be green! if not, you should update the code so that the action behaves in the same way with any of the two databases.

When all actions are green, update the bin/app file to pass the file database to all actions.

Run the code. It should behave in the same way as with the array database.

Check the shared examples docs page.

Define an interface for all database classes

The conversation between objects takes place using their interfaces. Classes implement methods, some of those methods are intended to be used by others and these methods make up its public interface. You are always supposed to code to an interface, to an API (Application Programming Interface).

Because we want to be able to swap our database classes without our code breaking, we have to enforce a common public API for all the database classes, so that future databases also comply with the API. We are going to define the interface that all database classes must follow in order to be used by other classes in this application.

To define the interface of a database class we will use inheritance. There are very few cases in software development when it makes sense to use inheritance. This is one of them.

When to use inheritance:

You would use inheritance in the very few cases when all of these apply:

Intructions:

def method_name
  raise NotImplementedError
end
class ChildClass < ParentClass
  #...
end  

Define an interface for all database classes

To expand further: