diff --git a/docs/utils/index.md b/docs/utils/index.md index 9ad31c6..6230e10 100644 --- a/docs/utils/index.md +++ b/docs/utils/index.md @@ -1,9 +1,5 @@ # Funções utilitárias -Este módulo fornece funções para formatar diversos tipos de dados. - -## Formatadores - ### pluralize() Funções para aplicação de plurais em palavras ou lista de palavras. @@ -12,6 +8,12 @@ Funções para aplicação de plurais em palavras ou lista de palavras. ### commaline() -Funções para formatar listas de strings com vírgulas e conjunção "e". +Função para formatar listas de strings com vírgulas e conjunção "e". - [Documentação](./commaline.md) + +### sanitizeForm() + +Função para sanitizar dados de formulário e aplicar transformações antes de enviá-los ao backend. + +- [Documentação](./sanitizeForm.md) diff --git a/docs/utils/sanitizeForm.md b/docs/utils/sanitizeForm.md new file mode 100644 index 0000000..da8d759 --- /dev/null +++ b/docs/utils/sanitizeForm.md @@ -0,0 +1,196 @@ +# Sanitize Form + +Utilitário para sanitizar dados de formulário antes de enviá-los ao backend. + +## Instalação e Importação + +```typescript +import { sanitizeForm } from '@sysvale/foundry'; +``` + +## Função + +### `sanitizeForm()` + +Sanitiza dados de formulário + +- Extrai IDs de objetos do tipo `{ id: string | number, value: string }` +- Mantém valores primitivos inalterados +- Processa recursivamente campos aninhados +- Aplica transformações em campos específicos por meio de `sanitizers` + +#### Sintaxe + +```typescript +sanitizeForm( + values: Record, + sanitizableFields?: SanitizableField[] +): Record +``` + +#### Parâmetros + +- **`values`** (`Record`): Dados do formulário a serem sanitizados +- **`sanitizableFields`** (`SanitizableField[]`, opcional): Array de campos com sanitizadores personalizados + +#### Tipos + +```typescript +type FormValue = + | string + | number + | boolean + | null + | undefined + | FormObject + | FormValue[]; + +type FormObject = { + id?: string | number; + [key: string]: FormValue; +}; + +interface SanitizableField { + field: string; + sanitizer: (value: any) => FormValue; +} +``` + +#### Retorno + +`Record` - Dados sanitizados conforme as regras aplicadas + +#### Regras de Sanitização + +1. **Valores primitivos**: Mantidos inalterados (string, number, boolean, null, undefined) +2. **Arrays de primitivos**: Preservados sem modificação +3. **Objetos com `id`**: Substituídos pelo valor da propriedade `id` +4. **Objetos sem `id`**: Processados recursivamente campo por campo +5. **Arrays com objetos**: Cada item é sanitizado recursivamente +6. **Sanitizadores personalizados**: Aplicados a campos específicos quando configurados +7. **Estruturas aninhadas**: Processamento recursivo em todos os níveis + +## Exemplos + +**Extração de IDs:** + +```typescript +// Objeto com ID é substituído pelo ID +sanitizeForm({ + category: { id: 'cat-123', name: 'Categoria A' }, +}); +// → { category: 'cat-123' } + +// Array de objetos com IDs +sanitizeForm({ + users: [ + { id: 1, name: 'João' }, + { id: 2, name: 'Maria' }, + { id: 3, name: 'Pedro' }, + ], +}); +// → { users: [1, 2, 3] } +``` + +
+ +**Processamento recursivo:** + +```typescript +// Objetos sem ID são processados recursivamente +sanitizeForm({ + config: { + theme: 'dark', + notifications: { + email: true, + push: false, + }, + }, +}); +// → { config: { theme: 'dark', notifications: { email: true, push: false } } } + +// Combinação de extração de ID e processamento recursivo +sanitizeForm({ + user: { id: 1, name: 'João' }, + preferences: { + theme: 'dark', + language: { id: 'pt-BR', name: 'Português' }, + }, +}); +// → { user: 1, preferences: { theme: 'dark', language: 'pt-BR' } } +``` + +
+ +**Estruturas complexas:** + +```typescript +// Arrays aninhados com objetos +sanitizeForm({ + categories: [ + { + id: 1, + items: [ + { id: 10, name: 'Item A' }, + { id: 11, name: 'Item B' }, + ], + }, + { + id: 2, + items: [{ id: 20, name: 'Item C' }], + }, + ], +}); +// → { categories: [1, 2] } + +// Objetos profundamente aninhados +sanitizeForm({ + company: { + id: 100, + departments: [ + { + id: 200, + employees: [ + { id: 300, name: 'João' }, + { id: 301, name: 'Maria' }, + ], + }, + ], + }, +}); +// → { company: 100 } +``` + +
+ +**Sanitizadores personalizados:** + +```typescript +// Aplicação de sanitizadores customizados +const sanitizers = [ + { + field: 'name', + sanitizer: value => value.toUpperCase(), + }, + { + field: 'phone', + sanitizer: value => value.replace(/\D/g, ''), + }, +]; + +sanitizeForm( + { + name: 'joão', + phone: '(11) 99999-9999', + age: 25, + }, + sanitizers +); +// → { name: 'JOÃO', phone: '11999999999', age: 25 } +``` + +## Limitações + +- A função assume que objetos com propriedade `id` devem ser convertidos para seu ID +- Arrays são sempre processados recursivamente, não há opção para preservar objetos em arrays +- A propriedade `id` sempre tem prioridade sobre processamento recursivo do objeto diff --git a/eslint.config.cjs b/eslint.config.cjs index f2d2f09..1de0016 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -43,6 +43,7 @@ module.exports = [ 'src/**/*.{ts,tsx}', 'lib/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}', + 'tests/**/*.{ts,tsx}', ], languageOptions: { ecmaVersion: 'latest', diff --git a/package.json b/package.json index 69b1554..b2c4193 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sysvale/foundry", - "version": "1.2.0", + "version": "1.3.0", "description": "A forge for composables, helpers, and front-end utilities.", "type": "module", "main": "./dist/foundry.cjs.js", diff --git a/src/index.ts b/src/index.ts index 1836e07..a0114f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './utils/pluralize'; export * from './utils/commaline'; +export * from './utils/sanitizeForm'; export { maskCpf, removeCpfMask } from './formatters/cpf'; export { maskPhone, removePhoneMask } from './formatters/phone'; diff --git a/src/utils/commaline.ts b/src/utils/commaline.ts index 6361b22..6e30a66 100644 --- a/src/utils/commaline.ts +++ b/src/utils/commaline.ts @@ -1,6 +1,14 @@ export function commaline(str: string, index: number, length: number): string; export function commaline(arr: string[]): string; +/** + * Função para formatar listas de strings com vírgulas e conjunção "e". + * + * @param strOrArray - String ou array de strings + * @param index - Índice (obrigatório se strOrArray for string) + * @param length - Tamanho total (obrigatório se strOrArray for string) + * @returns String formatada + */ export function commaline( strOrArray: string | string[], index?: number, diff --git a/src/utils/pluralize.ts b/src/utils/pluralize.ts index 93faf97..eabd3c4 100644 --- a/src/utils/pluralize.ts +++ b/src/utils/pluralize.ts @@ -1,3 +1,6 @@ +/** + * Mapeamento de palavras com plurais irregulares em português. + */ const irregulars: Record = { pão: 'pães', mão: 'mãos', @@ -11,6 +14,22 @@ const irregulars: Record = { nível: 'níveis', }; +/** + * Pluraliza uma palavra em português seguindo as regras gramaticais. + * + * @param count - Quantidade (0-1 = singular, >=2 = plural). Se string, trata como palavra. Null/undefined = 2. + * @param word - Palavra a ser pluralizada + * @param customPlural - Plural personalizado (opcional) + * @param customIrregulars - Plurais irregulares personalizados (opcional) + * @returns Palavra pluralizada conforme a quantidade + * + * @example + * pluralize(1, 'carro'); // → 'carro' + * pluralize(2, 'carro'); // → 'carros' + * pluralize(2, 'pão'); // → 'pães' (irregular) + * pluralize(2, 'livro', 'livrinhos'); // → 'livrinhos' + * pluralize('casa'); // → 'casas' + */ export function pluralize( count: number | string | null = null, word: string, @@ -69,6 +88,16 @@ export function pluralize( return word; } +/** + * Retorna a palavra pluralizada precedida pela quantidade. + * + * @param args - Mesmos parâmetros da função `pluralize` + * @returns Quantidade + palavra pluralizada + * + * @example + * pluralizeWithCount(1, 'carro'); // → '1 carro' + * pluralizeWithCount(3, 'avião'); // → '3 aviões' + */ export function pluralizeWithCount( ...args: Parameters ): string { @@ -76,6 +105,20 @@ export function pluralizeWithCount( return `${count} ${pluralize(...args)}`; } +/** + * Pluraliza múltiplas palavras simultaneamente. + * + * @param count - Quantidade para determinar pluralização + * @param words - Array de palavras + * @param customPlural - Array de plurais personalizados ou string única (opcional) + * @param customIrregulars - Plurais irregulares personalizados (opcional) + * @returns Palavras pluralizadas separadas por espaço + * + * @example + * pluralizeWords(1, ['avião', 'antigo']); // → 'avião antigo' + * pluralizeWords(2, ['avião', 'antigo']); // → 'aviões antigos' + * pluralizeWords(2, ['o', 'avião'], ['os', 'aviõezinhos']); // → 'os aviõezinhos' + */ export function pluralizeWords( count: number | string | null, words: string[], diff --git a/src/utils/sanitizeForm.ts b/src/utils/sanitizeForm.ts new file mode 100644 index 0000000..5e3117e --- /dev/null +++ b/src/utils/sanitizeForm.ts @@ -0,0 +1,66 @@ +type FormValue = + | string + | number + | boolean + | null + | undefined + | FormObject + | FormValue[]; + +type FormObject = { + id?: string | number; + [key: string]: FormValue; +}; + +type SanitizerFunction = (value: T) => R; + +export interface SanitizableField { + field: string; + sanitizer: SanitizerFunction; +} + +/** + * Sanitiza dados de formulário removendo campos desnecessários e aplicando transformações. + * + * @param { Object } values + * @param { Object[{ field: 'name', sanitizer: () => {} }] } sanitizableFields + * @returns { Object } + */ +export function sanitizeForm( + values: Record, + sanitizableFields?: SanitizableField[] +): Record { + const sanitizers = new Map( + (sanitizableFields ?? []).map(({ field, sanitizer }) => [field, sanitizer]) + ); + + const sanitizeValue = (value: FormValue, key?: string): FormValue => { + if (value == null) return value; + + if (Array.isArray(value)) { + return value.map(item => sanitizeValue(item)); + } + + if (typeof value === 'object' && value !== null) { + const obj = value as FormObject; + if (obj.id !== undefined) return obj.id; + + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, sanitizeValue(v, k)]) + ); + } + + if (key && sanitizers.has(key)) { + return sanitizers.get(key)!(value); + } + + return value; + }; + + return Object.fromEntries( + Object.entries(values).map(([key, value]) => [ + key, + sanitizeValue(value, key), + ]) + ); +} diff --git a/tests/sanitizeForm.test.ts b/tests/sanitizeForm.test.ts new file mode 100644 index 0000000..f990ae3 --- /dev/null +++ b/tests/sanitizeForm.test.ts @@ -0,0 +1,264 @@ +import { describe, test, expect } from 'vitest'; +import { sanitizeForm } from '../src/utils/sanitizeForm'; + +describe('sanitizeForm() - casos básicos', () => { + test('retorna objeto vazio quando recebe objeto vazio', () => { + expect(sanitizeForm({})).toEqual({}); + }); + + test('mantém valores primitivos inalterados', () => { + const data = { + name: 'João', + age: 25, + active: true, + description: null, + optional: undefined, + }; + + expect(sanitizeForm(data)).toEqual(data); + }); + + test('mantém arrays de valores primitivos', () => { + const data = { + tags: ['javascript', 'typescript'], + numbers: [1, 2, 3], + flags: [true, false], + }; + + expect(sanitizeForm(data)).toEqual(data); + }); +}); + +describe('sanitizeForm() - extração de IDs', () => { + test('extrai ID de objeto simples', () => { + const data = { + user: { id: 123, name: 'João', email: 'joao@email.com' }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + user: 123, + }); + }); + + test('extrai IDs de objetos em arrays', () => { + const data = { + users: [ + { id: 1, name: 'João' }, + { id: 2, name: 'Maria' }, + { id: 3, name: 'Pedro' }, + ], + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + users: [1, 2, 3], + }); + }); + + test('processa objetos sem ID recursivamente', () => { + const data = { + config: { + theme: 'dark', + notifications: { + email: true, + push: false, + }, + }, + }; + + expect(sanitizeForm(data)).toEqual(data); + }); + + test('combina extração de ID com processamento recursivo', () => { + const data = { + user: { id: 1, name: 'João' }, + preferences: { + theme: 'dark', + language: { id: 'pt-BR', name: 'Português' }, + }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + user: 1, + preferences: { + theme: 'dark', + language: 'pt-BR', + }, + }); + }); +}); + +describe('sanitizeForm() - estruturas aninhadas complexas', () => { + test('processa arrays aninhados com objetos', () => { + const data = { + categories: [ + { + id: 1, + items: [ + { id: 10, name: 'Item A' }, + { id: 11, name: 'Item B' }, + ], + }, + { + id: 2, + items: [{ id: 20, name: 'Item C' }], + }, + ], + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + categories: [1, 2], + }); + }); + + test('processa objetos profundamente aninhados', () => { + const data = { + company: { + id: 100, + departments: [ + { + id: 200, + employees: [ + { id: 300, name: 'João' }, + { id: 301, name: 'Maria' }, + ], + }, + ], + }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + company: 100, + }); + }); +}); + +describe('sanitizeForm() - sanitizadores customizados', () => { + test('aplica sanitizador customizado em campo específico', () => { + const data = { + name: 'joão', + phone: '(11) 99999-9999', + }; + + const sanitizers = [ + { + field: 'name', + sanitizer: (value: string) => value.toUpperCase(), + }, + { + field: 'phone', + sanitizer: (value: string) => value.replace(/\D/g, ''), + }, + ]; + + const result = sanitizeForm(data, sanitizers); + expect(result).toEqual({ + name: 'JOÃO', + phone: '11999999999', + }); + }); + + test('aplica sanitizador apenas no campo correto', () => { + const data = { + title: 'produto', + description: 'descrição do produto', + }; + + const sanitizers = [ + { + field: 'title', + sanitizer: (value: string) => value.toUpperCase(), + }, + ]; + + const result = sanitizeForm(data, sanitizers); + expect(result).toEqual({ + title: 'PRODUTO', + description: 'descrição do produto', + }); + }); + + test('combina sanitizador com extração de ID', () => { + const data = { + name: 'joão', + category: { id: 1, name: 'Categoria A' }, + }; + + const sanitizers = [ + { + field: 'name', + sanitizer: (value: string) => value.toUpperCase(), + }, + ]; + + const result = sanitizeForm(data, sanitizers); + expect(result).toEqual({ + name: 'JOÃO', + category: 1, + }); + }); +}); + +describe('sanitizeForm() - casos edge', () => { + test('preserva valores null e undefined', () => { + const data = { + nullable: null, + optional: undefined, + user: { id: 1, name: null }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + nullable: null, + optional: undefined, + user: 1, + }); + }); + + test('lida com arrays vazios', () => { + const data = { + emptyArray: [], + items: [{ id: 1, name: 'Item' }], + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + emptyArray: [], + items: [1], + }); + }); + + test('funciona sem sanitizadores customizados', () => { + const data = { + name: 'João', + user: { id: 1, name: 'Admin' }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + name: 'João', + user: 1, + }); + }); + + test('lida com array vazio de sanitizadores', () => { + const data = { name: 'João' }; + const result = sanitizeForm(data, []); + expect(result).toEqual(data); + }); + + test('processa ID como string', () => { + const data = { + user: { id: 'user-123', name: 'João' }, + }; + + const result = sanitizeForm(data); + expect(result).toEqual({ + user: 'user-123', + }); + }); +});