Skip to content

Commit

Permalink
refactor: redesign commands into command pattern with previous comman…
Browse files Browse the repository at this point in the history
…ds implemented
  • Loading branch information
diomonogatari committed Nov 2, 2024
1 parent cfece54 commit 037a859
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 101 deletions.
60 changes: 60 additions & 0 deletions application/command/codewarsLeaderboardCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { SendCodewarsLeaderboardToChannelInput } from "application/usecases/sendCodewarsLeaderboardToChannel/sendCodewarsLeaderboardToChannelInput";
import { Command } from "../../types";
import KataService from "../../domain/service/kataService/kataService";
import ChatService from "../../domain/service/chatService";
import KataLeaderboardUser from "../../domain/service/kataService/kataLeaderboardUser";

export default class CodewarsLeaderboardCommand implements Command {
readonly name = "!cwl";

private chatService: ChatService;

private kataService: KataService;

constructor(chatService: ChatService, kataService: KataService) {
this.chatService = chatService;
this.kataService = kataService;
}

private formatLeaderboard(leaderboard: KataLeaderboardUser[]): string {
let output = "```";
let position = 1;

const leaderboardTotalEntriesToShow = 10;
const leaderboardEntries = leaderboard.slice(0, leaderboardTotalEntriesToShow);
const leaderboardEntriesLeft = leaderboard.length - leaderboardTotalEntriesToShow;

leaderboardEntries.forEach((user: KataLeaderboardUser) => {
const pointsCollection = user.getPoints().map((points: number) => points || 0);

output += `${position}. ${user.getUsername()} - ${user.getScore()} - [${pointsCollection.join(",")}] points
`;

position += 1;
});

output += "```";

if (leaderboardEntriesLeft > 1) {
output += `
... e ${leaderboardEntriesLeft} outras participações em https://codewars.devpt.co`;
} else if (leaderboardEntriesLeft === 1) {
output += `
... e 1 outra participação em https://codewars.devpt.co`;
}

return output;
}

async execute({ channelId }: SendCodewarsLeaderboardToChannelInput): Promise<void> {
const leaderboard = await this.kataService.getLeaderboard();

if (leaderboard.length === 0) {
this.chatService.sendMessageToChannel("Ainda não existem participantes nesta ediçăo do desafio.", channelId);
return;
}

const formattedLeaderboard = this.formatLeaderboard(leaderboard);
this.chatService.sendMessageToChannel(formattedLeaderboard, channelId);
}
}
19 changes: 19 additions & 0 deletions application/command/dontAskToAskCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command, Context } from "../../types";
import ChatService from "../../domain/service/chatService";

export default class DontAskToAskCommand implements Command {
readonly name = "!ja";

private readonly message: string =
"Olá! Experimenta fazer a pergunta diretamente e contar o que já tentaste! Sabe mais aqui :point_right: https://dontasktoask.com/pt-pt/";

private chatService: ChatService;

constructor(chatService: ChatService) {
this.chatService = chatService;
}

async execute(context: Context): Promise<void> {
await this.chatService.sendMessageToChannel(this.message, context.channelId);
}
}
19 changes: 19 additions & 0 deletions application/command/onlyCodeQuestionsCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command, Context } from "../../types";
import ChatService from "../../domain/service/chatService";

export default class OnlyCodeQuestionsCommand implements Command {
readonly name = "!oc";

private chatService: ChatService;

private readonly message: string =
":warning: Este servidor é APENAS para questões relacionadas com programação! :warning:";

constructor(chatService: ChatService) {
this.chatService = chatService;
}

async execute(context: Context): Promise<void> {
await this.chatService.sendMessageToChannel(this.message, context.channelId);
}
}
74 changes: 17 additions & 57 deletions domain/service/commandUseCaseResolver.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,44 @@
import { promises as fs } from "fs";
import path from "path";
import { Context } from "../../types";
import CodewarsLeaderboardCommand from "../../application/command/codewarsLeaderboardCommand";
import DontAskToAskCommand from "../../application/command/dontAskToAskCommand";
import OnlyCodeQuestionsCommand from "../../application/command/onlyCodeQuestionsCommand";
import { Command, Context } from "../../types";
import UseCaseNotFound from "../exception/useCaseNotFound";
import SendMessageToChannelUseCase from "../../application/usecases/sendMessageToChannel/sendMessageToChannelUseCase";
import MessageRepository from "../repository/messageRepository";
import ChatService from "./chatService";
import LoggerService from "./loggerService";
import ChannelResolver from "./channelResolver";
import KataService from "./kataService/kataService";
import SendCodewarsLeaderboardToChannelUseCase from "../../application/usecases/sendCodewarsLeaderboardToChannel/sendCodewarsLeaderboardToChannelUseCase";
import LoggerService from "./loggerService";

