Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions services/paths-builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# `@byndyusoft-ui/paths-build`

## Установка

```bash
npm i @byndyusoft-ui/paths-build
```

## Основные понятия

**Route (маршрут, роут)** - конфигурация маршрута до экрана UI. Может включать в себя паф, правила перенаправления и др.

**Path (паф)** - путь, строка по которой формируется URL:

- статический `/about`, `/tasks`
- параметризированный `/tasks/:taskId`

Параметризированный `path` может содержать:

- опциональные параметры `/tasts/:taskId?`
- опциональные сегменты `/tasts/edit?`

##Доступные методы

- `generatePath(path, params?)` - метод получает паф и параметры для заполнения. Отдает готовый урл
- `createUrl(path, params?, options?)` - то же, что и метод `generatePath`. Можно задавать опции
- `createUrlsByPaths(paths, options?)` - метод получает объект с ключ-значениями пафов (ключ - название пафа, значение - паф) и отдает объект с ключ-значениями урлов (ключ - название урла, значение - метод формирования урла)

```ts
options => { baseUrl?: string; }
```

## Особенности использования

1. Параметры описываются через схему: `:paramName`
2. Тип параметра - `string`, `number` и `null`
3. Если параметры не заданы, они становятся `null`:

````ts
generatePath('/tasks/:taskId/', { taskId: '' }); // => /tasks/null/
generatePath('/tasks/:taskId/', { taskId: null }); // => `/tasks/null/```
````

4. Если опциональные параметры не заданы, они пропускаются
```ts
generatePath('/tasks/:taskId?'); // => '/tasks'
```
5. Опциональные сегменты остаются в url:

```ts
createUrl('/tasks/edit?'); // => /tasks/edit
```

6. Слеш в конце пафа не вырезается, если задан:

```ts
createUrl('/tasks/'); // => /tasks/
```

7. Константа `paths` для метода `createUrlsByPaths` обязательно должна быть создана как `as const`:

```ts
const paths = {
users: '/users',
userComment: '/users/:userId/comments/:commentId/'
} as const; // <= important
```

## Примеры использования

### generatePath

```ts
generatePath('/tasks'); // => '/tasks'
generatePath('/tasks/'); // => '/tasks/'
generatePath('/tasks/:taskId', { taskId: 1 }); // => '/tasks/1'
generatePath('/tasks/:taskId/comments/:commentId/', { taskId: 1, commentId: 5 }); // => '/tasks/1/comments/5/'
generatePath('/tasks/:taskId/comments/:commentId?/', { taskId: 1 }); // => '/tasks/1/comments/'
);
```

### createUrl

```ts
createUrl('/tasks'); // => '/tasks'
createUrl('/tasks/:taskId', { taskId: 1 }); // => '/tasks/1'
createUrl('/tasks/:taskId', { taskId: 1 }, { baseUrl: 'http://test.com' }); // => 'http://test.com/tasks/1'
```

### createUrlsByPaths

Пример 1:

```ts
const paths = {
users: '/users',
userComment: '/users/:userId/comments/:commentId'
} as const;

const urls = createUrlsByPaths(paths);

resultWithBaseUrl.users(); // => `/users`
resultWithBaseUrl.userComment({ userId: 1, commentId: 2 }); // => '/users/1/comments/2'
```

Пример 2:

```ts
const paths = {
users: '/users',
userComment: '/users/:userId/comments/:commentId'
} as const;

const urls = createUrlsByPaths(paths, { baseUrl: 'http://test.com' });

resultWithBaseUrl.users(); // => `http://test.com/users`
resultWithBaseUrl.userComment({ userId: 1, commentId: 2 }); // => 'http://test.com/users/1/comments/2'

