This application is a small console command-line tool written in C++ for managing car rental process.
Source code is on GitHub https://github.com/annechko/car-rental-system
- Functionality
- Usage
- List of available commands
- System architecture
- External dependencies
- Entity relationship diagram
- Use cases
- Tests
- User Management:
- user registration, with different roles: customer and admin
- user login (for some commands)
- Car Management:
- add car (admin) (ID, make, model, year, mileage, minimum rent period in days, maximum rent period.)
- get car list
- update car (admin)
- delete car (admin)
- Rental Booking:
- book car (customer, specify dates) (check minimum-maximum rent period before book car)
- see list of own bookings (customer)
- see list of all customer bookings (admin)
- calculate the rental fees based on the selected car and rental duration
- Rental Management:
- approve rental request (admin)
- reject rental request (admin)
To see a prompt on how to use the app, simply enter the executable file name and press Enter.
You will be presented with information about all available commands and options for each command, along with their respective functionalities.
./car_rental_system
To view a prompt for a specific command, type the executable file name, the command name and -h or --help
./car_rental_system car:update -h
These are all the commands and their help.
Car Rental System - help with the rental process, users and cars management.
Usage:
car_rental_system <command> [options]
-h, --help Print help.
Usage:
car_rental_system register [OPTION...]
-u, --username arg Username for a new user.
-p, --password arg Password for a new user.
-a, --admin Assign admin permissions (testing purposes).
Usage:
car_rental_system car:list [OPTION...]
--start arg start which date you want to book a car, format
dd/mm/yyy: 31/12/2020. (default: "")
--end arg Until when you want to book a car, format
dd/mm/yyy: 31/12/2020. (default: "")
-k, --make arg Company that made a car, for example: Ford, Honda,
Volkswagen. (default: "")
-o, --model arg Car's specific name, for example: Escape, Civic, or
Jetta. (default: "")
--year-from arg Show cars with a model year greater than the
specified year. (default: 0)
--year-to arg Show cars with a model year less than the specified
year. (default: 0)
--price-from arg Show cars with a price per day greater than the
specified year. (default: 0.0)
--price-to arg Show cars with a price per day less than the
specified year. (default: 0.0)
--sort arg Sort cars in a "sort-order" (ascending by default)
by the field which is one of
[make|model|year|price|min-rent|max-rent].
(default: "")
--sort-order arg Sort cars in ascending or descending order, one of
[asc|desc]. Requires sort option as well. (default:
"")
Usage:
car_rental_system car:delete [OPTION...]
Admin only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
-i, --id arg Id of the car to delete.
Usage:
car_rental_system car:add [OPTION...]
Admin only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
-k, --make arg Company that made a car, for example: Ford,
Honda, Volkswagen.
-o, --model arg Car's specific name, for example: Escape, Civic,
or Jetta.
-y, --year arg Model year.
-a, --mileage arg Number of miles that car can travel using one
gallon of fuel. (default: 0)
-c, --price-per-day arg Price to rent this car for 1 day.
--min-rent arg The minimum rent period in days. (default: 0)
--max-rent arg The maximum rent period in days. (default: 0)
Usage:
car_rental_system rent:calculate [OPTION...]
-i, --id arg Id of the car to book.
-s, --start arg From which date you want to book a car, format
dd/mm/yyy: 31/12/2020.
-e, --end arg Until when you want to book a car, format dd/mm/yyy:
31/12/2020.
Usage:
car_rental_system booking:list [OPTION...]
Authenticated only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
Usage:
car_rental_system car:update [OPTION...]
Admin only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
-i, --id arg Id of the car to update.
-k, --make arg Company that made a car, for example: Ford,
Honda, Volkswagen. (default: "")
-o, --model arg Car's specific name, for example: Escape, Civic,
or Jetta. (default: "")
-y, --year arg Model year. (default: -1)
-a, --mileage arg Number of miles that car can travel using one
gallon of fuel. (default: -1)
-c, --price-per-day arg Price to rent this car for 1 day. (default: 0)
--min-rent arg The minimum rent period in days. (default: -1)
--max-rent arg The maximum rent period in days. (default: -1)
Usage:
car_rental_system booking:add [OPTION...]
Customer only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
-i, --id arg Id of the car to book.
-s, --start arg From which date you want to book a car, format
dd/mm/yyy: 31/12/2020.
-e, --end arg Until when you want to book a car, format dd/mm/yyy:
31/12/2020.
Usage:
car_rental_system booking:update [OPTION...]
Admin only.
-u, --username arg Login as user. (default: "")
-p, --password arg Login password. (default: "")
-i, --id arg Id of the car booking.
-a, --approve Change status to approved.
-r, --reject Change status to rejected.
-
Authentication NOT required:
car_rental_system register
- add new user (customer or admin)car_rental_system car:list
- show a list of all cars. There are many option to filter the list (by price, year, rent period, etc.)car_rental_system rent:calculate
- show the total price for renting a car for specified period.
-
Authenticated (any role):
car_rental_system booking:list
- customers will see their bookings, admin will see bookings of all the customers.
-
Admin only:
car_rental_system car:delete
car_rental_system car:add
car_rental_system car:update
car_rental_system booking:update
- update a status of specified booking, reject or accept a booking.
-
Customer only:
car_rental_system booking:add
- create a booking for specified car and days.
All the application classes are located in the namespace crs
(car rental system), so we do not overlap with any library or standard classes,
functions, etc.
In main.cpp
we create an application object (crs::console::application
) and run it, passing the input arguments.
It will write all its output to the stream we pass to it in the constructor (that way we can use it for testing purposes,
providing std::stringstream
insteadof std::cout
and then checking the output that our application creates).
int main(int argc, char* argv[])
{
auto app = new crs::console::application(argc, argv, std::cout);
try
{
app->handle();
}
catch (const std::exception& exception)
{
std::cout << crs::console::text_helper::red(exception.what()) << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
While in tests:
std::stringstream buffer;
auto app = new crs::console::application(argc, argv, buffer);
// then check
// buffer.str() == "expected output"
The application class has a dictionary of so-called commands
(std::unordered_map<std::string, crs::console::command::abstract_command*>
) where a key is a command name and a corresponding value is a command
object itself.
The application creates all the commands classes and then delegates all the work to one of the commands.
For every command that you call from terminal such as car_rental_system car:add
there is a class that derives from basic abstract
class crs::console::command::abstract_command
.
Every command class has
- a name (equals to one that you type in terminal while running the application, such as
car:add
,register
,booking:list
) get_permission_level
method that returns an allowed role of a user to run this command (customer or admin)
namespace crs::console
{
enum ROLE
{
CUSTOMER = 0,
ADMIN = 1,
ANY = 2,
AUTHENTICATED = 3
};
}
handle
method that does everythingconfigure_options
method to specify all the options needed for the command (-u
,-p
,--year
, etc.).
Abstract command class:
namespace crs::console::command
{
class abstract_command
{
public:
abstract_command();
virtual void handle(const cxxopts::ParseResult& options, std::ostream& output) = 0;
virtual const std::string get_name() const = 0;
virtual const crs::console::ROLE get_permission_level() const = 0;
virtual void configure_options(cxxopts::OptionAdder& options);
protected:
cxxopts::OptionAdder& add_auth_params(cxxopts::OptionAdder& options_builder);
const void authenticate(const cxxopts::ParseResult& parsed_options);
const crs::core::user::user* user_;
private:
static const std::string OPTION_USERNAME;
static const std::string OPTION_PASSWORD;
const crs::core::service::auth_service* auth_service_;
};
}
So the main logic and purpose of the application class is to create the commands, ask every one of them to configure their own options, then detect a command name that was specified while running the application from terminal and delegate all the work to the needed command.
auto parsed_cmnd_options = options_commands[command_name]->parse(argc_, argv_);
auto command = commands_[command_name];
command->handle(parsed_cmnd_options, output_);
That way I tried to follow the O(pen-closed) principle from SOLID - when you need to add new functionality (for example delete a booking) you will not modify any existing code but you will add a new command and write your new logic there.
You get new behaviour by adding code, not changing it.
So this application is easily extensible.
Let's look at a typical command implementation - car_rental_system car:delete
with explaining comments.
#include "car_delete.h"
#include "core/core_exception.hpp"
#include <ostream>
namespace crs::console::command
{
car_delete::car_delete()
{
// initialise all the needed dependencies.
// in an ideal world there will be a dependency injection
// but for the learning purposes I left it as a simple manual initialization here
car_service_ = new crs::core::service::car_service;
}
const std::string car_delete::get_name() const
{
// the application class instance will ask for this name
// and compare it with the command name from the command-line input
return std::string("car:delete");
}
void car_delete::handle(const cxxopts::ParseResult& options, std::ostream& output)
{
// call a method from abstract parent class
// same implementation (logic) for all children of abstract_command
// it will find a user by specified username
// check his password from DB to the command-input one
// then check the role of this user and compare to the allowed role of this command
// (from car_delete::get_permission_level())
authenticate(options);
// extract the needed options from the input
// for
// car_rental_system car:delete -i 1
// id will be 1
int id = options["id"].as<int>();
if (id <= 0)
{
throw crs::core::core_exception("Id must be greater than 0.");
}
// delegate an actual work to a service
// the service will deal with the database, etc.
car_service_->delete_car(id);
output << "Car with id = " + std::to_string(id) + " was deleted!" << std::endl;
}
void car_delete::configure_options(cxxopts::OptionAdder& options_builder)
{
// what option you can specify when calling this command
// --id 1
// or
// -i 1
add_auth_params(options_builder)
("i,id", "Id of the car to delete.", cxxopts::value<int>());
}
const crs::console::ROLE car_delete::get_permission_level() const
{
// can be run by admin only.
return crs::console::ROLE::ADMIN;
}
}
I wanted to use a hexagonal architecture for this application.
Hexagonal architecture is a pattern that uses the mechanism of ports and adapters to achieve separation of concerns and isolate external systems and other external code such as user interfaces and databases from the core application.
So all core/domain logic is located in
src/core
folder and can be reused in a case if I add GUI.
All console-specific logic is inside src/console
folder.
My adapters for now are the commands classes, where I obtain all arguments from user console input and call core services classes.
If I add GUI or act as an API for mobile applications I will be able to call the same services, do the same logic for managing DB, just will get all the arguments from http requests or from GUI components (inputs, forms, etc.)
External libraries I use:
To execute all database related commands (select, update, delete, etc.).
Library repository https://github.com/fnc12/sqlite_orm.git
Help with parsing command-line arguments and options and configuring allowed options, also creates help output for all configured options.
Library repository https://github.com/jarro2783/cxxopts
To securely store user passwords in DB. You should never store plain passwords for security reasons, so I hash a given password when create a user, then store this hash in DB. So the next time this user provides his password for log in I can only compare that password hash with the stored hash.
Library repository https://github.com/hilch/Bcrypt.cpp
Help with drawing tables in console for car and booking lists.
Library repository https://github.com/p-ranav/tabulate
I do not have any external library code in my repository, I specify them as external dependencies in CMakeLists.txt and then make it available to use in my code.
The FetchContent_Declare
function specifies the details of the library, such as its name, the GitHub repository URL, and the version tag.
The FetchContent_MakeAvailable
function is then used to download and make the library available for use in the project:
include(FetchContent)
FetchContent_Declare(
sqlite_orm
GIT_REPOSITORY https://github.com/fnc12/sqlite_orm.git
GIT_TAG v1.8.2
)
FetchContent_MakeAvailable(sqlite_orm)
# link it to use in my code
target_link_libraries(car_rental_system PRIVATE sqlite_orm)
There are only 3 tables
- user (stores information about users)
- car (information about cars)
- car_booking (relation between cars and users with extra information, which user book which car for which dates and total price for a booking)
Register 2 users - admin with name a
and customer with name c
:
./car_rental_system register -u c -p p
./car_rental_system register -u a -p p -a
Add 2 cars:
./car_rental_system car:add -u a -p p --make toyota --model x2 --price-per-day 5 --year 2022 --min-rent 1 --max-rent 100
./car_rental_system car:add -u a -p p --make mazda --model cx-60 --price-per-day 2 --year 2023 --min-rent 7
Show cars:
./car_rental_system car:list
Show cars with filters:
./car_rental_system car:list --start 01/01/2025 --end 01/01/2025
./car_rental_system car:list --start 01/01/2025 --end 01/02/2025
Check a rent price to book a toyota car for one week since February 15, 2024:
./car_rental_system rent:calculate -i 1 --start 15/02/2024 --end 22/02/2024
Book a toyota car for customer with name c
for one week since February 15, 2024:
./car_rental_system booking:add -i 1 --start 15/02/2024 --end 22/02/2024 -u c -p p
See a new booking with status NEW:
./car_rental_system booking:list -u c -p p
Approve a new booking as admin:
./car_rental_system booking:update -i 1 -u a -p p --approve
See a new booking with status APPROVED:
./car_rental_system booking:list -u c -p p
Book a mazda car for customer with name c
for one week since February 15, 2024:
./car_rental_system booking:add -i 2 --start 15/02/2024 --end 22/02/2024 -u c -p p
Reject a new booking as admin:
./car_rental_system booking:update -i 2 -u a -p p --reject
See two bookings, the second one with status REJECTED:
./car_rental_system booking:list -u c -p p
Update a car with ID = 1, change model to "x3":
./car_rental_system car:update -i 1 -u a -p p --model x3
Check car list with filter by make:
./car_rental_system car:list --make to
Create a car, see it in the list, delete it, check the list without this car:
./car_rental_system car:add -u a -p p --make BMW --model R --price-per-day 50 --year 2002 --min-rent 1 --max-rent 10
./car_rental_system car:list
./car_rental_system car:delete -i 3 -u a -p p
./car_rental_system car:list
There are simple acceptance tests in tests/
folder. I run them with ctest. You can run them in IDE (I use CLion).
Also, those tests are automatically running in GitHub pipelines on every push in the GitHub repository with the sources.