From b31e080b66feeb82e2d7186bb7a69483031c953a Mon Sep 17 00:00:00 2001 From: Aral Roca Gomez Date: Sun, 28 May 2023 17:37:48 +0200 Subject: [PATCH] Add config for empty checker (#4) * add config for emptyChecker * format --- CODE_OF_CONDUCT.md | 20 ++--- README.md | 128 +++++++++++++++++------------ babel.config.js | 6 +- package.json | 3 +- src/index.test.ts | 195 +++++++++++++++++++++++++++++---------------- src/index.ts | 21 ++++- 6 files changed, 234 insertions(+), 139 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 92837ce..3410840 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/README.md b/README.md index c1c3f0c..40125e0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ - _A **tiny** (300B) **JavaScript library** that allows you to set **default values** for **nested objects**_ [![npm version](https://badge.fury.io/js/default-composer.svg)](https://badge.fury.io/js/default-composer) @@ -14,14 +13,17 @@ _A **tiny** (300B) **JavaScript library** that allows you to set **default value [![Maintenance Status](https://badgen.net/badge/maintenance/active/green)](https://github.com/aralroca/default-composer#maintenance-status) [![Weekly downloads](https://badgen.net/npm/dw/default-composer?color=blue)](https://www.npmjs.com/package/default-composer) [![PRs Welcome][badge-prwelcome]][prwelcome] + [badge-prwelcome]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square + [prwelcome]: http://makeapullrequest.com "default-composer" is a JavaScript library that allows you to set default values for **nested objects**. The library replaces empty strings/arrays/objects, null, or undefined values in an existing object with the defined default values, which helps simplify programming logic and reduce the amount of code needed to set default values. ## Installation + You can install "default-composer" using npm: ```bh @@ -36,41 +38,42 @@ yarn add default-composer ## Usage -To use "default-composer", simply require the library and call the `defaultComposer()` function with the default values object and the original object that you want to set default values for. For example: +To use "default-composer", simply import the library and call the `defaultComposer()` function with the default values object and the original object that you want to set default values for. For example: ```js -import defaultComposer from 'default-composer'; +import { defaultComposer } from "default-composer"; const defaults = { - name: 'Aral 😊', - surname: '', + name: "Aral 😊", + surname: "", isDeveloper: true, isDesigner: false, age: 33, address: { - street: '123 Main St', - city: 'Anytown', - state: 'CA', + street: "123 Main St", + city: "Anytown", + state: "CA", }, - emails: ['contact@aralroca.com'], - hobbies: ['programming'], + emails: ["contact@aralroca.com"], + hobbies: ["programming"], }; const originalObject = { - name: 'Aral', + name: "Aral", emails: [], - phone: '555555555', + phone: "555555555", age: null, address: { - zip: '54321' + zip: "54321", }, - hobbies: ['parkour', 'computer science', 'books', 'nature'], + hobbies: ["parkour", "computer science", "books", "nature"], }; const result = defaultComposer(defaults, originalObject); console.log(result); ``` + This will output: ```js @@ -94,48 +97,46 @@ This will output: ## API +### `defaultComposer` + ```js -defaultComposer(defaults, object1[, object2, ...]) +defaultComposer(defaultsPriorityN, [..., defaultsPriority2, defaultsPriority1, objectWithData]) ``` This function takes one or more objects as arguments and returns a new object with default values applied. The first argument should be an object containing the default values to apply. Subsequent arguments should be the objects to apply the default values to. -```js -defaultComposer(priority3, priority2, priority1) -``` - If a property in a given object is either empty, null, or undefined, and the corresponding property in the defaults object is not empty, null, or undefined, the default value will be used. -### Example +**Example**: ```js -import defaultComposer from 'default-composer'; +import { defaultComposer } from "default-composer"; const defaultsPriority1 = { - name: 'Aral 😊', - hobbies: ['reading'] + name: "Aral 😊", + hobbies: ["reading"], }; const defaultsPriority2 = { - name: 'Aral 🤔', + name: "Aral 🤔", age: 33, address: { - street: '123 Main St', - city: 'Anytown', - state: 'CA', - zip: '12345' + street: "123 Main St", + city: "Anytown", + state: "CA", + zip: "12345", }, - hobbies: ['reading', 'hiking'] -} + hobbies: ["reading", "hiking"], +}; const object = { address: { - street: '', - city: 'Anothercity', - state: 'NY', - zip: '' + street: "", + city: "Anothercity", + state: "NY", + zip: "", }, - hobbies: ['running'] + hobbies: ["running"], }; const result = defaultComposer(defaultsPriority2, defaultsPriority1, object); @@ -159,6 +160,27 @@ This will output: } ``` +### `setConfig` + +`setConfig` is a function that allows you to set configuration options for `defaultComposer`. Currently, the only configuration option available is `emptyChecker`, which is a function that determines whether a value should be considered empty or not. By default, is detected as empty when is null, undefined, an empty string, an empty array, or an empty object. However, you can use `setConfig` to provide your own implementation of `emptyChecker` if you need to customize this behavior. + +Here is an example of how you can use `setConfig`: + +```ts +import { defaultComposer, setConfig } from "default-composer"; + +const isNullOrWhitespace = (key: string, value: unknown) => { + return value === null || (typeof value === "string" && value.trim() === ""); +}; + +setConfig({ emptyChecker: isNullOrWhitespace }); + +const defaults = { example: "replaced", anotherExample: "also replaced" }; +const originalObject = { example: " ", anotherExample: null }; +const result = defaultComposer(defaults, originalObject); +console.log(result); // { example: 'replaced', anotherExample: 'also replaced' } +``` + ## TypeScript In order to use in TypeScript you can pass a generic with the expected output, and all the expected input by default should be partials of this generic. @@ -167,36 +189,36 @@ Example: ```ts type Addres = { - street: string, - city: string, - state: string, - zip: string -} + street: string; + city: string; + state: string; + zip: string; +}; type User = { - name: string, - age: number, - address: Address, - hobbies: string[] -} + name: string; + age: number; + address: Address; + hobbies: string[]; +}; const defaults = { - name: 'Aral 😊', - hobbies: ['reading'] + name: "Aral 😊", + hobbies: ["reading"], }; const object = { age: 33, address: { - street: '', - city: 'Anothercity', - state: 'NY', - zip: '' + street: "", + city: "Anothercity", + state: "NY", + zip: "", }, - hobbies: [] + hobbies: [], }; -defaultComposer(defaults, object) +defaultComposer(defaults, object); ``` ## Contributing diff --git a/babel.config.js b/babel.config.js index d19d38b..dd242dc 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,6 @@ module.exports = { presets: [ - ['@babel/preset-env', { targets: { node: 'current' } }], - '@babel/preset-typescript', + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", ], -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index e4280d5..b619bcd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default-composer", - "version": "0.1.3", + "version": "0.2.0", "description": "A JavaScript library that allows you to set default values for nested objects", "main": "dist/index.js", "umd:main": "dist/index.umd.js", @@ -39,6 +39,7 @@ "test:watch": "jest ./tests --watch", "build": "microbundle", "dev": "microbundle watch", + "format": "npx prettier --write .", "prepublish": "yarn build" }, "devDependencies": { diff --git a/src/index.test.ts b/src/index.test.ts index 1401394..1f50b83 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,123 +1,127 @@ -import defaultComposer from "." +import { defaultComposer, setConfig } from "."; type Address = { - street?: string, - city?: string, - state?: string, - zip?: string -} + street?: string; + city?: string; + state?: string; + zip?: string; +}; type User = { - name: string, - surname?: string, - isDeveloper?: boolean, - isDesigner?: boolean | null, - age: number | null, - address: Address - hobbies: string[] - emails: string[] -} - -describe('defaultComposer', () => { - it('should defaultComposer defaults with originalObject', () => { + name: string; + surname?: string; + isDeveloper?: boolean; + isDesigner?: boolean | null; + age: number | null; + address: Address; + hobbies: string[]; + emails: string[]; +}; + +describe("defaultComposer", () => { + beforeEach(() => setConfig({})); + + it("should defaultComposer defaults with originalObject", () => { const defaults = { - name: 'Aral 😊', - surname: '', + name: "Aral 😊", + surname: "", isDeveloper: true, isDesigner: false, age: 33, address: { - street: '123 Main St', - city: 'Anytown', - state: 'CA', + street: "123 Main St", + city: "Anytown", + state: "CA", }, - emails: ['contact@aralroca.com'], - hobbies: ['programming'], + emails: ["contact@aralroca.com"], + hobbies: ["programming"], }; const originalObject = { - name: 'Aral', + name: "Aral", emails: [], isDesigner: null, - phone: '555555555', + phone: "555555555", age: null, address: { - zip: '54321' + zip: "54321", }, - hobbies: ['parkour', 'computer science', 'books', 'nature'], + hobbies: ["parkour", "computer science", "books", "nature"], }; const expected = { - name: 'Aral', - surname: '', + name: "Aral", + surname: "", isDeveloper: true, isDesigner: false, - emails: ['contact@aralroca.com'], - phone: '555555555', + emails: ["contact@aralroca.com"], + phone: "555555555", age: 33, address: { - street: '123 Main St', - city: 'Anytown', - state: 'CA', - zip: '54321' + street: "123 Main St", + city: "Anytown", + state: "CA", + zip: "54321", }, - hobbies: ['parkour', 'computer science', 'books', 'nature'], - } + hobbies: ["parkour", "computer science", "books", "nature"], + }; - expect(defaultComposer(defaults, originalObject)).toEqual(expected) - }) + expect(defaultComposer(defaults, originalObject)).toEqual(expected); + }); - it('should work with multiple objects', () => { + it("should work with multiple objects", () => { const defaultsPriority1 = { - name: 'Aral 😊', - hobbies: ['reading'] + name: "Aral 😊", + hobbies: ["reading"], }; const defaultsPriority2 = { - name: 'Aral 🤔', + name: "Aral 🤔", age: 33, address: { - street: '123 Main St', - city: 'Anytown', - state: 'CA', - zip: '12345' + street: "123 Main St", + city: "Anytown", + state: "CA", + zip: "12345", }, - hobbies: ['reading', 'hiking'] - } + hobbies: ["reading", "hiking"], + }; const object = { address: { - street: '', - city: 'Anothercity', - state: 'NY', - zip: '' + street: "", + city: "Anothercity", + state: "NY", + zip: "", }, - hobbies: ['running'] + hobbies: ["running"], }; const expected = { - name: 'Aral 😊', + name: "Aral 😊", age: 33, address: { - street: '123 Main St', - city: 'Anothercity', - state: 'NY', - zip: '12345' + street: "123 Main St", + city: "Anothercity", + state: "NY", + zip: "12345", }, - hobbies: ['running'] - } + hobbies: ["running"], + }; - expect(defaultComposer(defaultsPriority2, defaultsPriority1, object)).toEqual(expected) - }) + expect( + defaultComposer(defaultsPriority2, defaultsPriority1, object) + ).toEqual(expected); + }); - it('should work with functions inside the object', () => { + it("should work with functions inside the object", () => { const mockFn = jest.fn(); const defaults = { test: () => mockFn(), }; const object = { - test: null + test: null, }; const output = defaultComposer(defaults, object); @@ -125,5 +129,60 @@ describe('defaultComposer', () => { output.test(); expect(mockFn).toBeCalledTimes(1); - }) + }); + + it("should work with a custom emptyChecker", () => { + const defaults = { + original: { + shouldKeepOriginal1: "replaced", + shouldKeepOriginal2: "replaced", + }, + mixed: { + shouldTakeDefault1: "replaced", + shouldTakeDefault2: "replaced", + shouldKeepOriginal1: "replaced", + shouldKeepOriginal2: "replaced", + shouldKeepOriginal3: "replaced", + shouldKeepOriginal4: "replaced", + }, + }; + const object = { + original: { + shouldKeepOriginal1: "original", + shouldKeepOriginal2: true, + }, + mixed: { + shouldTakeDefault1: " ", + shouldTakeDefault2: null, + shouldKeepOriginal1: false, + shouldKeepOriginal2: undefined, + shouldKeepOriginal3: [], + shouldKeepOriginal4: {}, + }, + }; + const isNullOrWhitespace = (key: string, value: unknown) => { + return ( + value === null || (typeof value === "string" && value.trim() === "") + ); + }; + + const expected = { + original: { + shouldKeepOriginal1: "original", + shouldKeepOriginal2: true, + }, + mixed: { + shouldTakeDefault1: "replaced", + shouldTakeDefault2: "replaced", + shouldKeepOriginal1: false, + shouldKeepOriginal2: undefined, + shouldKeepOriginal3: [], + shouldKeepOriginal4: {}, + }, + }; + + setConfig({ emptyChecker: isNullOrWhitespace }); + + expect(defaultComposer(defaults, object)).toEqual(expected); + }); }); diff --git a/src/index.ts b/src/index.ts index 82544da..32430c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,35 @@ -export default function defaultComposer(...args: Partial[]): T { +type emptyCheckerType = (key: string, value: unknown) => boolean; + +type Config = { + emptyChecker?: emptyCheckerType; +}; + +let config: Config = {}; + +export function setConfig(newConfig: Config): void { + config = newConfig; +} + +export function defaultComposer(...args: Partial[]): T { return args.reduce(compose, args[0]) as T; } function compose(defaults: Partial, obj: Partial): Partial { const result: Partial = {}; const allKeys = new Set([defaults, obj].flatMap(Object.keys)); + const isEmptyFn = config.emptyChecker || isEmpty; for (let key of allKeys) { const defaultsValue = defaults[key]; const originalObjectValue = obj[key]; const hasDefault = key in defaults; - if (hasDefault && isEmpty(originalObjectValue)) { + if (hasDefault && isEmptyFn(key, originalObjectValue)) { result[key] = defaultsValue; continue; } - if (hasDefault && isObject(originalObjectValue)) { + if (isObject(defaultsValue) && isObject(originalObjectValue)) { result[key] = compose(defaultsValue, originalObjectValue); continue; } @@ -36,7 +49,7 @@ function isEmptyObjectOrArray(object: T): boolean { return Object.keys(object).length === 0; } -function isEmpty(value: any): boolean { +function isEmpty(key: string, value: unknown): boolean { return ( value === undefined || value === "" ||