resultWithBaseUrl.users({ baseUrl: 'http://another-site.com' }); // => 'http://another-site.com/users'
resultWithBaseUrl.userComment({ userId: 1, commentId: 2 }, { baseUrl: 'http://another-site.com' }); // => 'http://another-site.com/users/1/comments/2'
```
1 change: 1 addition & 0 deletions services/paths-builder/npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src
36 changes: 36 additions & 0 deletions services/paths-builder/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@byndyusoft-ui/paths-builder",
"version": "0.0.1",
"description": "Byndyusoft UI PathsBuilder Service",
"keywords": [
"byndyusoft",
"byndyusoft-ui",
"paths"
],
"author": "Byndyusoft Frontend Developer <frontend@byndyusoft.com>",
"homepage": "https://github.com/Byndyusoft/ui/tree/master/services/paths-builder#readme",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/Byndyusoft/ui.git"
},
"scripts": {
"build": "rollup --config",
"clean": "rimraf dist",
"lint": "eslint src --config ../../eslint.config.js",
"lint:check": "npm run eslint:check && npm run prettier:check",
"test": "jest --config ../../jest.config.js --roots services/paths-builder/src",
"eslint:check": "eslint src --config ../../eslint.config.js",
"eslint:fix": "eslint src --config ../../eslint.config.js --fix",
"prettier:check": "prettier --check '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'",
"prettier:fix": "prettier --write '**/*.{ts,tsx,css,scss,json}' '!**/dist/**'"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
},
"publishConfig": {
"access": "public"
}
}
11 changes: 11 additions & 0 deletions services/paths-builder/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typescript from '@rollup/plugin-typescript';
import baseConfig from '../../rollup.base.config';

export default {
...baseConfig,
input: ['src/index.ts'],
plugins: [
...baseConfig.plugins,
typescript({ tsconfig: './tsconfig.json', exclude: ['node_modules'] })
]
};
1 change: 1 addition & 0 deletions services/paths-builder/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createUrl, createUrlsByPaths, generatePath } from './pathsBuilder.utilities';
114 changes: 114 additions & 0 deletions services/paths-builder/src/pathsBuilder.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { createUrl, createUrlsByPaths, generatePath, isPathExtendsParams } from './pathsBuilder.utilities';

describe('services/paths-builder', () => {
describe('isPathExtendsParams', () => {
test('should return correct result', () => {
expect(isPathExtendsParams('/users')).toBeFalsy();
expect(isPathExtendsParams('/users/comments/')).toBeFalsy();
expect(isPathExtendsParams('/users/comments?/')).toBeFalsy();
expect(isPathExtendsParams('/users/comments?')).toBeFalsy();
expect(isPathExtendsParams('/users/comments/?queryParam1=1&queryParam2=2')).toBeFalsy();
expect(isPathExtendsParams('tasks/:taskId')).toBeTruthy();
expect(isPathExtendsParams('tasks/:taskId?')).toBeTruthy();
expect(isPathExtendsParams('/tasks/:taskId/edit?')).toBeTruthy();
expect(isPathExtendsParams('/tasks/:taskId/comments/:commentId/?queryParam1=1&queryParam2=2')).toBeTruthy();
});
});

describe('generatePath', () => {
test('should return correct result', () => {
// without params
expect(generatePath('/tasks/')).toBe('/tasks/');
expect(generatePath('/tasks')).toBe('/tasks');
expect(generatePath('tasks/')).toBe('tasks/');
expect(generatePath('tasks')).toBe('tasks');
expect(generatePath('/tasks/?queryParam1=1&queryParam2=2')).toBe('/tasks/?queryParam1=1&queryParam2=2');
expect(generatePath('tasks/?queryParam1=1&queryParam2=2')).toBe('tasks/?queryParam1=1&queryParam2=2');

// with params
expect(generatePath('/tasks/:taskId/', { taskId: 1 })).toBe('/tasks/1/');
expect(generatePath('tasks/:taskId/', { taskId: 1 })).toBe('tasks/1/');
expect(generatePath('/tasks/:taskId', { taskId: 1 })).toBe('/tasks/1');
expect(generatePath('tasks/:taskId', { taskId: 1 })).toBe('tasks/1');
expect(generatePath('/tasks/:taskId/comments/:commentId', { taskId: 1, commentId: 5 })).toBe(
'/tasks/1/comments/5'
);
expect(
generatePath('/tasks/:taskId/comments/:commentId/?queryParam1=1&queryParam2=2', {
taskId: 1,
commentId: 5
})
).toBe('/tasks/1/comments/5/?queryParam1=1&queryParam2=2');

// with empty required params
expect(generatePath('/tasks/:taskId/', { taskId: '' })).toBe('/tasks/null/');
expect(generatePath('/tasks/:taskId/', { taskId: null })).toBe('/tasks/null/');

// without required params
// @ts-ignore
expect(() => generatePath('/tasks/:taskId')).toThrow('Missing ":taskId" param');
// @ts-ignore
expect(() => generatePath('/tasks/:taskId/comments/:commentId')).toThrow('Missing ":taskId" param');

// with optional params
// @ts-ignore
expect(generatePath('/tasks/:taskId?')).toBe('/tasks');
// @ts-ignore
expect(generatePath('/tasks/:taskId?/')).toBe('/tasks/');
expect(generatePath('/tasks/:taskId?/', { taskId: '' })).toBe('/tasks/');
expect(generatePath('/tasks/:taskId?/', { taskId: null })).toBe('/tasks/');
// @ts-ignore
expect(generatePath('/tasks/:taskId?/edit/?queryParam1=1&queryParam2=2')).toBe(
'/tasks/edit/?queryParam1=1&queryParam2=2'
);

// with optional segments
expect(generatePath('/tasks/edit?/')).toBe('/tasks/edit/');
expect(generatePath('/tasks/:taskId/edit?', { taskId: 1 })).toBe('/tasks/1/edit');
expect(generatePath('/tasks/:taskId/edit?/?queryParam1=1&queryParam2=2', { taskId: 1 })).toBe(
'/tasks/1/edit/?queryParam1=1&queryParam2=2'
);
});
});

describe('createUrl', () => {
test('should return correct result', () => {
expect(createUrl('/users')).toBe('/users');
expect(createUrl('/users/:userId', { userId: 1 })).toBe('/users/1');
expect(createUrl('/users', { baseUrl: '/v1' })).toBe('/v1/users');
expect(createUrl('/users/:userId/', { userId: 1 }, { baseUrl: 'http://test.com' })).toBe(
'http://test.com/users/1/'
);
});
});

describe('createUrlsByPaths', () => {
test('should return correct result', () => {
const paths = {
users: '/users',
userComment: '/users/:userId/comments/:commentId/'
} as const;

const result = createUrlsByPaths(paths);

expect(result.users()).toBe('/users');
expect(result.userComment({ userId: 1, commentId: 2 })).toBe('/users/1/comments/2/');

const resultWithBaseUrl = createUrlsByPaths(paths, { baseUrl: 'http://test.com' });

// with baseUrl
expect(resultWithBaseUrl.users()).toBe('http://test.com/users');
expect(resultWithBaseUrl.userComment({ userId: 1, commentId: 2 })).toBe(
'http://test.com/users/1/comments/2/'
);

// with replaced baseUrl
expect(resultWithBaseUrl.users({ baseUrl: 'http://another-site.com' })).toBe(
'http://another-site.com/users'
);
expect(
resultWithBaseUrl.userComment({ userId: 1, commentId: 2 }, { baseUrl: 'http://another-site.com' })
).toBe('http://another-site.com/users/1/comments/2/');
});
});
});
28 changes: 28 additions & 0 deletions services/paths-builder/src/pathsBuilder.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
type ExtractParam<Path> = Path extends `:${infer Param}`
? Param extends `${infer Optional}?`
? Optional
: Param
: never;

// eslint-disable-next-line @typescript-eslint/naming-convention
export type ExtractParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}`
? ExtractParams<Segment> | ExtractParams<Rest>
: ExtractParam<Path>;

export type TParamValue = number | string | null;

export interface ICreateUrlOptions {
baseUrl?: string;
}

export type TCreateUrlsByPaths<Paths extends Record<string, string>> = {
[PathKey in keyof Paths]: ExtractParams<Paths[PathKey]> extends never
? (options?: ICreateUrlOptions) => string
: (
params: {
[key in ExtractParams<Paths[PathKey]>]: TParamValue;
},
options?: ICreateUrlOptions
) => string;
};
Loading