diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv new file mode 100644 index 0000000..00d2dfb --- /dev/null +++ b/mocks/mock-data.tsv @@ -0,0 +1,4 @@ +Кристина Виновен или нет? Какая разница. Исход один. Сверхспособности не помогут. 2022-04-06T08:45:40.283Z institute-book.jpg Sell 1080 Книги;Журналы Пьер Безухов test@pisem.local awesome-avatar.jpg +Зелёная миля Как похудеть и не сойти с ума? А как вернуть вес обратно? Необычная история противостояния. 2022-04-11T08:45:40.284Z book.jpg Buy 888 Посуда;Разное;Диски Роланд Дискейн president@dka.local photo.jpg +Тёмная башня Прими звонок и превратись в зомби. Рассказ о зомбировании через средства связи. 2022-04-05T08:45:40.284Z book.jpg Buy 780 Книги;Журналы;Посуда;Разное;Диски Олег Сидоров president@dka.local photo.jpg +Долгая прогулка Идти или остановиться? Вопрос без правильного ответа. Рассказ о долгой и смертельной прогулки. 2022-04-06T08:45:40.284Z book.jpg Sell 1610 Книги Иван Иванов president@dka.local awesome-avatar.jpg diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts new file mode 100644 index 0000000..bdc09cc --- /dev/null +++ b/src/cli/cli-application.ts @@ -0,0 +1,40 @@ +import { Command } from './commands/command.interface.js'; +import { CommandParser } from './command-parser.js'; + +type CommandCollection = Record; + +export class CLIApplication { + private commands: CommandCollection = {}; + + constructor( + private readonly defaultCommand: string = '--help' + ) {} + + 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 getCommand(commandName: string): Command { + return this.commands[commandName] ?? this.getDefaultCommand(); + } + + 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 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..8b6baa7 --- /dev/null +++ b/src/cli/commands/command.interface.ts @@ -0,0 +1,4 @@ +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..42ccc91 --- /dev/null +++ b/src/cli/commands/help.command.ts @@ -0,0 +1,20 @@ +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..3beccc9 --- /dev/null +++ b/src/cli/commands/import.command.ts @@ -0,0 +1,26 @@ +import { Command } from './command.interface.js'; +import { TSVFileReader } from '../../shared/libs/file-reader/index.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..b6aeace --- /dev/null +++ b/src/cli/commands/version.command.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { Command } from './command.interface.js'; + +type PackageJSONConfig = { + version: string; +} + +function isPackageJSONConfig(value: unknown): value is PackageJSONConfig { + return ( + 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..f28f95f --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,5 @@ +export * from './cli-application.js'; +export * from './command-parser.js'; +export * from './commands/help.command.js'; +export * from './commands/version.command.js'; +export * from './commands/import.command.js'; diff --git a/src/main.cli.ts b/src/main.cli.ts new file mode 100644 index 0000000..a737264 --- /dev/null +++ b/src/main.cli.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import { CLIApplication, HelpCommand, ImportCommand, VersionCommand } from './cli/index.js'; + +function 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..4e9fa00 --- /dev/null +++ b/src/shared/libs/file-reader/index.ts @@ -0,0 +1,2 @@ +export * from './file-reader.interface.js'; +export * from './tsv-file-reader.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..934d945 --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -0,0 +1,37 @@ +import { FileReader } from './file-reader.interface.js'; +import { readFileSync } from 'node:fs'; +import { Offer, OfferType } from '../../types/index.js'; + +export class TSVFileReader implements FileReader { + private rawData = ''; + + constructor( + private readonly filename: string + ) {} + + public read(): void { + this.rawData = readFileSync(this.filename, { encoding: 'utf-8' }); + } + + public toArray(): Offer[] { + if (!this.rawData) { + throw new Error('File was not read'); + } + + return this.rawData + .split('\n') + .filter((row) => row.trim().length > 0) + .map((line) => line.split('\t')) + .map(([title, description, createdDate, image, type, price, categories, firstname, lastname, email, avatarPath]) => ({ + title, + description, + postDate: new Date(createdDate), + image, + type: OfferType[type as 'Buy' | 'Sell'], + categories: categories.split(';') + .map((name) => ({name})), + price: Number.parseInt(price, 10), + user: { email, firstname, lastname, avatarPath }, + })); + } +} diff --git a/src/shared/types/category.type.ts b/src/shared/types/category.type.ts new file mode 100644 index 0000000..568dc00 --- /dev/null +++ b/src/shared/types/category.type.ts @@ -0,0 +1,3 @@ +export type Category = { + name: string; +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..f669f58 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,4 @@ +export * from './category.type.js'; +export * from './offer-type.enum.js'; +export * from './user.type.js'; +export * from './offer.type.js'; diff --git a/src/shared/types/offer-type.enum.ts b/src/shared/types/offer-type.enum.ts new file mode 100644 index 0000000..8460f95 --- /dev/null +++ b/src/shared/types/offer-type.enum.ts @@ -0,0 +1,4 @@ +export enum OfferType { + Buy = 'Buy', + Sell = 'Sell', +} diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts new file mode 100644 index 0000000..ed207dd --- /dev/null +++ b/src/shared/types/offer.type.ts @@ -0,0 +1,14 @@ +import { OfferType } from './offer-type.enum.js'; +import { Category } from './category.type.js'; +import { User } from './user.type.js'; + +export type Offer = { + title: string; + description: string; + postDate: Date; + image: string; + type: OfferType + price: number; + categories: Category[]; + user: User; +} diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts new file mode 100644 index 0000000..711dbe7 --- /dev/null +++ b/src/shared/types/user.type.ts @@ -0,0 +1,6 @@ +export type User = { + email: string; + avatarPath: string; + firstname: string; + lastname: string; +}