diff --git a/.editorconfig b/.editorconfig index b2d515d..494e53e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,7 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true +quote_type = single [*.md] trim_trailing_whitespace = false diff --git a/mocks/offers.tsv b/mocks/offers.tsv new file mode 100644 index 0000000..05d9050 --- /dev/null +++ b/mocks/offers.tsv @@ -0,0 +1,2 @@ +1 Canal View Prinsengracht Discover daily local life in city center, friendly neighborhood, clandestine casino, karaoke, old-style artisans, art gallery and artist studio downstairs. 2024-08-10T08:11:23.283Z Paris https://16.design.htmlacademy.pro/static/hotel/3.jpg https://16.design.htmlacademy.pro/static/hotel/17.jpg;https://16.design.htmlacademy.pro/static/hotel/8.jpg;https://16.design.htmlacademy.pro/static/hotel/4.jpg;https://16.design.htmlacademy.pro/static/hotel/10.jpg;https://16.design.htmlacademy.pro/static/hotel/13.jpg;https://16.design.htmlacademy.pro/static/hotel/12.jpg true false 3 house 1 6 235 Kitchen;Babyseat;Breakfast;CableTV Angelina 48.868610000000004 2.342499 +2 Wood and stone place Relax, rejuvenate and unplug in this ultimate rustic getaway experience in the country. In our beautiful screened Pondhouse, you can gaze at the stars and listen to the sounds of nature from your cozy warm bed. 2024-06-11T10:05:00.283Z Paris https://16.design.htmlacademy.pro/static/hotel/15.jpg https://16.design.htmlacademy.pro/static/hotel/3.jpg;https://16.design.htmlacademy.pro/static/hotel/4.jpg;https://16.design.htmlacademy.pro/static/hotel/9.jpg;https://16.design.htmlacademy.pro/static/hotel/2.jpg;https://16.design.htmlacademy.pro/static/hotel/1.jpg;https://16.design.htmlacademy.pro/static/hotel/10.jpg true false 4,6 house 4 3 376 CableTV;Fridge;Babyseat;Washer;Airconditioning Mikhail 48.858610000000006 2.330499 diff --git a/mocks/users.ts b/mocks/users.ts new file mode 100644 index 0000000..cccd895 --- /dev/null +++ b/mocks/users.ts @@ -0,0 +1,14 @@ +import { UserInfo } from '../src/shared/types/index.js'; + +export const usersMock: UserInfo[] = [ + { + name: 'Angelina', + avatarUrl: 'http://avatar.ru/images/1.png', + isPro: true, + }, + { + name: 'Mikhail', + avatarUrl: 'http://avatar.ru/images/2.png', + isPro: false, + }, +]; diff --git a/package-lock.json b/package-lock.json index d36d6cc..bc82ac6 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,12 @@ } }, "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==", + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1401,6 +1400,23 @@ "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, + "license": "MIT", + "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", @@ -1855,6 +1871,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3392,6 +3409,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4324,14 +4342,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 +4599,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..13cbd69 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..24cc38a --- /dev/null +++ b/src/cli/cli-application.ts @@ -0,0 +1,40 @@ +import { CommandParser } from './command-parser.js'; +import { Command } from './commands/command.interface.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..789ae18 --- /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..19d2a04 --- /dev/null +++ b/src/cli/commands/help-command.ts @@ -0,0 +1,36 @@ +import chalk from 'chalk'; +import { Command } from './command.interface.js'; + +export class HelpCommand implements Command { + public getName(): string { + return '--help'; + } + + public async execute(..._parameters: string[]): Promise { + const helpText = this.getCommandsFormat(` + --version: # выводит номер версии + --help: # печатает этот текст + --import : # импортирует данные из TSV + --generate : # генерирует произвольное количество тестовых данных`); + + console.info(` + Программа для подготовки данных для REST API сервера. + Пример: + ${chalk.green('cli.js -- [--arguments]')} + Команды: ${helpText} + `); + } + + private getCommandsFormat(text: string): string { + return text + .split('\n') + .map((line) => { + if (!line.trim().length) { + return line; + } + const textCols = line.split(':'); + return [chalk.blue(textCols[0]), textCols[1]].join(':'); + }) + .join('\n'); + } +} diff --git a/src/cli/commands/import-command.ts b/src/cli/commands/import-command.ts new file mode 100644 index 0000000..a829c19 --- /dev/null +++ b/src/cli/commands/import-command.ts @@ -0,0 +1,25 @@ +import { TSVFileReader } from '../../shared/libs/file-reader/index.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..4ec59ea --- /dev/null +++ b/src/cli/commands/version-command.ts @@ -0,0 +1,49 @@ +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..282206a --- /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 { ImportCommand } from './commands/import-command.js'; +export { VersionCommand } from './commands/version-command.js'; diff --git a/src/main.cli.ts b/src/main.cli.ts new file mode 100644 index 0000000..62e9b8c --- /dev/null +++ b/src/main.cli.ts @@ -0,0 +1,20 @@ +#!/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/const.ts b/src/shared/const.ts new file mode 100644 index 0000000..7d20993 --- /dev/null +++ b/src/shared/const.ts @@ -0,0 +1,47 @@ +import { CityName } from './types/city-name.enum.js'; +import { City } from './types/city.type.js'; + +export const Cities: [City, City, City, City, City, City] = [ + { + name: CityName.Paris, + location: { + latitude: 48.85661, + longitude: 2.351499, + }, + }, + { + name: CityName.Cologne, + location: { + latitude: 50.938361, + longitude: 6.959974, + }, + }, + { + name: CityName.Brussels, + location: { + latitude: 50.846557, + longitude: 4.351697, + }, + }, + { + name: CityName.Amsterdam, + location: { + latitude: 52.37454, + longitude: 4.897976, + }, + }, + { + name: CityName.Hamburg, + location: { + latitude: 53.550341, + longitude: 10.000654, + }, + }, + { + name: CityName.Dusseldorf, + location: { + latitude: 51.225402, + longitude: 6.776314, + }, + }, +]; 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..20bb5be --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -0,0 +1,118 @@ +import { readFileSync } from 'node:fs'; +import { usersMock } from '../../../../mocks/users.js'; +import { Cities } from '../../const.js'; +import { + City, + GoodType, + Location, + Offer, + OfferType, + UserInfo, +} from '../../types/index.js'; +import { FileReader } from './file-reader.interface.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 [ + id, + title, + description, + createdDate, + cityName, + previewImage, + images, + isPremium, + isFavorite, + rating, + type, + bedrooms, + maxAdults, + price, + goods, + hostName, + latitude, + longitude, + ] = line.split('\t'); + + return { + id, + title, + type: type as OfferType, + price: Number.parseInt(price, 10), + city: this.parseCity(cityName), + location: this.parseLocation(latitude, longitude), + isFavorite: this.parseBooleanField(isFavorite), + isPremium: this.parseBooleanField(isPremium), + rating: Number.parseFloat(rating), + description, + bedrooms: Number.parseInt(bedrooms, 10), + goods: this.parseGoods(goods), + host: this.parseUser(hostName), + images: this.parseImages(images), + previewImage, + maxAdults: Number.parseInt(maxAdults, 10), + createdDate: new Date(createdDate), + }; + } + + private parseGoods(goodsString: string): GoodType[] { + return goodsString.split(';').map((good) => good as GoodType); + } + + private parseImages(imageString: string): string[] { + return imageString.split(';'); + } + + private parseBooleanField(value: string): boolean { + return value === 'true'; + } + + private parseLocation(latitude: string, longitude: string): Location { + return { + latitude: Number.parseFloat(latitude), + longitude: Number.parseFloat(longitude), + }; + } + + private parseCity(cityName: string): City { + const cityIndex = Cities.findIndex((city) => city.name === cityName); + if (cityIndex === -1) { + throw new Error(`City "${cityName}" not found`); + } + return Cities[cityIndex]; + } + + private parseUser(username: string): UserInfo { + const userIndex = usersMock.findIndex((user) => user.name === username); + if (userIndex === -1) { + throw new Error(`User "${username}" not found in mock data`); + } + return usersMock[userIndex]; + } + + 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-name.enum.ts b/src/shared/types/city-name.enum.ts new file mode 100644 index 0000000..6d5b7cb --- /dev/null +++ b/src/shared/types/city-name.enum.ts @@ -0,0 +1,8 @@ +export enum CityName { + Paris = 'Paris', + Cologne = 'Cologne', + Brussels = 'Brussels', + Amsterdam = 'Amsterdam', + Hamburg = 'Hamburg', + Dusseldorf = 'Dusseldorf', +} diff --git a/src/shared/types/city.type.ts b/src/shared/types/city.type.ts new file mode 100644 index 0000000..9353b05 --- /dev/null +++ b/src/shared/types/city.type.ts @@ -0,0 +1,6 @@ +import { CityName, Location } from './index.js'; + +export type City = { + name: CityName; + location: Location; +}; diff --git a/src/shared/types/good-type.type.ts b/src/shared/types/good-type.type.ts new file mode 100644 index 0000000..97452b8 --- /dev/null +++ b/src/shared/types/good-type.type.ts @@ -0,0 +1,8 @@ +export type GoodType = + | 'Breakfast' + | 'Air conditioning' + | 'Laptop friendly workspace' + | 'Baby seat' + | 'Washer' + | 'Towels' + | 'Fridge'; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..1a64274 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,9 @@ +export { CityName } from './city-name.enum.js'; +export { City } from './city.type.js'; +export { GoodType } from './good-type.type.js'; +export { Location } from './location.type.js'; +export { OfferType } from './offer-type.enum.js'; +export { Offer } from './offer.type.js'; +export { Review } from './review.type.js'; +export { UserInfo } from './user-info.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..8fb2331 --- /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.enum.ts b/src/shared/types/offer-type.enum.ts new file mode 100644 index 0000000..d86fee4 --- /dev/null +++ b/src/shared/types/offer-type.enum.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..7f8a1b1 --- /dev/null +++ b/src/shared/types/offer.type.ts @@ -0,0 +1,21 @@ +import { City, GoodType, Location, OfferType, UserInfo } from './index.js'; + +export type Offer = { + id: string; + title: string; + type: OfferType; + price: number; + city: City; + location: Location; + isFavorite: boolean; + isPremium: boolean; + rating: number; + description: string; + bedrooms: number; + goods: GoodType[]; + host: UserInfo; + images: string[]; + previewImage: string; + maxAdults: number; + createdDate: Date; +}; diff --git a/src/shared/types/review.type.ts b/src/shared/types/review.type.ts new file mode 100644 index 0000000..99a0393 --- /dev/null +++ b/src/shared/types/review.type.ts @@ -0,0 +1,9 @@ +import { UserInfo } from './index.js'; + +export type Review = { + id: string; + date: Date; + user: UserInfo; + comment: string; + rating: number; +}; diff --git a/src/shared/types/user-info.type.ts b/src/shared/types/user-info.type.ts new file mode 100644 index 0000000..3556607 --- /dev/null +++ b/src/shared/types/user-info.type.ts @@ -0,0 +1,3 @@ +import { User } from './index.js'; + +export type UserInfo = Omit; diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts new file mode 100644 index 0000000..cbc05e3 --- /dev/null +++ b/src/shared/types/user.type.ts @@ -0,0 +1,8 @@ +export type User = { + id: string; + name: string; + email: string; + avatarUrl: string; + password: string; + isPro: boolean; +};