Jurrasic Park Management System

This project manages the dinosaurs in the park. It allows the user to add, remove, and view dinosaurs and their cages, and put manage them via cages. This is done via a RESTful api that is documented with swagger.

Getting Started

Make sure you have ruby 3+ installed. Then clone the repo and run bundle install to install the dependencies.

Run rails db:setup db:migrate to setup the database.

Review the token at db/seeds.rb, then run rails db:seed. This will seed a single user with an api-key 1234567890 by default.

Run rails rswag:specs:swaggerize to generate the interactive developer documentation. You can then view the documentation at /api-docs. These docs follow the swagger specification, and can be used to quickly test the api. It should look something like this:

Swagger UI

Click the Authorize button and enter your api-key (default: 1234567890)

Run rails server to start the server and visit /api-docs (link↗)

Testing

You can run spec examples using the rspec command.

Design Decisions

The general approach for this project is to keep it as simple as possible, so I stayed aligned with Rails' defaults like sqlite3, and to allow it to be reviewed more easily. This project does not use any database-specific features so it can be trvially migrated. This project makes use of Rails features, so it's pretty tied to Rails. While it's possible that may not always be desirable the alternative is unforeseeable.

Swagger docs

For quickly setting up an api that you can share with fellow developers and allow them to quickly play around with, I think this is an excellent choice. There are some plugins that can take swagger docs as input and generate rich clients for every popular language. This means that you have excellent developer docs that are up to date, and you have a nice integration plan for other vendors even if they don't write Ruby.

Security

Security is pretty simple - a preshared key. Secrets can be generated using rails secret. Must be set manually in the rails console for now. User.find('1user_id').update(token: "YOUR KEY")

Validations

For validations, ActiveRecord validations were used handle the data validation. This is preferable for smaller apps like these where super complex validation is not required.

For api development, getting the most value out of your tests is paramount. For this reason I decided to use the fantasic rswag gem, which allows you to perform basic tests of your api as well as provide developer-friendly documentation for yourself and other consumers. They also have the ability to generate clients in other languages for third party integrating customers.

I created fully documented CRUD (Create, Read, Update, Delete) operations for both cages as well as dinosaurs.

CRUD for users still needs to be done, as the dinosaurs keep eating the researchers, thus raising life insurance premiums.

I followed normal rails conventions, and there is room to make the controllers more DRY, however I decided that the complexity added would not be a worthwhile trade-off until there are more feature requests.

Presenters

I went with presenters for showing data, which I think is by far the most interesting part of this codebase. When you create a presenter, method calls should return other presenters and should NOT leak AR objects out. This is a problem that I see when most code that uses presenters. It's not immediately obvious how to use presenters in a way that retains the flexibility that you get from activerecord. For example, how do you present eager loaded associations, or nested activerecord queries, without directly dealing with the AR objects? You can solve this with a simple construct:

class CagePresenter < SimpleDelegator
  def dinosaurs # overrides AR relation
    if block_given?
      DinosaurPresenter.from_collection(yield _cage.dinosaurs)
    else
      DinosaurPresenter.from_collection(_cage.dinosaurs)
    end
  end

  def _cage =  __getobj__

  def carnivores # overrides AR scope
    if _cage.dinosaurs.loaded?
      dinosaurs.select { |dinosaur| dinosaur.diet == "carnivore" }
    else
      dinosaurs { |query| query.where(diet: "carnivore") }
    end
  end
end

As you can imagine, dinosaurs here is overriding the underlying AR relation, such that when you call cage.dinosaurs you get back a collection of DinosaurPresenters. However, because this method accepts a block, you can configure the underlying AR query with whatever you want right before the presenters get created, and still get back normal presenter objects, like so: cage_presenter.dinosaurs { |query| query.where(diet: 'carnivore') } #=> DinosaurPresenters of carnivores. Finally, because the block approach will always execute another query you can easily end up with an N+1 problem, which is common in ActiveRecord. For that reason we can override our AR scopes as well, and have the presenter intelligently check to see if the underlying association has been eager loaded, with reletively minimal increase in complexity.

There is a drawback to this approach, since it means that you may have to implement 2 versions of a scope in order for it to be efficient. This is a trade off I would accept because the awesome code locality (relevant code is nearby), as well the ability to define a clear and flexible pattern for efficient data loading.

"I think this style makes the presenter a monad, but don't quote me on that" - Jonathan

Performance / Scale

Since most of the validation code happens in ruby code, there may be potential concurrency issues. In the future, we would need to test that changes that are made are either:

  1. Fully wrapped in a transaction
  2. Record Versioning (such that if you try to save a record and you don't have the latest version the write will raise)

I would lean more towards versioning as it helps avoid locking.

To mitigate performance issues/excessive chattiness associated with api overfetching/underfetching, I've added a include_dinosaurs, include_cage options to the api. I'm guessing that clients are very likely to want to fetch associated cages with dinosaurs, so there is an option to fetch both at the same time. This gives the client a reasonable amount of control, without going full Graphql, as that would be too far.

I've added pagination support to the api, as that helps thing stay managable at larger record counts. I did add the total count as a header value instead of adding it to the payload. I do think it's important to have that, and doing it this way allows the request body to be a simple array, instead of something nested.

Database

I'm a Postgresql fan, howerver since I did not need any postgres-specific features to complete this project I kept using sqlite, to keep the project simple, and the installation process easier.