export default class CommandUseCaseResolver {
private messageRepository: MessageRepository;

private chatService: ChatService;
private commands: Command[] = [];

private loggerService: LoggerService;

private channelResolver: ChannelResolver;

private kataService: KataService;

private commandMessages: Record<string, string> = {};

constructor({
messageRepository,
chatService,
loggerService,
channelResolver,
kataService,
loggerService,
}: {
messageRepository: MessageRepository;
chatService: ChatService;
loggerService: LoggerService;
channelResolver: ChannelResolver;
kataService: KataService;
loggerService: LoggerService;
}) {
this.messageRepository = messageRepository;
this.chatService = chatService;
this.loggerService = loggerService;
this.channelResolver = channelResolver;
this.kataService = kataService;
}

private async loadCommands(): Promise<void> {
const filePath = path.join(__dirname, "commands.json");
const data = await fs.readFile(filePath, "utf-8");
this.commandMessages = JSON.parse(data);
this.commands.push(
new CodewarsLeaderboardCommand(chatService, kataService),
new DontAskToAskCommand(chatService),
new OnlyCodeQuestionsCommand(chatService)
);
}

async resolveByCommand(command: string, context: Context): Promise<void> {
this.loggerService.log(`Command received: "${command}"`);

const deps = {
messageRepository: this.messageRepository,
chatService: this.chatService,
loggerService: this.loggerService,
channelResolver: this.channelResolver,
kataService: this.kataService,
};

if (Object.keys(this.commandMessages).length === 0) {
await this.loadCommands();
}

if (this.commandMessages[command]) {
new SendMessageToChannelUseCase(deps).execute({
channelId: context.channelId,
message: this.commandMessages[command],
});
return;
}
const commandInstance = this.commands.find((cmd) => cmd.name === command);

if (command === "!cwl") {
new SendCodewarsLeaderboardToChannelUseCase(deps).execute({
channelId: context.channelId,
});
return;
if (!commandInstance) {
throw new UseCaseNotFound().byCommand(command);
}

throw new UseCaseNotFound().byCommand(command);
await commandInstance.execute(context);
}
}
4 changes: 0 additions & 4 deletions domain/service/commands.json

This file was deleted.

9 changes: 9 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ export enum ChannelSlug {
ENTRANCE = "ENTRANCE",
JOBS = "JOBS",
}

export type CommandMessages = {
[command: string]: string;
};

export interface Command {
name: string;
execute(context: Context): Promise<void>;
}
46 changes: 6 additions & 40 deletions vitest/service/commandUseCaseResolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,41 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import { promises as fs } from "fs";
import CommandUseCaseResolver from "../../domain/service/commandUseCaseResolver";
import SendMessageToChannelUseCase from "../../application/usecases/sendMessageToChannel/sendMessageToChannelUseCase";
import SendCodewarsLeaderboardToChannelUseCase from "../../application/usecases/sendCodewarsLeaderboardToChannel/sendCodewarsLeaderboardToChannelUseCase";
import { Context } from "../../types";

vi.mock("../../application/usecases/sendMessageToChannel/sendMessageToChannelUseCase");
vi.mock("../../application/usecases/sendCodewarsLeaderboardToChannel/sendCodewarsLeaderboardToChannelUseCase");

vi.mock("fs", () => ({
promises: {
readFile: vi.fn(),
},
}));

describe("CommandUseCaseResolver", () => {
let commandUseCaseResolver: CommandUseCaseResolver;
const mockContext: Context = {
channelId: "test-channel",
};

beforeEach(async () => {
(fs.readFile as vi.Mock).mockResolvedValueOnce(
JSON.stringify({
"!ja": "Olá! Test message",
"!oc": ":warning: Only Code questions!",
})
);

commandUseCaseResolver = new CommandUseCaseResolver({
messageRepository: {
getRandomIntroMessage: vi.fn(),
getRandomWelcomingMessage: vi.fn(),
},
chatService: {
sendMessageToChannel: vi.fn(),
},
loggerService: { log: vi.fn() },
channelResolver: {
getBySlug: vi.fn(),
},
kataService: {
getLeaderboard: vi.fn().mockResolvedValue([]),
},
});
});

it("should send the correct message for !ja command", async () => {
it("should invoke for !ja command", async () => {
await commandUseCaseResolver.resolveByCommand("!ja", mockContext);

expect(SendMessageToChannelUseCase.prototype.execute).toHaveBeenCalledWith({
channelId: "test-channel",
message: "Olá! Test message",
});
expect(() => commandUseCaseResolver.resolveByCommand("!ja", mockContext)).not.toThrow();
});

it("should send the correct message for !oc command", async () => {
it("should invoke for !oc command", async () => {
await commandUseCaseResolver.resolveByCommand("!oc", mockContext);

expect(SendMessageToChannelUseCase.prototype.execute).toHaveBeenCalledWith({
channelId: "test-channel",
message: ":warning: Only Code questions!",
});
expect(() => commandUseCaseResolver.resolveByCommand("!oc", mockContext)).not.toThrow();
});

it("should handle the !cwl command by sending the leaderboard", async () => {
it("should invoke for !cwl command", async () => {
await commandUseCaseResolver.resolveByCommand("!cwl", mockContext);

expect(SendCodewarsLeaderboardToChannelUseCase.prototype.execute).toHaveBeenCalledWith({
channelId: "test-channel",
});
expect(() => commandUseCaseResolver.resolveByCommand("!cwl", mockContext)).not.toThrow();
});

it("should throw UseCaseNotFound error for unknown command", async () => {
Expand Down

0 comments on commit 037a859

Please sign in to comment.