Skip to content

Commit

Permalink
Реализует CLI приложение.
Browse files Browse the repository at this point in the history
  • Loading branch information
RnizereB committed Sep 22, 2024
1 parent 9ce8c96 commit e5cbdc8
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 17 deletions.
55 changes: 38 additions & 17 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
"engines": {
"node": "^20.0.0",
"npm": ">=10"
},
"dependencies": {
"chalk": "5.3.0"
}
}
44 changes: 44 additions & 0 deletions src/cli/cli.application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CommandParser } from './command-parser.js';
import { Command } from './commands/command.interface.js';

type CommandCollection = Record<string, Command>

export class CLIApplication {

constructor(
private readonly defaultCommand: string = '--help'
) {}

private commands: CommandCollection = {};

public registerCommands(commandList: Command[]): void {
commandList.forEach((command) => {

if (Object.hasOwn(this.commands, command.getName())) {
throw new Error (`Command ${command.getName()} is already registered`);
}

this.commands[command.getName()] = command;
});
}

public getDefaultCommand(): Command | never {
if (! this.commands[this.defaultCommand]) {
throw new Error(`The default command (${this.defaultCommand}) is not registered.`);
}

return this.commands[this.defaultCommand];
}

public getCommand(commandName: string): Command {
return this.commands[commandName] ?? this.getDefaultCommand();
}

public processCommand(argv: string[]): void {
const parsedCommand = CommandParser.parse(argv);
const [commandName] = Object.keys(parsedCommand);
const command = this.getCommand(commandName);
const commandArguments = parsedCommand[commandName] ?? [];
command.execute(...commandArguments);
}
}
19 changes: 19 additions & 0 deletions src/cli/command-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type ParsedCommand = Record<string, string[]>

export class CommandParser {
static parse(cliArguments: string[]): ParsedCommand {
const parsedCommand: ParsedCommand = {};
let currentCommand = '';

for (const argument of cliArguments) {
if (argument.startsWith('--')) {
parsedCommand[argument] = [];
currentCommand = argument;
} else if (currentCommand && argument) {
parsedCommand[currentCommand].push(argument);
}
}

return parsedCommand;
}
}
5 changes: 5 additions & 0 deletions src/cli/commands/command.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Command {
getName(): string;
execute(... parameters: string[]): void;
}

21 changes: 21 additions & 0 deletions src/cli/commands/help.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from './command.interface.js';

export class HelpCommand implements Command {

public getName(): string {
return '--help';
}

public async execute(..._parameters: string[]): Promise<void> {
console.info(`
Программа для подготовки данных для REST API сервера.
Пример:
cli.js --<command> [--arguments]
Команды:
--version: # выводит номер версии
--help: # печатает этот текст
--import <path>: # импортирует данные из TSV
--generate <n> <path> <url> # генерирует произвольное количество тестовых данных
`);
}
}
25 changes: 25 additions & 0 deletions src/cli/commands/import.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { TSVFileReader } from '../../shared/libs/file-reader/tsv-file-reader.js';
import { Command } from './command.interface.js';

export class ImportCommand implements Command {
public getName(): string {
return '--import';
}

public execute(...parameters: string[]): void {
const [filename] = parameters;
const fileReader = new TSVFileReader(filename.trim());

try {
fileReader.read();
console.log(fileReader.toArray());
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}

console.error(`Can't import data from file: ${filename}`);
console.error(`Details: ${err.message}`);
}
}
}
49 changes: 49 additions & 0 deletions src/cli/commands/version.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Command } from './command.interface.js';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

type PackageJSONConfig = {
version: string;
}

const isPackageJSONConfig = (value: unknown): value is PackageJSONConfig => (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.hasOwn(value, 'version')
);

export class VersionCommand implements Command {

constructor(
private readonly filePath: string = 'package.json'
) {}

private readVersion(): string {
const jsonContent = readFileSync(resolve(this.filePath), 'utf-8');
const importedContent: unknown = JSON.parse(jsonContent);

if (! isPackageJSONConfig(importedContent)) {
throw new Error('Failed to parse json content.');
}

return importedContent.version;
}

public getName(): string {
return '--version';
}

public async execute(..._parameters: string[]): Promise<void> {
try {
const version = this.readVersion;
console.info(version);
} catch (error: unknown) {
console.error(`Failed to read version from ${this.filePath}`);

if (error instanceof Error) {
console.error(error.message);
}
}
}
}
5 changes: 5 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { CLIApplication } from './cli.application.js';
export { CommandParser } from './command-parser.js';
export { HelpCommand } from './commands/help.command.js';
export { VersionCommand } from './commands/version.command.js';
export { ImportCommand } from './commands/import.command.js';
10 changes: 10 additions & 0 deletions src/main.cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CLIApplication, HelpCommand, VersionCommand, ImportCommand } from './cli/index.js';

const bootstrap = () => {
const cliApplication = new CLIApplication();
cliApplication.registerCommands([new HelpCommand(), new VersionCommand(), new ImportCommand()]);

cliApplication.processCommand(process.argv);
};

bootstrap();
3 changes: 3 additions & 0 deletions src/shared/libs/file-reader/file-reader.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface FileReader {
read(): void;
}
2 changes: 2 additions & 0 deletions src/shared/libs/file-reader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TSVFileReader } from './tsv-file-reader.js';
export { FileReader } from './file-reader.interface.js';
97 changes: 97 additions & 0 deletions src/shared/libs/file-reader/tsv-file-reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { readFileSync } from 'node:fs';
import { FileReader } from './index.js';
import { Offer, TypeUser } from '../../types/index.js';
export class TSVFileReader implements FileReader {
private rawData = '';

constructor(
private readonly filename: string
) {}

private parseRawDataToOffers(): Offer[] {
return this.rawData
.split('\n')
.filter((row) => row.trim().length > 0)
.map((line) => this.parseLineToOffer(line));
}

private parseLineToOffer(line: string): Offer {
const [
title,
description,
date,
city,
previewImage,
images,
isFavorite,
isPremium,
rating,
type,
bedrooms,
maxAdults,
price,
goods,
name,
email,
avatarUser,
password,
typeUser,
] = line.split('\t');

return {
title,
description,
postDate: new Date(date),
city,
previewImage,
images: this.parseImages(images),
isFavorite: this.parseBoolean(isFavorite),
isPremium: this.parseBoolean(isPremium),
rating: this.parseStringToNumber(rating),
type,
bedrooms: this.parseStringToNumber(bedrooms),
maxAdults: this.parseStringToNumber(maxAdults),
price: this.parseStringToNumber(price),
goods: this.parseGoods(goods),
host: this.parseHost(name, email, avatarUser, password, typeUser)
};
}

private parseBoolean(itemString: string): boolean {
if (itemString === 'true') {
return true;
}
return false;
}

private parseStringToNumber(itemString: string): number {
return Number.parseInt(itemString, 10);
}

private parseGoods(goods: string): {good: string}[] {
return goods.split(',').map((good) => ({good}));
}

private parseImages(images: string): {img: string}[] {
return images.split(',').map((img) => ({img}));
}

private parseHost(name: string, email: string, avatarUser: string, password: string, typeUser: string) {
return {name, email, avatarUser, password, typeUser:TypeUser[typeUser as 'Pro' | 'Usual']};
}

private validateRawData(): void {
if (! this.rawData) {
throw new Error('File was not read');
}
}

public read(): void {
this.rawData = readFileSync(this.filename, 'utf-8');
}

public toArray(): Offer[] {
this.validateRawData();
return this.parseRawDataToOffers();
}
}

0 comments on commit e5cbdc8

Please sign in to comment.