From 0e6c97ab8d3a4c8a362813fe87ddcce10f31a818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=A1=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Fri, 15 Nov 2024 16:24:26 +0300 Subject: [PATCH 1/4] test --- Workflow.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Workflow.md b/Workflow.md index 8b87981..4ba5cc9 100644 --- a/Workflow.md +++ b/Workflow.md @@ -83,3 +83,9 @@ npm start ### Остальное Все остальные файлы в проекте являются служебными. Пожалуйста, не удаляйте и не изменяйте их самовольно. Только если того требует задание или наставник. + +## Полезные команды + +npm run ts ./src/main.cli.ts -- --import +./dist/main.cli.js --help +chmod u+x ./dist/main.cli.js diff --git a/package.json b/package.json index 02b8cab..8b2eb7a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "lint": "eslint src/ --ext .ts", "compile": "tsc -p tsconfig.json", "clean": "rimraf dist", - "ts": "tsc --noEmit && node --no-warnings=--no-warnings=ExperimentalWarning --loader ts-node/esm" + "ts": "tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm" }, "devDependencies": { "@types/node": "20.12.7", From 9f00eb6599666480d9af266e77048f8b0e1a4c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=A1=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Fri, 15 Nov 2024 16:38:15 +0300 Subject: [PATCH 2/4] fix main --- src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index b4de394..e69de29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1 +0,0 @@ -11 From 1b712666730ce402791c98bd4cadc3a155b44c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=A1=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Sun, 24 Nov 2024 11:44:40 +0300 Subject: [PATCH 3/4] rdy task --- mocks/data.tsv | 2 + src/cli/cli-application.ts | 40 ++++++++ src/cli/command-parser.ts | 19 ++++ src/cli/commands/command.interface.ts | 5 + src/cli/commands/help.command.ts | 20 ++++ src/cli/commands/import.command.ts | 26 +++++ src/cli/commands/version.command.ts | 51 ++++++++++ src/cli/index.ts | 6 ++ src/main.cli.ts | 15 +++ .../libs/file-reader/file-reader.interface.ts | 3 + src/shared/libs/file-reader/index.ts | 2 + .../libs/file-reader/tsv-file-reader.ts | 99 +++++++++++++++++++ src/shared/types/city.type.ts | 8 ++ src/shared/types/goods.type.ts | 9 ++ src/shared/types/index.ts | 6 ++ src/shared/types/location.type.ts | 4 + src/shared/types/offer-type.type.ts | 6 ++ src/shared/types/offer.type.ts | 27 +++++ src/shared/types/user.type.ts | 10 ++ 19 files changed, 358 insertions(+) create mode 100644 mocks/data.tsv create mode 100644 src/cli/cli-application.ts create mode 100644 src/cli/command-parser.ts create mode 100644 src/cli/commands/command.interface.ts create mode 100644 src/cli/commands/help.command.ts create mode 100644 src/cli/commands/import.command.ts create mode 100644 src/cli/commands/version.command.ts create mode 100644 src/cli/index.ts create mode 100644 src/main.cli.ts create mode 100644 src/shared/libs/file-reader/file-reader.interface.ts create mode 100644 src/shared/libs/file-reader/index.ts create mode 100644 src/shared/libs/file-reader/tsv-file-reader.ts create mode 100644 src/shared/types/city.type.ts create mode 100644 src/shared/types/goods.type.ts create mode 100644 src/shared/types/index.ts create mode 100644 src/shared/types/location.type.ts create mode 100644 src/shared/types/offer-type.type.ts create mode 100644 src/shared/types/offer.type.ts create mode 100644 src/shared/types/user.type.ts diff --git a/mocks/data.tsv b/mocks/data.tsv new file mode 100644 index 0000000..b4c4f0f --- /dev/null +++ b/mocks/data.tsv @@ -0,0 +1,2 @@ +Nice, cozy, warm big bed apartment Design interior in most sympathetic area! Complitely renovated, well-equipped, cosy studio in idyllic, over 100 years old wooden house. Calm street, fast connection to center and airport. 2024-04-07T08:45:40.283Z Amsterdam https://16.design.htmlacademy.pro/static/hotel/17.jpg https://16.design.htmlacademy.pro/static/hotel/16.jpg;https://16.design.htmlacademy.pro/static/hotel/13.jpg;https://16.design.htmlacademy.pro/static/hotel/11.jpg;https://16.design.htmlacademy.pro/static/hotel/20.jpg;https://16.design.htmlacademy.pro/static/hotel/6.jpg;https://16.design.htmlacademy.pro/static/hotel/8.jpg true false 2,4 house 5 7 709 Towels;Laptop friendly workspace;Baby seat Ангелина angelina@mail.ru https://16.design.htmlacademy.pro/static/host/avatar-angelina.jpg password1 ordinary 10 48.868610000000004;2.342499 +Tile House Peaceful studio in the most wanted area in town. Quiet house Near of everything. Completely renovated. Lovely neighbourhood, lot of trendy shops, restaurants and bars in a walking distance. 2023-05-02T08:45:40.283Z Brussels https://16.design.htmlacademy.pro/static/hotel/20.jpg https://16.design.htmlacademy.pro/static/hotel/16.jpg;https://16.design.htmlacademy.pro/static/hotel/13.jpg;https://16.design.htmlacademy.pro/static/hotel/11.jpg;https://16.design.htmlacademy.pro/static/hotel/20.jpg;https://16.design.htmlacademy.pro/static/hotel/6.jpg;https://16.design.htmlacademy.pro/static/hotel/8.jpg true false 4,5 house 6 8 152 Fridge;Towels;Laptop friendly workspace;Washer Екатерина ekaterina@mail.ru https://16.design.htmlacademy.pro/static/host/avatar-angelina.jpg password2 pro 10 50.950361;6.961974 diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts new file mode 100644 index 0000000..4276fb0 --- /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..9ff07ef --- /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..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..3a73554 --- /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..9febecd --- /dev/null +++ b/src/cli/commands/version.command.ts @@ -0,0 +1,51 @@ +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..9fc4b23 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,6 @@ +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..b8f8acb --- /dev/null +++ b/src/main.cli.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import { CLIApplication, HelpCommand, VersionCommand, ImportCommand } 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..fd68e64 --- /dev/null +++ b/src/shared/libs/file-reader/index.ts @@ -0,0 +1,2 @@ +export { FileReader } from './file-reader.interface.js'; +export { TSVFileReader } 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..2dfd712 --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -0,0 +1,99 @@ +import { readFileSync } from 'node:fs'; + +import { FileReader } from './file-reader.interface.js'; +import { Offer, City, OfferType, Goods, User, Location } from '../../types/index.js'; +import { TypeUser } from '../../types/user.type.js'; + + +export class TSVFileReader implements FileReader { + private rawData = ''; + + constructor( + private readonly filename: string + ) {} + + private validateRawData(): void { + if (!this.rawData) { + throw new Error('File was not read'); + } + } + + 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, + postDate, + city, + previewImage, + images, + isPremium, + isFavorite, + rating, + type, + bedrooms, + maxAdults, + price, + goods, + firstname, + email, + avatarPath, + password, + typeUser, + reviewsCount, + location + ] = line.split('\t'); + + return { + title, + description, + postDate: new Date(postDate), + city: city as City, + previewImage, + images:this.parseImages(images), + isPremium: isPremium === 'true', + isFavorite: isFavorite === 'true', + rating: Number.parseInt(rating, 10), + type:type as OfferType, + bedrooms:Number.parseInt(bedrooms,10), + maxAdults:Number.parseInt(maxAdults,10), + price: Number.parseInt(price, 10), + goods: this.parseGoods(goods), + user: this.parseUser(firstname, email, avatarPath, password, typeUser as TypeUser), + reviewsCount:Number.parseInt(reviewsCount,10), + location: this.parseLocation(location) + }; + } + + private parseGoods(goodsString: string): Goods[] { + return goodsString.split(';').map((name) => name as Goods); + } + + private parseLocation(locationString: string): Location { + const [latitude, longitude] = locationString.split(';').map((name) => name); + return {latitude:Number.parseFloat(latitude),longitude:Number.parseFloat(longitude)} ; + } + + private parseImages(imagesString: string): string[] { + return imagesString.split(';').map((name) => name); + } + + private parseUser(firstname: string, email: string, avatarPath: string, password: string,type: TypeUser): User { + return { firstname, email, avatarPath, password, type }; + } + + public read(): void { + this.rawData = readFileSync(this.filename, { encoding: 'utf-8' }); + } + + public toArray(): Offer[] { + this.validateRawData(); + return this.parseRawDataToOffers(); + } +} diff --git a/src/shared/types/city.type.ts b/src/shared/types/city.type.ts new file mode 100644 index 0000000..54665d1 --- /dev/null +++ b/src/shared/types/city.type.ts @@ -0,0 +1,8 @@ +export enum City { + Paris = 'Paris', + Cologne = 'Cologne', + Brussels = 'Brussels', + Amsterdam = 'Amsterdam', + Hamburg = 'Hamburg', + Dusseldorf = 'Dusseldorf', +} diff --git a/src/shared/types/goods.type.ts b/src/shared/types/goods.type.ts new file mode 100644 index 0000000..a9397b1 --- /dev/null +++ b/src/shared/types/goods.type.ts @@ -0,0 +1,9 @@ +export enum Goods { + Breakfast = 'Breakfast', + AirConditioning = 'Air conditioning', + Laptop = 'Laptop friendly workspace', + Baby = 'Baby seat', + Washer = 'Washer', + Towels = 'Towels', + Fridge = 'Fridge' +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..bc3bc03 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,6 @@ +export { Offer} from './offer.type.js'; +export { City } from './city.type.js'; +export { Goods } from './goods.type.js'; +export { Location } from './location.type.js'; +export { OfferType } from './offer-type.type.js'; +export { User } from './user.type.js'; diff --git a/src/shared/types/location.type.ts b/src/shared/types/location.type.ts new file mode 100644 index 0000000..ca31958 --- /dev/null +++ b/src/shared/types/location.type.ts @@ -0,0 +1,4 @@ +export type Location = { + latitude: number; + longitude: number; +} diff --git a/src/shared/types/offer-type.type.ts b/src/shared/types/offer-type.type.ts new file mode 100644 index 0000000..3c6074c --- /dev/null +++ b/src/shared/types/offer-type.type.ts @@ -0,0 +1,6 @@ +export enum OfferType { + Apartment = 'Apartment', + House = 'House', + Room = 'Room', + Hotel = 'Hotel', +} diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts new file mode 100644 index 0000000..9695210 --- /dev/null +++ b/src/shared/types/offer.type.ts @@ -0,0 +1,27 @@ +import { City } from './city.type.js'; +import { Goods } from './goods.type.js'; +import { Location } from './location.type.js'; +import { OfferType } from './offer-type.type.js'; +import { User } from './user.type.js'; + +export type OfferPreview = { + title: string;//наименование + type: OfferType;//тип жилья + price: number;//стоимость аренды + city: City;//город + location: Location;//координаты предложения + isFavorite: boolean;//флаг избранное + isPremium: boolean;//флаг премиум + rating: number;//рейтинг + previewImage: string;//превью изображения +}; +export type Offer = OfferPreview & { + description: string;//описание + postDate:Date;//дата публикации + bedrooms: number;//количество комнат + goods: Goods[];//удобства + user: User;//пользователь + images: string[];//фотографии жилья + maxAdults: number;//Количество гостей + reviewsCount :number;//количество отзывов +}; diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts new file mode 100644 index 0000000..7205d61 --- /dev/null +++ b/src/shared/types/user.type.ts @@ -0,0 +1,10 @@ +export type TypeUser = 'ordinary' | 'pro'; + +export type User = { + firstname: string; + email: string; + avatarPath: string; + password: string; + type:TypeUser; + +} From 4f9d85f076d12a24b46c3121cc3c48ba331b7270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2=20=D0=A1=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0?= Date: Sun, 24 Nov 2024 11:51:13 +0300 Subject: [PATCH 4/4] fix lint --- src/cli/cli-application.ts | 11 +++++------ src/cli/commands/import.command.ts | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts index 4276fb0..9d3d6d3 100644 --- a/src/cli/cli-application.ts +++ b/src/cli/cli-application.ts @@ -1,14 +1,13 @@ -import { Command } from './commands/command.interface.js'; -import {CommandParser} from "./command-parser.js"; +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' - ) {} + constructor(private readonly defaultCommand: string = '--help') { + } public registerCommands(commandList: Command[]): void { commandList.forEach((command) => { @@ -24,7 +23,7 @@ export class CLIApplication { } public getDefaultCommand(): Command | never { - if (! this.commands[this.defaultCommand]) { + if (!this.commands[this.defaultCommand]) { throw new Error(`The default command (${this.defaultCommand}) is not registered.`); } return this.commands[this.defaultCommand]; diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 3a73554..fb05344 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,5 +1,5 @@ -import { Command } from './command.interface.js'; -import {TSVFileReader} from "../../shared/libs/file-reader/index.js"; +import {Command} from './command.interface.js'; +import {TSVFileReader} from '../../shared/libs/file-reader/index.js'; export class ImportCommand implements Command { public getName(): string {