diff --git a/package-lock.json b/package-lock.json index d36d6cc..2c24ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "six-cities", "version": "7.0.0", + "dependencies": { + "chalk": "5.3.0" + }, "devDependencies": { "@types/node": "20.12.7", "@typescript-eslint/eslint-plugin": "6.7.0", @@ -821,16 +824,11 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1401,6 +1399,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4324,14 +4338,9 @@ "dev": true }, "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" }, "ci-info": { "version": "3.8.0", @@ -4586,6 +4595,18 @@ "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } } }, "eslint-config-htmlacademy": { diff --git a/package.json b/package.json index 02b8cab..2b5a9ea 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ "engines": { "node": "^20.0.0", "npm": ">=10" + }, + "dependencies": { + "chalk": "5.3.0" } } diff --git a/src/cli/cli.application.ts b/src/cli/cli.application.ts new file mode 100644 index 0000000..781251a --- /dev/null +++ b/src/cli/cli.application.ts @@ -0,0 +1,44 @@ +import { CommandParser } from './command-parser.js'; +import { Command } from './commands/command.interface.js'; + +type CommandCollection = Record + +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); + } +} diff --git a/src/cli/command-parser.ts b/src/cli/command-parser.ts new file mode 100644 index 0000000..e4da774 --- /dev/null +++ b/src/cli/command-parser.ts @@ -0,0 +1,19 @@ +type ParsedCommand = Record + +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; + } +} diff --git a/src/cli/commands/command.interface.ts b/src/cli/commands/command.interface.ts new file mode 100644 index 0000000..382d415 --- /dev/null +++ b/src/cli/commands/command.interface.ts @@ -0,0 +1,5 @@ +export interface Command { + getName(): string; + execute(... parameters: string[]): void; +} + diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts new file mode 100644 index 0000000..b0a6d42 --- /dev/null +++ b/src/cli/commands/help.command.ts @@ -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 { + console.info(` + Программа для подготовки данных для REST API сервера. + Пример: + cli.js -- [--arguments] + Команды: + --version: # выводит номер версии + --help: # печатает этот текст + --import : # импортирует данные из TSV + --generate # генерирует произвольное количество тестовых данных + `); + } +} diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts new file mode 100644 index 0000000..8e9763c --- /dev/null +++ b/src/cli/commands/import.command.ts @@ -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}`); + } + } +} diff --git a/src/cli/commands/version.command.ts b/src/cli/commands/version.command.ts new file mode 100644 index 0000000..6164946 --- /dev/null +++ b/src/cli/commands/version.command.ts @@ -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 { + 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); + } + } + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..67ca9c5 --- /dev/null +++ b/src/cli/index.ts @@ -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'; diff --git a/src/main.cli.ts b/src/main.cli.ts new file mode 100644 index 0000000..3498237 --- /dev/null +++ b/src/main.cli.ts @@ -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(); diff --git a/src/shared/libs/file-reader/file-reader.interface.ts b/src/shared/libs/file-reader/file-reader.interface.ts new file mode 100644 index 0000000..59f8279 --- /dev/null +++ b/src/shared/libs/file-reader/file-reader.interface.ts @@ -0,0 +1,3 @@ +export interface FileReader { + read(): void; +} diff --git a/src/shared/libs/file-reader/index.ts b/src/shared/libs/file-reader/index.ts new file mode 100644 index 0000000..4578411 --- /dev/null +++ b/src/shared/libs/file-reader/index.ts @@ -0,0 +1,2 @@ +export { TSVFileReader } from './tsv-file-reader.js'; +export { FileReader } from './file-reader.interface.js'; diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts new file mode 100644 index 0000000..2ab334b --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -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(); + } +}