diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..bdfb56f
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,29 @@
+{
+ "extends": [
+ "eslint:recommended",
+ "google",
+ "plugin:react/recommended"
+ ],
+ "env": {
+ "browser": true,
+ "es6": true,
+ "node": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 2020,
+ "sourceType": "module"
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "rules": {
+ "react/react-in-jsx-scope": "off",
+ "object-curly-spacing": ["error", "always"],
+ "semi": ["error", "never"],
+ "max-len": ["error", { "ignoreComments": true, "code": 120 }],
+ "react/prop-types": "off",
+ "react/display-name": "off"
+ }
+}
diff --git a/.github/workflows/rails.yml b/.github/workflows/rails.yml
new file mode 100644
index 0000000..6cb0618
--- /dev/null
+++ b/.github/workflows/rails.yml
@@ -0,0 +1,60 @@
+name: "Ruby on Rails CI"
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ services:
+ mysql2:
+ image: mysql:5.7
+ ports:
+ - "3306:3306"
+ env:
+ MYSQL_DATABASE: rails_test
+ MYSQL_USER: rails
+ MYSQL_PASSWORD: password
+ MYSQL_TCP_PORT: 3306
+ MYSQL_RANDOM_ROOT_PASSWORD: true
+ MYSQL_ONETIME_PASSWORD: true
+ # Before continuing, verify the mysql container is reachable from the ubuntu host
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ env:
+ RAILS_ENV: test
+ NODE_ENV: test
+ DATABASE_URL: "mysql2://rails:password@127.0.0.1:3306/rails_test" # localhost doesn't work because the application can't connect to the linux host socket
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: Install Ruby and gems
+ uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
+ with:
+ bundler-cache: true
+ - name: Set up database schema
+ run: bundle exec rails db:create db:migrate
+ - name: Install Node.js and packages
+ uses: actions/setup-node@v2
+ with:
+ node-version: '18'
+ - name: Install Yarn and packages
+ run: npm install -g yarn@1.22.19 && yarn install
+ - name: Run webpacker
+ run: NODE_OPTIONS="--openssl-legacy-provider" bundle exec rails webpacker:compile # NODE_OPTIONS needs to be set inline to avoid a webpacker error with node 18
+ - name: Run rspec
+ run: bundle exec rspec
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: Install Ruby and gems
+ uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
+ with:
+ bundler-cache: true
+ # Add or replace any other lints here
+ - name: Run rubocop
+ run: bundle exec rubocop
diff --git a/.github/workflows/react.yml b/.github/workflows/react.yml
new file mode 100644
index 0000000..8e6d017
--- /dev/null
+++ b/.github/workflows/react.yml
@@ -0,0 +1,42 @@
+name: "React CI"
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js 18
+ uses: actions/setup-node@v1
+ with:
+ node-version: 18
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Run the tests
+ run: yarn test
+
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js 18
+ uses: actions/setup-node@v1
+ with:
+ node-version: 18
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Run the linter
+ run: yarn lint
diff --git a/.rspec b/.rspec
index c99d2e7..a35c44f 100644
--- a/.rspec
+++ b/.rspec
@@ -1 +1 @@
---require spec_helper
+--require rails_helper
diff --git a/.rubocop.yml b/.rubocop.yml
index beae6e5..81f41a0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -9,3 +9,20 @@ AllCops:
Style/Documentation:
Enabled: false
+
+RSpec/EmptyExampleGroup:
+ Exclude:
+ - "**/*swagger_spec.rb"
+
+RSpec/NestedGroups:
+ Max: 4
+
+RSpec/ExampleLength:
+ Enabled: false
+
+Layout/FirstHashElementIndentation:
+ EnforcedStyle: consistent
+
+Layout/HashAlignment:
+ EnforcedHashRocketStyle: table
+ EnforcedColonStyle: table
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..33a8789
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+ruby 2.7.7
diff --git a/Gemfile b/Gemfile
index 261dd39..d2b713f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,44 +5,32 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.7.7'
-# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
-gem 'rails', '~> 6.1.7', '>= 6.1.7.3'
-# Use mysql as the database for Active Record
+gem 'bootsnap', '>= 1.4.4', require: false
gem 'mysql2', '~> 0.5'
-# Use Puma as the app server
+gem 'oj'
gem 'puma', '~> 5.0'
-# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
+gem 'rails', '~> 6.1.7', '>= 6.1.7.3'
+gem 'rswag'
gem 'webpacker', '~> 5.0'
-# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 2.7'
-# Use Active Model has_secure_password
-# gem 'bcrypt', '~> 3.1.7'
-
-# Reduces boot times through caching; required in config/boot.rb
-gem 'bootsnap', '>= 1.4.4', require: false
group :development, :test do
- # Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: %i[mri mingw x64_mingw]
+ gem 'factory_bot_rails', '~> 6.0.0', require: false
+ gem 'faker', '~> 3.2'
+ gem 'pry-byebug'
+ gem 'rspec-rails', '~> 6.0.0'
+ gem 'rubocop', '~> 1.48', require: false
+ gem 'rubocop-performance', '~> 1.16', require: false
+ gem 'rubocop-rails', '~> 2.18', require: false
+ gem 'rubocop-rake', '~> 0.6.0', require: false
+ gem 'rubocop-rspec', '~> 2.19', require: false
end
group :development do
- # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
- gem 'web-console', '>= 4.1.0'
- # Display performance information such as SQL time and flame graphs for each request in your browser.
- # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md
gem 'rack-mini-profiler', '~> 2.0'
+ gem 'ruby-lsp-rails'
+ gem 'web-console', '>= 4.1.0'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
-
-gem 'rspec-rails', '~> 6.0.0', groups: %i[development test]
-
-gem 'factory_bot_rails', '~> 6.0.0', groups: %i[development test]
-
-gem 'rubocop', '~> 1.48', groups: %i[development test], require: false
-gem 'rubocop-performance', '~> 1.16', groups: %i[development test], require: false
-gem 'rubocop-rails', '~> 2.18', groups: %i[development test], require: false
-gem 'rubocop-rake', '~> 0.6.0', groups: %i[development test], require: false
-gem 'rubocop-rspec', '~> 2.19', groups: %i[development test], require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 908f27e..0b2a72c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -60,12 +60,16 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
+ addressable (2.8.6)
+ public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
+ bigdecimal (3.1.6)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.4)
byebug (11.1.3)
+ coderay (1.1.3)
concurrent-ruby (1.2.2)
crass (1.0.6)
date (3.3.3)
@@ -76,14 +80,16 @@ GEM
factory_bot_rails (6.0.0)
factory_bot (~> 6.0.0)
railties (>= 5.0.0)
+ faker (3.2.3)
+ i18n (>= 1.8.11, < 2)
globalid (1.1.0)
activesupport (>= 5.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
- jbuilder (2.11.5)
- actionview (>= 5.0.0)
- activesupport (>= 5.0.0)
json (2.6.3)
+ json-schema (4.1.1)
+ addressable (>= 2.8)
+ language_server-protocol (3.17.0.3)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -110,9 +116,19 @@ GEM
nio4r (2.5.8)
nokogiri (1.14.2-x86_64-linux)
racc (~> 1.4)
+ oj (3.16.3)
+ bigdecimal (>= 3.0)
parallel (1.22.1)
parser (3.2.1.1)
ast (~> 2.4.1)
+ prettier_print (1.2.1)
+ pry (0.14.2)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
+ pry-byebug (3.10.1)
+ byebug (~> 11.0)
+ pry (>= 0.13, < 0.15)
+ public_suffix (5.0.4)
puma (5.6.5)
nio4r (~> 2.0)
racc (1.6.2)
@@ -170,6 +186,21 @@ GEM
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
+ rswag (2.13.0)
+ rswag-api (= 2.13.0)
+ rswag-specs (= 2.13.0)
+ rswag-ui (= 2.13.0)
+ rswag-api (2.13.0)
+ activesupport (>= 3.1, < 7.2)
+ railties (>= 3.1, < 7.2)
+ rswag-specs (2.13.0)
+ activesupport (>= 3.1, < 7.2)
+ json-schema (>= 2.2, < 5.0)
+ railties (>= 3.1, < 7.2)
+ rspec-core (>= 2.14)
+ rswag-ui (2.13.0)
+ actionpack (>= 3.1, < 7.2)
+ railties (>= 3.1, < 7.2)
rubocop (1.48.1)
json (~> 2.3)
parallel (~> 1.10)
@@ -196,8 +227,17 @@ GEM
rubocop-rspec (2.19.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
+ ruby-lsp (0.4.5)
+ language_server-protocol (~> 3.17.0)
+ sorbet-runtime
+ syntax_tree (>= 6.1.1, < 7)
+ ruby-lsp-rails (0.1.0)
+ rails (>= 6.0)
+ ruby-lsp (~> 0.4.5)
+ sorbet-runtime (>= 0.5.9897)
ruby-progressbar (1.13.0)
semantic_range (3.0.0)
+ sorbet-runtime (0.5.11238)
sprockets (4.2.0)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
@@ -205,6 +245,8 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
+ syntax_tree (6.2.0)
+ prettier_print (>= 1.2.0)
thor (1.2.1)
timeout (0.3.2)
tzinfo (2.0.6)
@@ -232,17 +274,21 @@ DEPENDENCIES
bootsnap (>= 1.4.4)
byebug
factory_bot_rails (~> 6.0.0)
- jbuilder (~> 2.7)
+ faker (~> 3.2)
mysql2 (~> 0.5)
+ oj
+ pry-byebug
puma (~> 5.0)
rack-mini-profiler (~> 2.0)
rails (~> 6.1.7, >= 6.1.7.3)
rspec-rails (~> 6.0.0)
+ rswag
rubocop (~> 1.48)
rubocop-performance (~> 1.16)
rubocop-rails (~> 2.18)
rubocop-rake (~> 0.6.0)
rubocop-rspec (~> 2.19)
+ ruby-lsp-rails
tzinfo-data
web-console (>= 4.1.0)
webpacker (~> 5.0)
diff --git a/README.md b/README.md
index f0a2a1a..6e711b0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,33 @@ Coding challenge presented to candidates interviewing for a role at [Haistack.AI
_#findyourneedle_
-![A screenshot of the application](SCREENSHOT.png)
+![A screenshot of the application](./haistack_challenge/preview.png)
+
+## Resolution summary
+All of the PRs can be found at https://github.com/andrecego/haistack-coding-challenge/pulls?q=is%3Apr+is%3Aclosed
+API Documentation can be found at http://localhost:3000/api-docs
+
+Key points of each PR:
+- [PR #1](https://github.com/andrecego/haistack-coding-challenge/pull/1): Initial setup of the project with frontend aliases
+- [PR #2](https://github.com/andrecego/haistack-coding-challenge/pull/2): Added lint and test workflows
+- [PR #3](https://github.com/andrecego/haistack-coding-challenge/pull/3): Setup the frontend test environment with vitest and react-testing-library
+- [PR #4](https://github.com/andrecego/haistack-coding-challenge/pull/4): Add the candidate model
+- [PR #5](https://github.com/andrecego/haistack-coding-challenge/pull/5): Base validator and validator classes
+- [PR #6](https://github.com/andrecego/haistack-coding-challenge/pull/6): Create use case to create a candidate along with the route
+- [PR #7](https://github.com/andrecego/haistack-coding-challenge/pull/7): Add Swagger documentation
+- [PR #8](https://github.com/andrecego/haistack-coding-challenge/pull/8): Add Paginate service and oj for performance
+- [PR #9](https://github.com/andrecego/haistack-coding-challenge/pull/9): Show candidate endpoint
+- [PR #10](https://github.com/andrecego/haistack-coding-challenge/pull/10): Update candidate endpoint
+- [PR #11](https://github.com/andrecego/haistack-coding-challenge/pull/11): Delete candidate endpoint
+- [PR #12](https://github.com/andrecego/haistack-coding-challenge/pull/12): Add ESLint and linting rules
+- [PR #13](https://github.com/andrecego/haistack-coding-challenge/pull/13): Add i18n
+- [PR #14](https://github.com/andrecego/haistack-coding-challenge/pull/14): Add Candidate card
+- [PR #15](https://github.com/andrecego/haistack-coding-challenge/pull/15): Add Candidates list
+- [PR #16](https://github.com/andrecego/haistack-coding-challenge/pull/16): Add Candidate Form and Candidate New pages
+- [PR #17](https://github.com/andrecego/haistack-coding-challenge/pull/17): Refactor the candidate new to candidate upsert
+- [PR #18](https://github.com/andrecego/haistack-coding-challenge/pull/18): Add delete candidate button
+- [PR #19](https://github.com/andrecego/haistack-coding-challenge/pull/19): Documentation and final touches
+
## Installation
diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb
new file mode 100644
index 0000000..ce6773a
--- /dev/null
+++ b/app/controllers/api/api_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Api
+ class ApiController < ActionController::API
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
+
+ private
+
+ def not_found
+ head :not_found
+ end
+ end
+end
diff --git a/app/controllers/api/v1/candidates_controller.rb b/app/controllers/api/v1/candidates_controller.rb
new file mode 100644
index 0000000..763ff74
--- /dev/null
+++ b/app/controllers/api/v1/candidates_controller.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Api
+ module V1
+ class CandidatesController < ApiController
+ def index
+ paginated_candidates = PaginateService.new(
+ relation: Candidate.all,
+ page: params[:page]&.to_i,
+ per_page: params[:per_page]&.to_i
+ )
+
+ render json: {
+ candidates: paginated_candidates.call,
+ meta: paginated_candidates.meta
+ }
+ end
+
+ def show
+ candidate = Candidate.find(params[:id])
+ render json: candidate
+ end
+
+ def create
+ candidate = Candidates::CreateUseCase.new(candidate_params).call
+ return head :unprocessable_entity unless candidate
+
+ render json: {}, status: :created
+ end
+
+ def update
+ candidate = Candidates::UpdateUseCase.new(params[:id], candidate_params).call
+ return head :unprocessable_entity unless candidate
+
+ render json: {}, status: :ok
+ end
+
+ def destroy
+ candidate = Candidate.find(params[:id])
+ candidate.destroy
+
+ render json: {}, head: :ok
+ end
+
+ private
+
+ def candidate_params
+ params.require(:candidate).permit(:name, :email, :birthdate)
+ end
+ end
+ end
+end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 6cfa414..61f66da 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -3,6 +3,6 @@
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
-import Rails from "@rails/ujs"
+import Rails from '@rails/ujs'
Rails.start()
diff --git a/app/javascript/packs/hello_react.jsx b/app/javascript/packs/hello_react.jsx
deleted file mode 100644
index 772fc97..0000000
--- a/app/javascript/packs/hello_react.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
-// like app/views/layouts/application.html.erb. All it does is render
Hello React
at the bottom
-// of the page.
-
-import React from 'react'
-import ReactDOM from 'react-dom'
-import PropTypes from 'prop-types'
-
-const Hello = props => (
-