Skip to content

Commit

Permalink
feat: ✨ Create Task (#3)
Browse files Browse the repository at this point in the history
* feat: ✨ Create Task

* feat: ✨ associate task with user_id

* feat: ✨ input validations and api specs
  • Loading branch information
lakshmaji committed Jun 26, 2024
1 parent 5fd7848 commit a2381b9
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 2 deletions.
22 changes: 22 additions & 0 deletions app/adapters/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Tasks controller
class TasksController < ActionController::API
before_action :doorkeeper_authorize!

def create
create_task = CreateTask.new(TaskRepository.new)
begin
task = create_task.execute(task_params, doorkeeper_token.resource_owner_id)
render json: task, status: :created
rescue CreateTask::ValidationError => e
render json: { errors: e.errors }, status: :unprocessable_entity
end
end

private

def task_params
params.require(:task).permit(:title, :description, :user_id)
end
end
28 changes: 28 additions & 0 deletions app/adapters/repositories/task_repository.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# Outside clean arch, rails specific imple
class TaskRepository
def save(task_params)
Task.create(task_params)
end

def all
Task.all
end

def find(id)
Task.find_by(id:)
end

def update(id, task_params)
task = find(id)
task&.update(task_params)
task
end

def destroy(id)
task = find(id)
task&.destroy
task
end
end
19 changes: 19 additions & 0 deletions app/core/entities/task_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

# Task entity
class TaskEntity
include ActiveModel::Model

attr_accessor :id, :title, :description, :user_id, :status

validates :title, presence: true

def initialize(attributes = {})
super
@status ||= 0 # Default status if not provided
end

def save
raise NotImplementedError, 'Cannot save TaskEntity directly'
end
end
35 changes: 35 additions & 0 deletions app/core/use_cases/tasks/create_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# Create Task use case
class CreateTask
def initialize(repository)
@repository = repository
end

def execute(task_params, user_id)
task = TaskEntity.new(task_params.merge(user_id:))

raise ValidationError, task.errors unless task.valid?

@repository.save(title: task.title, description: task.description, status: 0, user_id: task.user_id)
end

# validation
class ValidationError < StandardError
attr_reader :errors

def initialize(errors)
@errors = errors
super(build_error_message(errors))
end

private

def build_error_message(errors)
errors.messages.map do |field, messages|
# messages.map { |message| { field: field.to_s.humanize, message: } }
{ field: field.to_s.humanize, message: messages.first }
end.flatten
end
end
end
2 changes: 2 additions & 0 deletions app/models/task.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# frozen_string_literal: true

# Task model
class Task < ApplicationRecord
validates :title, presence: true
end
5 changes: 5 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
module Todo
# Application
class Application < Rails::Application
config.autoload_paths << "#{root}/app/adapters/controllers"
config.autoload_paths << "#{root}/app/adapters/repositories"
config.autoload_paths << "#{root}/app/core/entities"
config.autoload_paths << "#{root}/app/core/use_cases/tasks"

# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0

Expand Down
12 changes: 12 additions & 0 deletions config/initializers/adapters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

# Rails.application.config.eager_load = true

# shapes = "#{Rails.root.join('app/core')}"
# Rails.autoloaders.main.collapse(shapes) # Not a namespace.

# unless Rails.application.config.eager_load
# Rails.application.config.to_prepare do
# Rails.autoloaders.main.eager_load_dir(shapes)
# end
# end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
end

get 'me', to: 'auth#current_user'
post 'task', to: 'tasks#create'
end
5 changes: 5 additions & 0 deletions db/migrate/20240623190417_add_user_id_to_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddUserIdToTask < ActiveRecord::Migration[7.0]
def change
add_reference :tasks, :user, null: false, foreign_key: true
end
end
5 changes: 4 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion spec/models/task_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@
require 'rails_helper'

RSpec.describe Task, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
describe 'validations' do
it { is_expected.to validate_presence_of(:title) }
end

describe 'database columns' do
it { is_expected.to have_db_column(:title).of_type(:string) }
it { is_expected.to have_db_column(:description).of_type(:text) }
it { is_expected.to have_db_column(:status).of_type(:integer) }
end
end
55 changes: 55 additions & 0 deletions spec/requests/tasks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require 'swagger_helper'
RSpec.describe 'Tasks', type: :request do
path '/task' do
post 'Create a task' do
tags 'Tasks'
consumes 'application/json'
produces 'application/json'
security [bearer_auth: []]

parameter name: :register_params, in: :body, schema: {
type: :object,
properties: {
title: { type: :string },
description: { type: :string }
},
required: %w[title]
},
required: true

let(:token) { token_scopes('public manage') }

let(:Authorization) { "Bearer #{token.token}" }

request_body_example value: {
title: 'Some title',
description: 'password'
}, name: '1', summary: 'Success 201'

response '201', 'task created' do
let(:register_params) do
{
title: 'Some title',
description: 'password'
}
end

run_test! do |response|
expect(response).to have_http_status(:created)
end
end

response '422', 'invalid request' do
let(:register_params) { { description: 'without title' } }
request_body_example value: {
description: 'without title'
}, name: 2, summary: 'Invalid input 422'
run_test! do |response|
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
end
20 changes: 20 additions & 0 deletions spec/use_cases/tasks/create_task_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe CreateTask, type: :use_case do
let(:task_repository) { instance_double(TaskRepository) }
let(:user) { create(:user) }
let(:create_task) { described_class.new(task_repository) }
let(:task_params) { { title: 'Sample Todo', description: 'This is a sample content.' } }
let(:model_task) { Task.new(task_params) }

it 'creates a task' do
task = Task.new(task_params)
allow(task_repository).to receive(:save).with(description: task[:description], status: 0,
title: task[:title], user_id: user.id).and_return(model_task)

result = create_task.execute(task_params, user.id)
expect(result).to be_a(Task)
end
end

0 comments on commit a2381b9

Please sign in to comment.