From 370ba728cee584af022723e586818d92d8e208bb Mon Sep 17 00:00:00 2001 From: Ihor Pyliak Date: Thu, 14 May 2026 18:38:03 +0300 Subject: [PATCH] solution --- .github/workflows/test.yml-template | 23 ++++++++++++ package-lock.json | 8 ++--- package.json | 2 +- src/config.js | 6 ++++ src/convertRequest/constants.js | 12 +++++++ src/convertRequest/messages.js | 19 ++++++++++ src/convertRequest/validateConvertRequest.js | 38 ++++++++++++++++++++ src/createServer.js | 20 +++++++++-- src/http/handleCaseTransformRequest.js | 36 +++++++++++++++++++ src/http/parseConvertUrl.js | 21 +++++++++++ src/main.js | 3 +- 11 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/test.yml-template create mode 100644 src/config.js create mode 100644 src/convertRequest/constants.js create mode 100644 src/convertRequest/messages.js create mode 100644 src/convertRequest/validateConvertRequest.js create mode 100644 src/http/handleCaseTransformRequest.js create mode 100644 src/http/parseConvertUrl.js diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 000000000..bb13dfc45 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/package-lock.json b/package-lock.json index fa582c7e6..5cb08f3fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -1467,9 +1467,9 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, "dependencies": { "@octokit/rest": "^17.11.2", diff --git a/package.json b/package.json index a6c885a1d..b70296ae9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", diff --git a/src/config.js b/src/config.js new file mode 100644 index 000000000..9e27b1b16 --- /dev/null +++ b/src/config.js @@ -0,0 +1,6 @@ +/** Default port; Host fallback in parseConvertUrl if header missing. */ +const DEFAULT_PORT = 5700; + +module.exports = { + DEFAULT_PORT, +}; diff --git a/src/convertRequest/constants.js b/src/convertRequest/constants.js new file mode 100644 index 000000000..948a3e96e --- /dev/null +++ b/src/convertRequest/constants.js @@ -0,0 +1,12 @@ +/** Supported target case names for the HTTP API and `wordsToCase`. */ +const SUPPORTED_CASES = Object.freeze([ + 'SNAKE', + 'KEBAB', + 'CAMEL', + 'PASCAL', + 'UPPER', +]); + +module.exports = { + SUPPORTED_CASES, +}; diff --git a/src/convertRequest/messages.js b/src/convertRequest/messages.js new file mode 100644 index 000000000..8926c1326 --- /dev/null +++ b/src/convertRequest/messages.js @@ -0,0 +1,19 @@ +const { SUPPORTED_CASES } = require('./constants'); + +const textIsMissed = + 'Text to convert is required. ' + + 'Correct request is: "/?toCase=".'; + +const toCaseIsMissed = + '"toCase" query param is required. Correct request is:' + + ' "/?toCase=".'; + +const toCaseIsNotSupported = + 'This case is not supported. Available cases: ' + + `${SUPPORTED_CASES.join(', ')}.`; + +module.exports = { + textIsMissed, + toCaseIsMissed, + toCaseIsNotSupported, +}; diff --git a/src/convertRequest/validateConvertRequest.js b/src/convertRequest/validateConvertRequest.js new file mode 100644 index 000000000..3a9e05a7c --- /dev/null +++ b/src/convertRequest/validateConvertRequest.js @@ -0,0 +1,38 @@ +const { SUPPORTED_CASES } = require('./constants'); +const { + textIsMissed, + toCaseIsMissed, + toCaseIsNotSupported, +} = require('./messages'); + +/** + * Validates case-transform API input (plain data, no Node `http`). + * + * @param {{ text: string, toCase: string | undefined }} input + */ +function validateConvertRequest({ text, toCase }) { + const result = { statusCode: 200, statusMessage: 'Ok', errors: [] }; + + if (!text) { + result.errors.push({ message: textIsMissed }); + } + + if (!toCase) { + result.errors.push({ message: toCaseIsMissed }); + } + + if (toCase && !SUPPORTED_CASES.includes(toCase)) { + result.errors.push({ message: toCaseIsNotSupported }); + } + + if (result.errors.length) { + result.statusCode = 400; + result.statusMessage = 'Bad request'; + } + + return result; +} + +module.exports = { + validateConvertRequest, +}; diff --git a/src/createServer.js b/src/createServer.js index 89724c920..dfc40adab 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,3 +1,17 @@ -// Write code here -// Also, you can create additional files in the src folder -// and import (require) them here +const http = require('http'); +const { + handleCaseTransformRequest, +} = require('./http/handleCaseTransformRequest'); + +function createServer() { + const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + handleCaseTransformRequest(req, res); + }); + + return server; +} + +module.exports = { + createServer, +}; diff --git a/src/http/handleCaseTransformRequest.js b/src/http/handleCaseTransformRequest.js new file mode 100644 index 000000000..4375baee8 --- /dev/null +++ b/src/http/handleCaseTransformRequest.js @@ -0,0 +1,36 @@ +const { convertToCase } = require('../convertToCase'); +const { + validateConvertRequest, +} = require('../convertRequest/validateConvertRequest'); +const { parseConvertUrl } = require('./parseConvertUrl'); + +/** + * Parse URL, validate, respond with JSON (success body or `{ errors }`). + * + * @param {import('http').IncomingMessage} req + * @param {import('http').ServerResponse} res + */ +function handleCaseTransformRequest(req, res) { + const { text, toCase } = parseConvertUrl(req); + const data = validateConvertRequest({ text, toCase }); + + res.statusCode = data.statusCode; + res.statusMessage = data.statusMessage; + + if (data.statusCode === 400) { + res.end(JSON.stringify({ errors: data.errors })); + + return; + } + + const result = convertToCase(text, toCase); + + result.targetCase = toCase; + result.originalText = text; + + res.end(JSON.stringify(result)); +} + +module.exports = { + handleCaseTransformRequest, +}; diff --git a/src/http/parseConvertUrl.js b/src/http/parseConvertUrl.js new file mode 100644 index 000000000..010bce7af --- /dev/null +++ b/src/http/parseConvertUrl.js @@ -0,0 +1,21 @@ +const { DEFAULT_PORT } = require('../config'); + +/** + * Reads `/?toCase=NAME` from `req.url` (no validation). + * + * @param {import('http').IncomingMessage} req + * @returns {{ text: string, toCase: string | undefined }} + */ +function parseConvertUrl(req) { + const host = req.headers.host || `localhost:${DEFAULT_PORT}`; + const base = `http://${host}`; + const url = new URL(req.url, base); + const text = url.pathname.slice(1); + const params = Object.fromEntries(url.searchParams); + + return { text, toCase: params.toCase }; +} + +module.exports = { + parseConvertUrl, +}; diff --git a/src/main.js b/src/main.js index b8d22f596..de3219d50 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ const { createServer } = require('./createServer'); +const { DEFAULT_PORT } = require('./config'); -createServer().listen(5700, () => { +createServer().listen(DEFAULT_PORT, () => { // eslint-disable-next-line no-console console.log('Server started! 🚀'); });