├── .gitignore ├── packages ├── zion │ ├── .gitignore │ ├── fantasyland-logo.png │ ├── src │ │ ├── index.js │ │ ├── data │ │ │ ├── index.js │ │ │ ├── keyValuePair.js │ │ │ ├── objectTree.js │ │ │ ├── step.js │ │ │ ├── reader.js │ │ │ ├── map.js │ │ │ ├── list.js │ │ │ └── maybe.js │ │ └── prelude │ │ │ ├── __tests__ │ │ │ └── do.tests.js │ │ │ └── index.js │ ├── babel.config.js │ ├── jest.config.js │ ├── .vscode │ │ ├── settings.json │ │ └── launch.json │ ├── package.json │ └── README.md ├── change-tracking │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── objectUtils │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── arrayUtils │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ └── dirtyInfo │ │ │ └── index.d.ts │ ├── package.json │ └── README.md ├── pure-validations │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── jest.config.js │ ├── .vscode │ │ ├── settings.json │ │ └── launch.json │ ├── src │ │ ├── higherOrderValidators │ │ │ ├── readFrom.js │ │ │ ├── parent.js │ │ │ ├── shape.js │ │ │ ├── logTo.js │ │ │ ├── filterFields.js │ │ │ ├── fromModel.js │ │ │ ├── fromParent.js │ │ │ ├── concatFailure.js │ │ │ ├── fromRoot.js │ │ │ ├── stopOnFirstFailure.js │ │ │ ├── errorMessage.js │ │ │ ├── _utils.js │ │ │ ├── items.js │ │ │ ├── index.js │ │ │ ├── when.js │ │ │ └── field.js │ │ ├── validationError.d.ts │ │ ├── __mocks__ │ │ │ └── i18next.js │ │ ├── index.js │ │ ├── index.d.ts │ │ ├── parser.d.ts │ │ ├── validation.d.ts │ │ ├── validationError.js │ │ ├── primitiveValidators │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── parser.js │ │ ├── validation.js │ │ ├── validator.d.ts │ │ └── validator.js │ └── package.json ├── react-state-lens │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ │ ├── hooks │ │ │ ├── index.js │ │ │ ├── index.d.ts │ │ │ └── useStateLens.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── stateLens.js │ │ └── lensProxy │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── README.md │ ├── jest.config.js │ ├── README.md │ └── package.json ├── rules-algebra │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── higherOrderRules │ │ │ ├── readFrom.js │ │ │ ├── parent.js │ │ │ ├── logTo.js │ │ │ ├── scope.js │ │ │ ├── fromModel.js │ │ │ ├── fromRoot.js │ │ │ ├── fromParent.js │ │ │ ├── root.js │ │ │ ├── when.js │ │ │ ├── ifThenElse.js │ │ │ ├── shape.js │ │ │ ├── until.js │ │ │ ├── items.js │ │ │ ├── chainRules.js │ │ │ ├── index.js │ │ │ └── field.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── parser.d.ts │ │ ├── _utils.js │ │ ├── parser.js │ │ ├── rule.d.ts │ │ ├── objectUtils │ │ │ └── index.js │ │ ├── primitiveRules │ │ │ ├── index.js │ │ │ └── index.d.ts │ │ ├── __tests__ │ │ │ └── workshop.tests.js │ │ ├── rule.js │ │ └── predicates │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── __tests__ │ │ │ └── predicates.tests.js │ └── package.json ├── change-tracking-react │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ │ ├── index.js │ │ ├── index.d.ts │ │ ├── hooks │ │ │ ├── index.js │ │ │ ├── useDirtyInfo.js │ │ │ ├── useChangeTrackingState.js │ │ │ ├── useChangeTrackingLens.js │ │ │ ├── __tests__ │ │ │ │ └── useDirtyInfo.tests.js │ │ │ └── index.d.ts │ │ └── __mocks__ │ │ │ └── @totalsoft │ │ │ └── change-tracking │ │ │ └── src.js │ ├── jest.config.js │ ├── package.json │ └── README.md ├── rules-algebra-react │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ │ ├── hooks │ │ │ ├── index.js │ │ │ ├── useRules.js │ │ │ ├── useRulesLens.js │ │ │ └── index.d.ts │ │ ├── index.js │ │ ├── index.d.ts │ │ └── __mocks__ │ │ │ └── @totalsoft │ │ │ ├── rules-algebra │ │ │ └── src.js │ │ │ └── change-tracking │ │ │ └── src.js │ ├── jest.config.js │ ├── package.json │ └── README.md └── pure-validations-react │ ├── .gitignore │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ ├── index.js │ ├── index.d.ts │ ├── hooks │ │ ├── index.js │ │ ├── useDirtyFieldValidation.js │ │ ├── index.d.ts │ │ └── useValidation.js │ ├── __mocks__ │ │ ├── react-i18next │ │ │ └── hooks.js │ │ └── @totalsoft │ │ │ └── pure-validations │ │ │ └── src.js │ └── validationProxy │ │ ├── index.d.ts │ │ ├── __tests__ │ │ └── validationProxy.tests.js │ │ └── index.js │ ├── .vscode │ ├── settings.json │ └── launch.json │ ├── jest.config.js │ ├── package.json │ └── README.md ├── docs └── Rules-algebra.pptx ├── jest.config.js ├── lerna.json ├── .vscode ├── settings.json └── launch.json ├── .github └── release-drafter.yml ├── jsconfig.json ├── tslint.json ├── .eslintrc.json ├── LICENSE ├── package.json ├── tsconfig.json ├── README.md ├── babel.config.js └── scripts └── copy-files.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .yarn -------------------------------------------------------------------------------- /packages/zion/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/change-tracking/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/pure-validations/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/react-state-lens/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/rules-algebra/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/change-tracking-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/rules-algebra-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/pure-validations-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /packages/change-tracking/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/pure-validations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/react-state-lens/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/rules-algebra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/change-tracking-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /packages/rules-algebra-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig" 3 | } -------------------------------------------------------------------------------- /docs/Rules-algebra.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osstotalsoft/jsbb/HEAD/docs/Rules-algebra.pptx -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "verbose": true, 3 | testPathIgnorePatterns: ["/build/"] 4 | } -------------------------------------------------------------------------------- /packages/zion/fantasyland-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osstotalsoft/jsbb/HEAD/packages/zion/fantasyland-logo.png -------------------------------------------------------------------------------- /packages/zion/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './prelude'; -------------------------------------------------------------------------------- /packages/zion/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "5.0.37" 8 | } 9 | -------------------------------------------------------------------------------- /packages/rules-algebra/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/change-tracking/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/pure-validations/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/react-state-lens/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/rules-algebra-react/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/change-tracking-react/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../babel.config.js"); 2 | 3 | module.exports = function(api){ 4 | return base(api); 5 | } -------------------------------------------------------------------------------- /packages/react-state-lens/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './useStateLens' 5 | -------------------------------------------------------------------------------- /packages/zion/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "zion", 6 | displayName: "zion" 7 | }; -------------------------------------------------------------------------------- /packages/pure-validations-react/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks' 5 | export * from './validationProxy' -------------------------------------------------------------------------------- /packages/react-state-lens/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks'; 5 | export * from './lensProxy'; 6 | -------------------------------------------------------------------------------- /packages/rules-algebra/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "rules-algebra", 6 | displayName: "rules-algebra" 7 | }; -------------------------------------------------------------------------------- /packages/change-tracking/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "change-tracking", 6 | displayName: "change-tracking" 7 | }; -------------------------------------------------------------------------------- /packages/change-tracking/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./arrayUtils"; 5 | export * from "./dirtyInfo"; 6 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './useRules' 5 | export * from './useRulesLens' -------------------------------------------------------------------------------- /packages/zion/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "javascript.format.enable": true, 4 | "prettier.printWidth": 150, 5 | "typescript.format.enable": false 6 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./hooks"; 5 | export * from "./validationProxy"; 6 | -------------------------------------------------------------------------------- /packages/pure-validations/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "pure-validations", 6 | displayName: "pure-validations" 7 | }; -------------------------------------------------------------------------------- /packages/react-state-lens/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "react-state-lens", 6 | displayName: "react-state-lens" 7 | }; -------------------------------------------------------------------------------- /packages/change-tracking-react/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks' 5 | export * from "@totalsoft/react-state-lens/lensProxy"; -------------------------------------------------------------------------------- /packages/pure-validations/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "javascript.format.enable": true, 4 | "prettier.printWidth": 150, 5 | "typescript.format.enable": false 6 | } -------------------------------------------------------------------------------- /packages/rules-algebra-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "rules-algebra-react", 6 | displayName: "rules-algebra-react" 7 | }; -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks' 5 | export * from "@totalsoft/react-state-lens/lensProxy"; -------------------------------------------------------------------------------- /packages/change-tracking-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "change-tracking-react", 6 | displayName: "change-tracking-react" 7 | }; -------------------------------------------------------------------------------- /packages/pure-validations-react/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "javascript.format.enable": true, 4 | "prettier.printWidth": 150, 5 | "typescript.format.enable": false 6 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './useDirtyFieldValidation' 5 | export * from './useValidation' -------------------------------------------------------------------------------- /packages/change-tracking-react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks'; 5 | export * from "@totalsoft/react-state-lens/lensProxy"; 6 | -------------------------------------------------------------------------------- /packages/change-tracking/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./dirtyInfo"; 5 | export * from "./arrayUtils"; 6 | export * from "./objectUtils"; -------------------------------------------------------------------------------- /packages/pure-validations-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require("../../jest.config.js"); 2 | 3 | module.exports = { 4 | ...base, 5 | name: "pure-validations-react", 6 | displayName: "pure-validations-react" 7 | }; -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks'; 5 | export * from "@totalsoft/react-state-lens/lensProxy"; 6 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './hooks' 5 | export * from './lensProxy' 6 | export { StateLens } from './stateLens' -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './useDirtyInfo' 5 | export * from './useChangeTrackingState' 6 | export * from './useChangeTrackingLens' 7 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/readFrom.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | 6 | export default function readFrom(func) { 7 | return Validator(func); 8 | } 9 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/readFrom.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { ensurePredicate } from "../predicates"; 5 | 6 | export default function readFrom(func) { 7 | return ensurePredicate(func); 8 | } 9 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/__mocks__/react-i18next/hooks.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const mock = {} 5 | 6 | mock.useTranslation = jest.fn(() => [undefined, ({ 7 | language: 'ro-RO' 8 | })]); 9 | 10 | module.exports = mock; -------------------------------------------------------------------------------- /packages/rules-algebra/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./higherOrderRules"; 5 | export * from "./primitiveRules"; 6 | export * from "./predicates"; 7 | export * from "./parser"; 8 | export { Rule, applyRule } from "./rule"; 9 | -------------------------------------------------------------------------------- /packages/zion/src/data/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from './keyValuePair'; 5 | export * from './list'; 6 | export * from './map'; 7 | export * from './maybe'; 8 | export * from './reader'; 9 | export * from './objectTree'; 10 | export * from './step'; -------------------------------------------------------------------------------- /packages/rules-algebra/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./higherOrderRules"; 5 | export * from "./primitiveRules"; 6 | export * from "./predicates"; 7 | export * from "./objectUtils"; 8 | export * from "./parser"; 9 | export { Rule, applyRule } from "./rule"; -------------------------------------------------------------------------------- /packages/pure-validations/src/validationError.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | interface ValidationError {} 5 | declare let ValidationError: { 6 | (errors: string | string[], fields?: { [key: string]: ValidationError }): ValidationError; 7 | }; 8 | 9 | export default ValidationError; 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "javascript.format.enable": true, 4 | "prettier.printWidth": 150, 5 | "typescript.format.enable": false, 6 | "licenser.license": "Custom", 7 | "licenser.author": "TotalSoft", 8 | "licenser.customHeader": "Copyright (c) @AUTHOR@.\nThis source code is licensed under the MIT license." 9 | } -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: v$NEXT_PATCH_VERSION 2 | tag-template: v$NEXT_PATCH_VERSION 3 | categories: 4 | - title: 🚀 Features 5 | label: feature 6 | - title: 🐛 Bug Fixes 7 | label: fix 8 | - title: 🛠️ Maintenance 9 | label: chore 10 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 11 | template: | 12 | ## Changes 13 | 14 | $CHANGES 15 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@totalsoft/zion": ["./packages/zion/src"], 6 | "@totalsoft/zion/*": ["./packages/zion/src/*"], 7 | "@totalsoft/react-state-lens": ["./packages/react-state-lens/src"], 8 | "@totalsoft/react-state-lens/*": ["./packages/react-state-lens/src/*"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/parent.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkRules } from "../_utils"; 6 | 7 | export default function parent(rule) { 8 | checkRules(rule); 9 | return rule |> contramap((_, ctx) => [ctx.parentModel, ctx.parentContext]); 10 | } 11 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/parent.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkValidators } from "./_utils"; 6 | 7 | export default function parent(validator) { 8 | checkValidators(validator); 9 | return validator |> contramap((_, ctx) => [ctx.parentModel, ctx.parentContext]); 10 | } 11 | -------------------------------------------------------------------------------- /packages/pure-validations/src/__mocks__/i18next.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const i18nextMock = jest.genMockFromModule("i18next"); 5 | i18nextMock.t = (text, props = {}) => { 6 | // eslint-disable-next-line no-unused-vars 7 | const { defaultValue, ...rest } = props; 8 | return text + (rest ? JSON.stringify(rest) : ""); 9 | }; 10 | 11 | export default i18nextMock; 12 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/shape.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { map } from "ramda" 5 | import field from "./field" 6 | import concatFailure from "./concatFailure" 7 | 8 | 9 | export default function shape(validatorObj) { 10 | return Object.entries(validatorObj) 11 | |> map(([k, v]) => field(k, v)) 12 | |> concatFailure 13 | } 14 | -------------------------------------------------------------------------------- /packages/pure-validations/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./higherOrderValidators"; 5 | export * from "./primitiveValidators"; 6 | export { Failure, Success, isValid, getErrors, getInner } from "./validation"; 7 | export { Validator, validate } from "./validator"; 8 | export { default as ValidationError } from "./validationError"; 9 | export * from "./parser" 10 | -------------------------------------------------------------------------------- /packages/pure-validations/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export * from "./higherOrderValidators"; 5 | export * from "./primitiveValidators"; 6 | export { Failure, Success, isValid, getErrors, getInner } from "./validation"; 7 | export { Validator, validate } from "./validator"; 8 | export { default as ValidationError } from "./validationError"; 9 | export * from "./parser"; 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["dtslint/dtslint.json"], 4 | "jsRules": {}, 5 | "rules": { 6 | "deprecation": true, 7 | "file-name-casing": [true, "camel-case"], 8 | "no-empty-interface": false, 9 | "no-unnecessary-generics": true, 10 | "no-redundant-jsdoc": false, 11 | "interface-over-type-literal": false, 12 | "semicolon": [true, "always", "ignore-bound-class-methods"] 13 | } 14 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/logTo.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkRules } from "../_utils"; 6 | import { curry } from "ramda"; 7 | 8 | const logTo = curry(function logTo(logger, rule) { 9 | checkRules(rule); 10 | return rule |> contramap((model, context) => [model, { ...context, log: true, logger }]); 11 | }); 12 | 13 | export default logTo; 14 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/scope.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | 6 | export default function scope(rule) { 7 | return rule |> 8 | contramap((model, ctx) => [model, { 9 | ...ctx, 10 | document: model, 11 | prevDocument: ctx.prevModel, 12 | scopePath: [...ctx.scopePath, ...ctx.fieldPath], 13 | fieldPath: [] 14 | }]) 15 | } 16 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/logTo.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkValidators } from "./_utils"; 6 | import { curry } from "ramda"; 7 | 8 | const logTo = curry(function logTo(logger, validator) { 9 | checkValidators(validator); 10 | return validator |> contramap((model, context) => [model, { ...context, log: true, logger }]); 11 | }); 12 | 13 | export default logTo; 14 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/filterFields.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkValidators } from "./_utils"; 6 | import { curry } from "ramda"; 7 | 8 | const filterFields = curry(function filterFields(fieldFilter, validator) { 9 | checkValidators(validator); 10 | return validator |> contramap((model, context) => [model, { ...context, fieldFilter }]); 11 | }); 12 | 13 | export default filterFields; 14 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/fromModel.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkValidators } from "./_utils"; 7 | 8 | export default function fromModel(validatorFactory) { 9 | return $do(function*() { 10 | const [model] = yield Validator.ask(); 11 | 12 | const v = validatorFactory(model); 13 | checkValidators(v); 14 | return yield v; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/fromParent.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkValidators } from "./_utils"; 7 | 8 | export default function fromParent(validatorFactory) { 9 | return $do(function*() { 10 | const [, { parentModel }] = yield Validator.ask(); 11 | 12 | const v = validatorFactory(parentModel); 13 | checkValidators(v); 14 | return yield v; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/concatFailure.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { concat } from "ramda"; 5 | import { variadicApply, checkValidators } from "./_utils"; 6 | import { Validator } from "../validator"; 7 | import { Success } from "../validation" 8 | 9 | const concatFailure = variadicApply(function concatFailure(...validators) { 10 | checkValidators(...validators); 11 | return validators.reduce(concat, Validator.of(Success)); 12 | }); 13 | 14 | export default concatFailure; 15 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/parser.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "./rule"; 5 | 6 | export type ParserOptions = { 7 | /** 8 | * Scope containing functions or values that are accessible from the rule. 9 | */ 10 | scope: {[key: string]: any} 11 | }; 12 | 13 | /** 14 | * Parses a rule from a string. 15 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#parse 16 | */ 17 | export function parse(rule: string, options?: ParserOptions): Rule; 18 | -------------------------------------------------------------------------------- /packages/change-tracking/src/objectUtils/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | /** 5 | * Sets the value to an inner property of an object corresponding to the given path. 6 | * The path can be a dot delimeted string or an array 7 | */ 8 | export function setInnerProp(obj: {}, path: string | string[], value: any): void; 9 | 10 | /** 11 | * Gets the value of the inner property of an object corresponding to the given search key path 12 | */ 13 | export function getInnerProp(obj: {}, searchKeyPath: string[]): any; 14 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/fromModel.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | 8 | export default function fromModel(ruleFactory) { 9 | return $do(function* () { 10 | const [model] = yield Rule.ask(); 11 | if (model === null || model === undefined) { 12 | return model; 13 | } 14 | const v = ruleFactory(model); 15 | checkRules(v); 16 | return yield v; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/pure-validations/src/parser.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "./validator"; 5 | 6 | export type ParserOptions = { 7 | /** 8 | * Scope containing functions or values that are accessible from the validator. 9 | */ 10 | scope: {[key: string]: any} 11 | }; 12 | 13 | /** 14 | * Parses a validator from a string. 15 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#parse 16 | */ 17 | export function parse(validator: string, options?: ParserOptions): Validator; 18 | -------------------------------------------------------------------------------- /packages/pure-validations/src/validation.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import ValidationError from "./validationError"; 5 | 6 | export interface Success {} 7 | export interface Failure {} 8 | 9 | export const Success: Success; 10 | export function Failure(validationError: ValidationError): Failure; 11 | export function getErrors(validation: Success | Failure): string[]; 12 | export function getInner(path: string[], validation: Success | Failure): Success | Failure; 13 | export function isValid(validation: Success | Failure): boolean; 14 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/fromRoot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | 8 | export default function fromRoot(ruleFactory) { 9 | return $do(function* () { 10 | let [model, ctx] = yield Rule.ask(); 11 | while (ctx?.parentModel) { 12 | model = ctx.parentModel; 13 | ctx = ctx.parentContext; 14 | } 15 | const v = ruleFactory(model); 16 | checkRules(v); 17 | return yield v; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/fromParent.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | 8 | export default function fromParent(ruleFactory) { 9 | return $do(function* () { 10 | const [model, { parentModel }] = yield Rule.ask(); 11 | if (parentModel === null || parentModel === undefined) { 12 | return model; 13 | } 14 | const v = ruleFactory(parentModel); 15 | checkRules(v); 16 | return yield v; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/zion/src/data/keyValuePair.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { tagged } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | 7 | const KeyValuePair = tagged("KeyValuePair", ["key", "value"]); 8 | 9 | KeyValuePair.getKey = function getKey(x) { 10 | return x.key; 11 | }; 12 | 13 | KeyValuePair.getValue = function getValue(x) { 14 | return x.value; 15 | }; 16 | 17 | /* Functor ObjectTree */ 18 | KeyValuePair.prototype[fl.map] = function(f) { 19 | return KeyValuePair(this.key, f(this.value)); 20 | }; 21 | 22 | export default KeyValuePair; 23 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/__mocks__/@totalsoft/rules-algebra/src.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const mock = jest.genMockFromModule("@totalsoft/rules-algebra"); 5 | 6 | mock.Rule = { 7 | of: value => ({ value }) 8 | }; 9 | mock.applyRule = jest.fn((rule, model, _prevModel) => ({ ...model, _ruleValue: rule.value })); 10 | 11 | mock.logTo = jest.fn(_ => rule => ({ ...rule })); 12 | 13 | mock.__clearMocks = function () { 14 | mock.applyRule.mockClear(); 15 | mock.logTo.mockClear(); 16 | }; 17 | 18 | // eslint-disable-next-line no-undef 19 | module.exports = mock; 20 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/root.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap } from "@totalsoft/zion"; 5 | import { checkRules } from "../_utils"; 6 | 7 | export default function root(rule) { 8 | checkRules(rule); 9 | return rule |> contramap((_, ctx) => _getRootModelAndContext(ctx)); 10 | } 11 | 12 | function _getRootModelAndContext(currentContext) { 13 | let [model, ctx] = [undefined, currentContext]; 14 | 15 | while (ctx?.parentModel) { 16 | model = ctx.parentModel; 17 | ctx = ctx.parentContext; 18 | } 19 | 20 | return [model, ctx] 21 | } -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/fromRoot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkValidators } from "./_utils"; 7 | 8 | export default function fromRoot(validatorFactory) { 9 | return $do(function*() { 10 | let [model, ctx] = yield Validator.ask(); 11 | 12 | while (ctx?.parentModel) { 13 | model = ctx.parentModel; 14 | ctx = ctx.parentCtx; 15 | } 16 | 17 | const v = validatorFactory(model); 18 | checkValidators(v); 19 | return yield v; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { DirtyInfo } from "@totalsoft/change-tracking"; 5 | import { LensProxy } from "../lensProxy"; 6 | 7 | /** 8 | * Provides a stateful profunctor lens over the model. 9 | * Receives the initial model 10 | * Returns a stateful profunctor over the model. 11 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens#usestatelens-hook 12 | */ 13 | export function useStateLens( 14 | initialModel: any, 15 | deps: any[] 16 | ): [ 17 | // Model lens 18 | LensProxy 19 | ]; 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:jest/recommended" 10 | ], 11 | "parser": "babel-eslint", 12 | "plugins": [ 13 | "babel", 14 | "jest", 15 | "react-hooks" 16 | ], 17 | "globals": { 18 | "Atomics": "readonly", 19 | "SharedArrayBuffer": "readonly" 20 | }, 21 | "parserOptions": { 22 | "ecmaVersion": 2018, 23 | "sourceType": "module" 24 | }, 25 | "rules": { 26 | "no-unused-vars": [2, {"args": "after-used", "argsIgnorePattern": "^_"}] 27 | } 28 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/when.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | import { unchanged } from "../primitiveRules" 8 | import { ensurePredicate } from "../predicates"; 9 | 10 | export const when = curry(function when(predicate, rule) { 11 | const predicateReader = ensurePredicate(predicate); 12 | checkRules(rule); 13 | 14 | return $do(function* () { 15 | const isTrue = yield predicateReader; 16 | return isTrue ? yield rule : yield unchanged; 17 | }); 18 | }); 19 | 20 | 21 | export default when 22 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/_utils.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "./rule"; 5 | 6 | export function variadicApply(variadicFn) { 7 | const res = function(...args) { 8 | if (args.length === 1 && Array.isArray(args[0])) { 9 | return variadicFn(...args[0]); 10 | } 11 | 12 | return variadicFn(...args); 13 | }; 14 | res.toString = function() { 15 | return variadicFn.toString(); 16 | }; 17 | return res; 18 | } 19 | 20 | export function checkRules(...rules) { 21 | rules.forEach(function(rule) { 22 | if (!Rule.is(rule)) { 23 | throw new Error(`Value '${rule ? rule.toString() : rule}' is not a rule!`); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/useDirtyInfo.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useState, useCallback } from 'react'; 5 | import { create, update } from '@totalsoft/change-tracking'; 6 | 7 | export function useDirtyInfo() { 8 | const [dirtyInfo, setDirtyInfo] = useState(create) 9 | 10 | return [ 11 | dirtyInfo, 12 | 13 | // Set dirty path 14 | useCallback((propertyPath) => { 15 | setDirtyInfo(prevDirtyInfo => update(propertyPath, true, prevDirtyInfo)) 16 | }, []), 17 | 18 | // Reset 19 | useCallback(() => { 20 | setDirtyInfo(create()) 21 | }, []) 22 | ] 23 | } 24 | 25 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/__mocks__/@totalsoft/change-tracking/src.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const mock = jest.genMockFromModule("@totalsoft/change-tracking"); 5 | 6 | mock.ensureArrayUIDsDeep = jest.fn(obj => obj); 7 | mock.setInnerProp = jest.fn((obj, _propPath, _value) => obj._innerProp === _value ? obj : ({ ...obj, _innerProp: _value })); 8 | mock.toUniqueIdMap = jest.fn(arr => arr.reduce((acc, value, index) => ({ ...acc, value, index }), {})); 9 | 10 | mock.__clearMocks = function () { 11 | mock.ensureArrayUIDsDeep.mockClear(); 12 | mock.toUniqueIdMap.mockClear(); 13 | mock.setInnerProp.mockClear(); 14 | }; 15 | 16 | // eslint-disable-next-line no-undef 17 | module.exports = mock; 18 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/ifThenElse.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | import { ensurePredicate } from "../predicates"; 8 | 9 | export const ifThenElse = curry(function ifThenElse(predicate, ruleWhenTrue, ruleWhenFalse) { 10 | const predicateReader = ensurePredicate(predicate); 11 | checkRules(ruleWhenTrue, ruleWhenFalse); 12 | 13 | return $do(function* () { 14 | const isTrue = yield predicateReader; 15 | return isTrue ? yield ruleWhenTrue : yield ruleWhenFalse; 16 | }); 17 | }); 18 | 19 | 20 | export default ifThenElse 21 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/shape.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { map, compose } from "ramda"; 6 | import { $do } from "@totalsoft/zion"; 7 | import { field, chainRules } from "./"; 8 | import scope from "./scope" 9 | 10 | function _shape(ruleObj) { 11 | return $do(function* () { 12 | const [model] = yield Rule.ask(); 13 | if (model === null || model === undefined) { 14 | return model; 15 | } 16 | 17 | return yield Object.entries(ruleObj) 18 | |> map(([k, v]) => field(k, v)) 19 | |> chainRules; 20 | }); 21 | } 22 | 23 | export default compose(scope, _shape) -------------------------------------------------------------------------------- /packages/change-tracking-react/src/__mocks__/@totalsoft/change-tracking/src.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const mock = jest.genMockFromModule("@totalsoft/change-tracking"); 5 | 6 | 7 | mock.ensureArrayUIDsDeep = jest.fn(obj => obj); 8 | mock.setInnerProp = jest.fn((obj, _propPath, _value) => obj._innerProp === _value ? obj : ({ ...obj, _innerProp: _value })); 9 | mock.toUniqueIdMap = jest.fn(arr => arr.reduce((acc, value, index) => ({ ...acc, value, index }), {})); 10 | 11 | mock.__clearMocks = function () { 12 | mock.ensureArrayUIDsDeep.mockClear(); 13 | mock.toUniqueIdMap.mockClear(); 14 | mock.setInnerProp.mockClear(); 15 | }; 16 | 17 | // eslint-disable-next-line no-undef 18 | module.exports = mock; 19 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/stopOnFirstFailure.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { $do } from "@totalsoft/zion"; 5 | import { concat } from "ramda"; 6 | import { variadicApply, checkValidators } from "./_utils"; 7 | import { isValid } from "../validation" 8 | 9 | function _stopOnFirstFailure(f1, f2) { 10 | return $do(function*() { 11 | const v1 = yield f1; 12 | const result = !isValid(v1) ? v1 : concat(v1, yield f2); 13 | return result; 14 | }); 15 | } 16 | 17 | const stopOnFirstFailure = variadicApply(function stopOnFirstFailure(...validators) { 18 | checkValidators(...validators); 19 | return validators.reduce(_stopOnFirstFailure); 20 | }); 21 | 22 | export default stopOnFirstFailure; 23 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/until.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | import { $do } from "@totalsoft/zion"; 6 | import { checkRules } from "../_utils"; 7 | import { unchanged } from "../primitiveRules" 8 | import { ensurePredicate } from "../predicates"; 9 | import { chainRules } from "./"; 10 | 11 | export const until = curry(function until(predicate, rule) { 12 | const predicateReader = ensurePredicate(predicate); 13 | checkRules(rule); 14 | 15 | return $do(function* () { 16 | const isTrue = yield predicateReader; 17 | return isTrue ? yield unchanged: yield chainRules(rule, until(predicate, rule)); 18 | }); 19 | }); 20 | 21 | 22 | export default until 23 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/errorMessage.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { checkValidators } from "./_utils"; 5 | import { curry, map } from "ramda" 6 | import ValidationError from "../validationError"; 7 | import i18next from "i18next"; 8 | 9 | function tryTranslate(error) { 10 | if (typeof error === 'string') { 11 | return i18next.t(error) || error 12 | } 13 | 14 | if (Array.isArray(error)) { 15 | return error.map(tryTranslate) 16 | } 17 | 18 | return error 19 | } 20 | 21 | const errorMessage = curry(function logTo(error, validator) { 22 | checkValidators(validator); 23 | return validator |> map(map(_ => ValidationError(tryTranslate(error)))) 24 | }); 25 | 26 | export default errorMessage; 27 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/items.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { map, addIndex } from "ramda"; 6 | import { $do } from "@totalsoft/zion"; 7 | import { field, chainRules } from "./"; 8 | import { checkRules } from "../_utils"; 9 | 10 | const mapIndexed = addIndex(map); 11 | 12 | export default function items(itemRule) { 13 | checkRules(itemRule); 14 | return $do(function* () { 15 | const [items] = yield Rule.ask(); 16 | if (items === null || items === undefined || items.length === 0) { 17 | return items; 18 | } 19 | return yield items 20 | |> mapIndexed((_, index) => field(index, itemRule)) 21 | |> chainRules; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/_utils.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | 6 | export function variadicApply(variadicFn) { 7 | const res = function(...args) { 8 | if (args.length === 1 && Array.isArray(args[0])) { 9 | return variadicFn(...args[0]); 10 | } 11 | 12 | return variadicFn(...args); 13 | }; 14 | res.toString = function() { 15 | return variadicFn.toString(); 16 | }; 17 | return res; 18 | } 19 | 20 | export function checkValidators(...validators) { 21 | validators.forEach(function(validator) { 22 | if (!Validator.is(validator)) { 23 | throw new Error(`Value '${validator ? validator.toString() : validator}' is not a validator!`); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/items.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { $do } from "@totalsoft/zion"; 5 | import { addIndex, map } from "ramda"; 6 | import { Validator } from "../validator"; 7 | import field from "./field"; 8 | import { checkValidators } from "./_utils"; 9 | import { Success } from "../validation"; 10 | import concatFailure from "./concatFailure" 11 | 12 | var mapWithIndex = addIndex(map) 13 | 14 | export default function items(itemValidator) { 15 | checkValidators(itemValidator); 16 | return $do(function* () { 17 | const [items] = yield Validator.ask(); 18 | if (items === null || items === undefined) { 19 | return Success; 20 | } 21 | 22 | return yield items |> mapWithIndex((_, i) => field(i, itemValidator)) |> concatFailure 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/validationProxy/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Success, Failure } from "@totalsoft/pure-validations"; 5 | 6 | // tslint:disable:no-unnecessary-generics 7 | export function eject(validationProxy: ValidationProxy): object; 8 | 9 | // tslint:disable:no-unnecessary-generics 10 | export function getErrors(validationProxy: ValidationProxy, separator: string): string; 11 | 12 | // tslint:disable:no-unnecessary-generics 13 | export function isValid(validationProxy: ValidationProxy): boolean; 14 | 15 | export type ValidationProxy = { 16 | [k in keyof TModel]-?: ValidationProxy; 17 | }; 18 | 19 | export function ValidationProxy(validation: Success | Failure): ValidationProxy; 20 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/hooks/useStateLens.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useMemo, useRef, useState } from 'react'; 5 | import { LensProxy, reuseCache } from '../lensProxy' 6 | import StateLens from '../stateLens'; 7 | 8 | export function useStateLens(initialModel, deps = []) { 9 | const [state, setState] = useState(initialModel) 10 | const prevProxy = useRef(null) 11 | 12 | const lensProxyMemoized = useMemo( 13 | () => { 14 | const proxy = StateLens(state, setState) |> LensProxy 15 | if (prevProxy.current) { 16 | reuseCache(prevProxy.current, proxy) 17 | } 18 | prevProxy.current = proxy; 19 | 20 | return proxy; 21 | }, 22 | [state, ...deps]) 23 | 24 | return lensProxyMemoized 25 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/chainRules.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { variadicApply, checkRules } from "../_utils"; 5 | import { chain } from "ramda"; 6 | import { contramap } from "@totalsoft/zion"; 7 | import { setInnerProp } from "../objectUtils"; 8 | 9 | function _concat(rule1, rule2) { 10 | return rule1 |> chain(newModel => rule2 |> contramap((model, ctx) => [newModel, _getContext(model, newModel, ctx)])) 11 | } 12 | 13 | function _getContext(model, newModel, ctx) { 14 | const { fieldPath, document } = ctx; 15 | return model === newModel ? ctx : { ...ctx, document: setInnerProp(document, fieldPath, newModel) } 16 | } 17 | 18 | const chainRules = variadicApply(function chainRules(...rules) { 19 | checkRules(...rules); 20 | 21 | return rules.reduce(_concat); 22 | }); 23 | 24 | export default chainRules; 25 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import when from "./when"; 5 | import until from "./until"; 6 | import fromModel from "./fromModel"; 7 | import field from "./field"; 8 | import shape from "./shape"; 9 | import items from "./items"; 10 | import logTo from "./logTo"; 11 | import chainRules from "./chainRules"; 12 | import scope from "./scope"; 13 | import ifThenElse from "./ifThenElse" 14 | import readFrom from "./readFrom" 15 | import parent from "./parent" 16 | import root from "./root" 17 | import fromParent from "./fromParent" 18 | import fromRoot from "./fromRoot" 19 | 20 | export { 21 | when, 22 | until, 23 | fromModel, 24 | field, 25 | shape, 26 | items, 27 | chainRules, 28 | logTo, 29 | scope, 30 | ifThenElse, 31 | readFrom, 32 | parent, 33 | root, 34 | fromParent, 35 | fromRoot 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import when from "./when"; 5 | import fromModel from "./fromModel"; 6 | import fromParent from "./fromParent"; 7 | import fromRoot from "./fromRoot"; 8 | import field from "./field"; 9 | import parent from "./parent"; 10 | import readFrom from "./readFrom"; 11 | import shape from "./shape"; 12 | import items from "./items"; 13 | import filterFields from "./filterFields"; 14 | import logTo from "./logTo"; 15 | import stopOnFirstFailure from "./stopOnFirstFailure"; 16 | import concatFailure from "./concatFailure"; 17 | import errorMessage from "./errorMessage"; 18 | 19 | export { 20 | stopOnFirstFailure, 21 | concatFailure, 22 | when, 23 | parent, 24 | fromModel, 25 | fromParent, 26 | fromRoot, 27 | field, 28 | shape, 29 | items, 30 | filterFields, 31 | readFrom, 32 | logTo, 33 | errorMessage 34 | }; 35 | -------------------------------------------------------------------------------- /packages/pure-validations/src/validationError.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import List from "@totalsoft/zion/data/list"; 5 | import Maybe from "@totalsoft/zion/data/maybe"; 6 | import Map from "@totalsoft/zion/data/map"; 7 | import ObjectTree from "@totalsoft/zion/data/objectTree"; 8 | import { curry } from "ramda"; 9 | 10 | const { Just, Nothing } = Maybe; 11 | 12 | function ValidationError(errors, fields = {}) { 13 | const maybeError = 14 | Array.isArray(errors) && errors.length > 0 ? Just(List.fromArray(errors)) : typeof errors === "string" ? Just(List.fromArray([errors])) : Nothing; 15 | 16 | const fieldsMap = Map.fromObj(fields); 17 | 18 | return ObjectTree(maybeError, fieldsMap); 19 | } 20 | 21 | ValidationError.getField = ObjectTree.getChildAt; 22 | 23 | ValidationError.moveToField = curry(function moveToField(field, error) { 24 | return ValidationError([], { [field]: error }); 25 | }); 26 | 27 | export default ValidationError; 28 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/when.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { $do } from "@totalsoft/zion"; 5 | import { curry } from "ramda"; 6 | import { checkValidators } from "./_utils"; 7 | import { Validator } from "../validator"; 8 | import { Success } from "../validation" 9 | 10 | 11 | const when = curry(function when(predicate, validator) { 12 | const predicateReader = ensurePredicate(predicate); 13 | checkValidators(validator); 14 | 15 | return $do(function* () { 16 | const isTrue = yield predicateReader; 17 | return isTrue ? yield validator : Success; 18 | }); 19 | }); 20 | 21 | function ensurePredicate(predicate) { 22 | if (typeof predicate === "boolean") { 23 | return Validator.of(predicate); 24 | } 25 | if (typeof predicate === "function") { 26 | return Validator(predicate); 27 | } 28 | 29 | checkValidators(predicate); 30 | 31 | return predicate; 32 | } 33 | 34 | export default when; 35 | -------------------------------------------------------------------------------- /packages/pure-validations/src/primitiveValidators/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../index"; 5 | 6 | export const atLeastOne: Validator; 7 | 8 | export const email: Validator; 9 | 10 | export function greaterThan(min: number): Validator; 11 | 12 | export function lessThan(max: number): Validator; 13 | 14 | export function between(min: number, max: number): Validator; 15 | 16 | export function matches(regex: RegExp): Validator; 17 | 18 | export function maxLength(max: number): Validator; 19 | 20 | export function minLength(min: number): Validator; 21 | 22 | export const required: Validator; 23 | 24 | export const integer: Validator; 25 | 26 | export const number: Validator; 27 | 28 | export const valid: Validator; 29 | 30 | export function unique(selector: string | string[], displayName: string): Validator; 31 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/parser.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import * as V from "."; 5 | import { intersection } from "ramda" 6 | 7 | function injectRulesAlgebraScope(scope) { 8 | const rulesAlgebraScope = { ...V, V } 9 | 10 | const conflicts = intersection(Object.keys(scope), Object.keys(rulesAlgebraScope)) 11 | if (conflicts.length > 0) { 12 | throw new Error(`The following keywords are reserved: ${conflicts.join(", ")}`) 13 | } 14 | 15 | return { ...scope, ...rulesAlgebraScope } 16 | } 17 | 18 | export function parse(rule, { scope = {} } = {}) { 19 | const sanitizedRuleString = rule.trim() 20 | const augmentedScope = injectRulesAlgebraScope(scope) 21 | const assignScope = `const {${Object.keys(augmentedScope).join(", ")}} = $scope` 22 | 23 | const fn = Function('$scope', `'use strict';\n${assignScope};\nreturn ${sanitizedRuleString}`) 24 | 25 | return fn(augmentedScope) 26 | } 27 | -------------------------------------------------------------------------------- /packages/pure-validations/src/parser.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import * as R from "."; 5 | import { intersection } from "ramda" 6 | 7 | function injectRulesAlgebraScope(scope) { 8 | const rulesAlgebraScope = { ...R, R } 9 | 10 | const conflicts = intersection(Object.keys(scope), Object.keys(rulesAlgebraScope)) 11 | if (conflicts.length > 0) { 12 | throw new Error(`The following keywords are reserved: ${conflicts.join(", ")}`) 13 | } 14 | 15 | return { ...scope, ...rulesAlgebraScope } 16 | } 17 | 18 | export function parse(ruleString, { scope = {} } = {}) { 19 | const sanitizedRuleString = ruleString.trim() 20 | const augmentedScope = injectRulesAlgebraScope(scope) 21 | const assignScope = `const {${Object.keys(augmentedScope).join(", ")}} = $scope` 22 | 23 | const fn = Function('$scope', `'use strict';\n${assignScope};\nreturn ${sanitizedRuleString}`) 24 | 25 | return fn(augmentedScope) 26 | } 27 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/__mocks__/@totalsoft/pure-validations/src.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | const mock = jest.genMockFromModule("@totalsoft/pure-validations"); 5 | 6 | mock.Success = { value: true, toString: () => "Success" }; 7 | mock.Failure = () => ({ value: false, toString: () => "Failure" }); 8 | 9 | mock.isValid = jest.fn(validation => validation.value); 10 | mock.getErrors = jest.fn(_ => []); 11 | 12 | mock.Validator = { 13 | of: validation => ({ validation }) 14 | }; 15 | mock.validate = jest.fn(validator => validator.validation); 16 | 17 | mock.logTo = jest.fn(_ => validator => ({ ...validator })); 18 | mock.filterFields = jest.fn(_ => validator => ({ ...validator })); 19 | 20 | mock.__clearMocks = function() { 21 | mock.validate.mockClear(); 22 | mock.logTo.mockClear(); 23 | mock.filterFields.mockClear(); 24 | mock.isValid.mockClear(); 25 | mock.getErrors.mockClear(); 26 | }; 27 | 28 | // eslint-disable-next-line no-undef 29 | module.exports = mock; 30 | -------------------------------------------------------------------------------- /packages/pure-validations/src/validation.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import Maybe from "@totalsoft/zion/data/maybe"; 5 | import List from "@totalsoft/zion/data/list"; 6 | import ObjectTree from "@totalsoft/zion/data/objectTree"; 7 | import ValidationError from "./validationError"; 8 | import { chain, map, identity, always, curry } from "ramda"; 9 | 10 | const Success = Maybe.Nothing; 11 | const Failure = Maybe.Just; 12 | 13 | function isValid(validation) { 14 | return Maybe.Nothing.is(validation); 15 | } 16 | 17 | function getErrors(validation) { 18 | const maybeArray = validation |> chain(ObjectTree.getValue) |> map(List.toArray); 19 | const result = maybeArray.cata({ 20 | Just: identity, 21 | Nothing: always([]) 22 | }); 23 | return result; 24 | } 25 | 26 | const getInner = curry(function getInner(path, validation) { 27 | return path.reduce((acc, key) => chain(ValidationError.getField(key), acc), validation); 28 | }); 29 | 30 | export { Success, Failure, isValid, getErrors, getInner }; 31 | -------------------------------------------------------------------------------- /packages/pure-validations/src/validator.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { F } from "ts-toolbelt"; 5 | import { Success, Failure } from "./validation"; 6 | 7 | export type ValidatorContext = { 8 | fieldPath: string[]; 9 | fieldFilter: (context: ValidatorContext) => boolean; 10 | log: boolean; 11 | logger: { log: (message: string) => void }; 12 | parentModel: any; 13 | parentContext: any; 14 | [key: string]: any; 15 | }; 16 | 17 | export interface Validator {} 18 | export let Validator: { 19 | (func: (model: TModel, context?: ValidatorContext) => Success | Failure): Validator; 20 | 21 | // tslint:disable:no-unnecessary-generics 22 | of(validation: Success | Failure): Validator; 23 | }; 24 | 25 | export function validate(validator: Validator, model: TModel, ctx?: ValidatorContext): Success | Failure; 26 | export function validate(validator: Validator): F.Curry<(model: TModel, ctx?: ValidatorContext) => Success | Failure>; 27 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/rule.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { F } from "ts-toolbelt"; 5 | 6 | export type RuleContext = { 7 | prevDocument: any, 8 | document: any, 9 | fieldPath: string[]; 10 | log: boolean; 11 | logger: { log: (message: string) => void }; 12 | parentModel: any; 13 | parentContext: any; 14 | [key: string]: any; 15 | }; 16 | 17 | export interface Rule {} 18 | export let Rule: { 19 | (func: (model: TModel, context?: RuleContext) => any): Rule; 20 | of(result: any): Rule; 21 | }; 22 | 23 | export function applyRule(rule: Rule, newModel: TModel, prevModel?: TModel, ctx?: RuleContext): any; 24 | export function applyRule(rule: Rule, newModel: TModel, prevModel?: TModel): (ctx?: RuleContext) => any; 25 | export function applyRule(rule: Rule, newModel: TModel): F.Curry<(prevModel?: TModel, ctx?: RuleContext) => any>; 26 | export function applyRule(rule: Rule): F.Curry<(model: TModel, prevModel?: TModel, ctx?: RuleContext) => any>; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) TotalSoft. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/pure-validations-react/src/validationProxy/__tests__/validationProxy.tests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { ValidationProxy, eject } from "../"; 5 | import { Success, Failure, ValidationError } from "@totalsoft/pure-validations"; 6 | 7 | 8 | jest.unmock("@totalsoft/pure-validations"); 9 | 10 | describe("validation proxy", () => { 11 | it("returns inner validation", () => { 12 | // Arrange 13 | const validation = Failure(ValidationError("", { value: ValidationError("Error") })) 14 | const proxy = ValidationProxy(validation) 15 | 16 | // Act 17 | const innerValidation = eject(proxy.value); 18 | 19 | // Assert 20 | expect(innerValidation).toStrictEqual(Failure(ValidationError("Error"))) 21 | }); 22 | it("returns success if inner validation not found", () => { 23 | // Arrange 24 | const validation = Success 25 | const proxy = ValidationProxy(validation) 26 | 27 | // Act 28 | const innerValidation = eject(proxy.value); 29 | 30 | // Assert 31 | expect(innerValidation).toBe(Success) 32 | }); 33 | }) -------------------------------------------------------------------------------- /packages/rules-algebra/src/objectUtils/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | 6 | export const setInnerProp = curry(function setInnerProp(obj, path, value) { 7 | function inner(obj, searchKeyPath) { 8 | const [prop, ...rest] = searchKeyPath; 9 | if (prop == undefined) { 10 | return value 11 | } 12 | const newValue = rest.length > 0 ? inner(obj[prop], rest) : value 13 | return _immutableAssign(obj, prop, newValue); 14 | } 15 | 16 | if (typeof path === "string") { 17 | path = path.split(".") 18 | } 19 | 20 | return inner(obj, path); 21 | }); 22 | 23 | export const getInnerProp = curry(function getInnerProp(obj, searchKeyPath = []) { 24 | const [prop, ...rest] = searchKeyPath; 25 | return prop !== undefined ? getInnerProp(obj[prop], rest) : obj; 26 | }); 27 | 28 | 29 | function _immutableAssign(obj, prop, value) { 30 | if (obj[prop] === value) { 31 | return obj; 32 | } 33 | 34 | return Array.isArray(obj) 35 | ? Object.assign([], obj, { [prop]: value }) 36 | : { ...obj, [prop]: value } 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest --watchAll" 6 | }, 7 | "devDependencies": { 8 | "@babel/cli": "^7.5.5", 9 | "@babel/core": "^7.5.5", 10 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 11 | "@babel/plugin-proposal-pipeline-operator": "^7.5.0", 12 | "@babel/plugin-transform-modules-commonjs": "^7.6.0", 13 | "@babel/plugin-transform-runtime": "^7.6.2", 14 | "@babel/preset-env": "^7.5.5", 15 | "@babel/preset-react": "^7.18.6", 16 | "babel-eslint": "^10.0.2", 17 | "babel-jest": "^24.9.0", 18 | "babel-plugin-module-resolver": "^3.0.0", 19 | "cross-env": "^6.0.3", 20 | "dtslint": "^0.9.3", 21 | "eslint": "^6.2.0", 22 | "eslint-plugin-babel": "^5.3.0", 23 | "eslint-plugin-jest": "^22.16.0", 24 | "eslint-plugin-react-hooks": "^2.0.1", 25 | "fs-extra": "^8.1.0", 26 | "glob": "^7.1.2", 27 | "i18next": "^19.1.0", 28 | "jest": "^26.6.3", 29 | "lerna": "^3.22.1", 30 | "prettier-eslint": "^9.0.0", 31 | "react": "^16.9.0", 32 | "react-i18next": "^11.3.1", 33 | "rimraf": "^3.0.0", 34 | "tern": "0.24.2", 35 | "ts-toolbelt": "4.9.15" 36 | }, 37 | "workspaces": [ 38 | "packages/*" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/change-tracking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/change-tracking", 3 | "version": "5.0.37", 4 | "description": "Change tracking library for models including objects and arrays", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/change-tracking" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "dependencies": { 28 | "uniqid": "^5.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/zion", 3 | "version": "5.0.22", 4 | "description": "Fantasy Land compliant algebraic data types", 5 | "main": "./index.js", 6 | "module": "./index.js", 7 | "scripts": { 8 | "prebuild": "rimraf build", 9 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 10 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 12 | "build:copy-files": "node ../../scripts/copy-files.js", 13 | "prepare": "yarn run build", 14 | "test": "jest --watchAll" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/osstotalsoft/jsbb.git", 19 | "directory": "packages/zion" 20 | }, 21 | "author": "TotalSoft", 22 | "license": "ISC", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/osstotalsoft/jsbb/issues" 28 | }, 29 | "homepage": "https://github.com/osstotalsoft/jsbb#readme", 30 | "dependencies": { 31 | "daggy": "^1.4.0", 32 | "fantasy-land": "^4.1.0", 33 | "immutagen": "^1.0.9", 34 | "ramda": "^0.26.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-state-lens/README.md: -------------------------------------------------------------------------------- 1 | # react-state-lens 2 | React extensions for stateful profunctor lenses. 3 | 4 | 5 | ## installation 6 | ```javascript 7 | npm install @totalsoft/react-state-lens 8 | ``` 9 | 10 | ## info 11 | The library provides one hook: 12 | - **useStateLens** - provides a stateful profunctor lens over a model 13 | 14 | 15 | ## useStateLens hook 16 | Provides a stateful profunctor lens over the model. 17 | * Receives the initial model 18 | * Returns a stateful profunctor over the model. 19 | 20 | Usage example: 21 | 22 | ```jsx 23 | import { useStateLens, get, set } from "@totalsoft/react-state-lens"; 24 | 25 | const onTextBoxChange = onPropertyChange => event => onPropertyChange(event.target.value) 26 | 27 | const SomeComponent = props => { 28 | const personLens = useStateLens({}); 29 | 30 | return ( 31 | <> 32 | get} 34 | onChange={personLens.firstName |> set |> onTextBoxChange} 35 | /> 36 | get} 38 | onChange={personLens.lastName |> set |> onTextBoxChange} 39 | /> 40 | 41 | 42 | ); 43 | }; 44 | ``` 45 | 46 | [Read more about lens operations](src/lensProxy/README.md) 47 | -------------------------------------------------------------------------------- /packages/zion/src/data/objectTree.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { tagged } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import Map from './map'; 7 | import { equals, map, concat, curry } from "ramda"; 8 | 9 | //data ObjectTree a b :: Maybe a * Map b (ObjectTree a) 10 | const ObjectTree = tagged("ObjectTree", ["value", "children"]); 11 | 12 | ObjectTree.Leaf = function(value) { 13 | return ObjectTree(value, Map({})); 14 | }; 15 | 16 | ObjectTree.getValue = curry(function getValue(tree) { 17 | return tree.value; 18 | }) 19 | 20 | ObjectTree.getChildAt = curry(function getChildAt(key, tree) { 21 | const result = tree.children |> Map.getValueAt(key); 22 | return result; 23 | }) 24 | 25 | /* Setoid a => Setoid (ObjectTree a) */ 26 | ObjectTree.prototype[fl.equals] = function(that) { 27 | return equals(this.value, that.value) && equals(this.children, that.children); 28 | }; 29 | 30 | /* Functor ObjectTree */ 31 | ObjectTree.prototype[fl.map] = function(f) { 32 | return ObjectTree(this.value |> map(f), this.children |> map(map(f))); 33 | }; 34 | 35 | /* Semigroup ObjectTree */ 36 | ObjectTree.prototype[fl.concat] = function(that) { 37 | return ObjectTree(concat(this.value, that.value), concat(this.children, that.children)); 38 | }; 39 | 40 | export default ObjectTree; 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6", "dom"], 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noEmit": true, 11 | "experimentalDecorators": true, 12 | "baseUrl": "./", 13 | "allowSyntheticDefaultImports": true, 14 | "noErrorTruncation": true, 15 | "allowJs": true, 16 | "paths": { 17 | "@totalsoft/change-tracking": ["./packages/change-tracking/src"], 18 | "@totalsoft/change-tracking-react": ["./packages/change-tracking-react/src"], 19 | "@totalsoft/pure-validations": ["./packages/pure-validations/src"], 20 | "@totalsoft/pure-validations-react": ["./packages/pure-validations-react/src"], 21 | "@totalsoft/rules-algebra": ["./packages/rules-algebra/src"], 22 | "@totalsoft/rules-algebra-react": ["./packages/rules-algebra-react/src"], 23 | "@totalsoft/react-state-lens": ["./packages/react-state-lens/src"], 24 | "@totalsoft/react-state-lens/lensProxy": ["./packages/react-state-lens/src/lensProxy"], 25 | "@totalsoft/zion": ["./packages/zion/src"], 26 | "@totalsoft/zion/*": ["./packages/zion/src/*"] 27 | } 28 | }, 29 | "exclude": ["**/build/"] 30 | } -------------------------------------------------------------------------------- /packages/pure-validations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/pure-validations", 3 | "version": "5.0.32", 4 | "description": "Validation api via functional composition", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/pure-validations", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/pure-validations" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "dependencies": { 28 | "@totalsoft/zion": "^5.0.22", 29 | "daggy": "^1.4.0", 30 | "fantasy-land": "^4.1.0" 31 | }, 32 | "peerDependencies": { 33 | "i18next": ">= 17.0.11" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/hooks/useDirtyFieldValidation.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useState, useCallback } from 'react'; 5 | import { useValidation } from './' 6 | import { isPropertyDirty } from '@totalsoft/change-tracking' 7 | 8 | export function useDirtyFieldValidation(rules, options = {}, deps = []) { 9 | const [dirtyOnly, setDirtyOnly] = useState(true) 10 | 11 | const isDirtyFilter = useCallback((context) => { 12 | if (!context.dirtyInfo) { 13 | return true 14 | } 15 | 16 | return isPropertyDirty(context.fieldPath.join("."), context.dirtyInfo) ? true : false 17 | }, []) 18 | 19 | const [validation, validate, reset] = 20 | useValidation(rules, { 21 | ...options, 22 | fieldFilterFunc: isDirtyFilter 23 | }, [...deps]) 24 | 25 | 26 | return [ 27 | validation, 28 | 29 | // Validate 30 | useCallback((model, dirtyInfo) => { 31 | if (!dirtyInfo) { 32 | setDirtyOnly(false) 33 | } 34 | return validate(model, { dirtyInfo: dirtyOnly ? dirtyInfo : undefined }) 35 | }, [validate, dirtyOnly]), 36 | 37 | // Reset 38 | useCallback(() => { 39 | setDirtyOnly(true) 40 | reset() 41 | }, [reset]) 42 | ] 43 | } -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/useChangeTrackingState.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useState, useCallback } from 'react'; 5 | import { create, ensureArrayUIDsDeep, detectChanges, setInnerProp } from '@totalsoft/change-tracking'; 6 | 7 | export function useChangeTrackingState(initialModel) { 8 | const [dirtyInfo, setDirtyInfo] = useState(create) 9 | const [model, setModel] = useState(ensureArrayUIDsDeep(initialModel)) 10 | 11 | return [ 12 | model, 13 | 14 | dirtyInfo, 15 | 16 | // Update model or property 17 | useCallback((value, propertyPath = undefined) => { 18 | const changedModel = propertyPath ? setInnerProp(model, propertyPath, value) : value 19 | if (changedModel === model) { 20 | return model; 21 | } 22 | 23 | const newModel = ensureArrayUIDsDeep(changedModel) 24 | setDirtyInfo(prevDirtyInfo => detectChanges(newModel, model, prevDirtyInfo)) 25 | setModel(newModel) 26 | }, [model]), 27 | 28 | // Reset tracking 29 | useCallback((newModel = undefined) => { 30 | setDirtyInfo(create()) 31 | if (newModel !== undefined) { 32 | setModel(ensureArrayUIDsDeep(newModel)); 33 | } 34 | }, []) 35 | ] 36 | } 37 | 38 | -------------------------------------------------------------------------------- /packages/rules-algebra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/rules-algebra", 3 | "version": "5.0.37", 4 | "description": "Business rules api via functional composition", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/rules-algebra" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "dependencies": { 28 | "@totalsoft/change-tracking": "^5.0.37", 29 | "@totalsoft/zion": "^5.0.22", 30 | "daggy": "^1.4.0", 31 | "fantasy-land": "^4.1.0", 32 | "uniqid": "^5.2.0" 33 | }, 34 | "peerDependencies": { 35 | "i18next": ">= 17.0.11" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /packages/zion/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /packages/pure-validations/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /packages/change-tracking-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/change-tracking-react", 3 | "version": "5.0.37", 4 | "description": "React extensions for the \"change-tracking\" library", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking-react", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/change-tracking-react" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/react-hooks": "^2.0.1", 29 | "react-test-renderer": "^16.9.0" 30 | }, 31 | "dependencies": { 32 | "@totalsoft/change-tracking": "^5.0.37", 33 | "@totalsoft/react-state-lens": "^5.0.37" 34 | }, 35 | "peerDependencies": { 36 | "react": ">= 16.9.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/rules-algebra-react", 3 | "version": "5.0.37", 4 | "description": "React extensions for the \"rules-algebra\" business rules api", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra-react", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/rules-algebra-react" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/react-hooks": "^2.0.1", 29 | "react-test-renderer": "^16.9.0" 30 | }, 31 | "dependencies": { 32 | "@totalsoft/change-tracking-react": "^5.0.37", 33 | "@totalsoft/rules-algebra": "^5.0.37" 34 | }, 35 | "peerDependencies": { 36 | "react": ">= 16.9.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsbb 2 | ##### JavaScript building blocks for a better, safer, side-effect-free world. 3 | 4 | *"Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function."* 5 | 6 | ## The blocks 7 | - [`zion`](./packages/zion#readme) 8 | - [`pure-validations`](./packages/pure-validations#readme) 9 | - [`pure-validations-react`](./packages/pure-validations-react#readme) 10 | - [`rules-algebra`](./packages/rules-algebra#readme) 11 | - [`rules-algebra-react`](./packages/rules-algebra-react#readme) 12 | - [`change-tracking`](./packages/change-tracking#readme) 13 | - [`change-tracking-react`](./packages/change-tracking-react#readme) 14 | - [`react-state-lens`](./packages/react-state-lens#readme) 15 | 16 | ## Bootstrap 17 | ```javascript 18 | yarn install 19 | yarn lerna bootstrap 20 | ``` 21 | 22 | ## Build 23 | ```javascript 24 | yarn lerna run build 25 | ``` 26 | 27 | ## Lint typescript definitions 28 | ```javascript 29 | yarn lerna run tslint 30 | ``` 31 | 32 | ## Test 33 | ```javascript 34 | yarn test 35 | ``` 36 | 37 | ## Publish 38 | ```javascript 39 | yarn lerna publish --contents build patch 40 | yarn lerna publish --contents build minor 41 | yarn lerna publish --contents build major 42 | ``` 43 | 44 | ## License 45 | NodeBB is licensed under the [MIT](LICENSE) license. 46 | 47 | ## Contributing 48 | When using Visual Studio Code please use the extension [`Licenser`](https://marketplace.visualstudio.com/items?itemName=ymotongpoo.licenser) for applying the license header in files. 49 | -------------------------------------------------------------------------------- /packages/zion/src/data/step.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { taggedSum } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import { equals, lte } from "ramda"; 7 | 8 | const Step = taggedSum("Step", { 9 | Loop: ["value"], 10 | Done: ["result"] 11 | }); 12 | 13 | const { Loop, Done } = Step; 14 | 15 | /* Setoid (Step b a) */ { 16 | Step.prototype[fl.equals] = function(that) { 17 | return this.cata({ 18 | Done: x => 19 | that.cata({ 20 | Done: equals(x), 21 | Loop: _ => false 22 | }), 23 | 24 | Loop: x => 25 | that.cata({ 26 | Done: _ => false, 27 | Loop: equals(x) 28 | }) 29 | }); 30 | }; 31 | } 32 | 33 | /* Ord (Step b a) */ { 34 | Step.prototype[fl.lte] = function(that) { 35 | return this.cata({ 36 | Done: x => 37 | that.cata({ 38 | Done: lte(x), 39 | Loop: _ => false 40 | }), 41 | 42 | Loop: x => 43 | that.cata({ 44 | Done: _ => true, 45 | Loop: lte(x) 46 | }) 47 | }); 48 | }; 49 | } 50 | 51 | /* Functor (Step b) */ { 52 | Step.prototype[fl.map] = function(f) { 53 | return this.cata({ 54 | Done: _ => this, 55 | Loop: x => Loop(f(x)) 56 | }); 57 | }; 58 | } 59 | 60 | /* Bifunctor Step */ { 61 | Step.prototype[fl.bimap] = function(f, g) { 62 | return this.cata({ 63 | Done: x => Done(f(x)), 64 | Loop: x => Loop(g(x)) 65 | }); 66 | }; 67 | } 68 | 69 | export { Loop, Done }; 70 | -------------------------------------------------------------------------------- /packages/react-state-lens/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/react-state-lens", 3 | "version": "5.0.37", 4 | "description": "React extensions for the \"change-tracking\" library", 5 | "main": "index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "author": "TotalSoft", 17 | "homepage": "https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/osstotalsoft/jsbb.git", 21 | "directory": "packages/react-state-lens" 22 | }, 23 | "license": "ISC", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/jest-dom": "^5.16.5", 29 | "@testing-library/react": "^12.1.3", 30 | "@testing-library/react-hooks": "^2.0.1", 31 | "react-dom": "^16.9.0", 32 | "react-test-renderer": "^16.9.0" 33 | }, 34 | "dependencies": { 35 | "@totalsoft/change-tracking-react": "^5.0.37", 36 | "@totalsoft/zion": "^5.0.22" 37 | }, 38 | "peerDependencies": { 39 | "react": ">= 16.9.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/pure-validations-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@totalsoft/pure-validations-react", 3 | "version": "5.0.37", 4 | "description": "React extensions for the \"pure-validations\" validation api", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prebuild": "rimraf build", 8 | "build": "yarn build:cjs && yarn build:esm && yarn build:copy-files", 9 | "build:cjs": "cross-env BABEL_ENV=cjs babel src/ --out-dir build/ --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 10 | "build:esm": "cross-env BABEL_ENV=esm babel src/ --out-dir build/esm --ignore \"**/__tests__\" --ignore \"**/__mocks__\"", 11 | "build:copy-files": "node ../../scripts/copy-files.js", 12 | "prepare": "yarn run build", 13 | "test": "jest --watchAll", 14 | "tslint": "tslint -p tsconfig.json \"{src,test}/**/*.{ts,tsx}\"" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/osstotalsoft/jsbb.git", 19 | "directory": "packages/pure-validations-react" 20 | }, 21 | "author": "TotalSoft", 22 | "license": "ISC", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/osstotalsoft/jsbb/issues" 28 | }, 29 | "homepage": "https://github.com/osstotalsoft/jsbb#readme", 30 | "dependencies": { 31 | "@totalsoft/change-tracking-react": "^5.0.37", 32 | "@totalsoft/pure-validations": "^5.0.32", 33 | "lodash.get": "^4.4.2" 34 | }, 35 | "devDependencies": { 36 | "@testing-library/react-hooks": "^2.0.1", 37 | "react-test-renderer": "^16.9.0" 38 | }, 39 | "peerDependencies": { 40 | "react": ">= 16.9.0", 41 | "react-i18next": ">= 10.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/pure-validations/src/higherOrderValidators/field.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { contramap, $do } from "@totalsoft/zion"; 5 | import { Validator } from "../validator"; 6 | import { map, curry } from "ramda"; 7 | 8 | import ValidationError from "../validationError"; 9 | import { Success, isValid } from "../validation" 10 | import { checkValidators } from "./_utils"; 11 | 12 | const field = curry(function field(key, validator) { 13 | checkValidators(validator); 14 | return ( 15 | validator 16 | |> _logFieldPath 17 | |> _filterFieldPath 18 | |> contramap((model, ctx) => [model ? model[key] : undefined, _getFieldContext(model, ctx, key)]) 19 | |> map(map(ValidationError.moveToField(key))) 20 | ); 21 | }); 22 | 23 | function _logFieldPath(validator) { 24 | return $do(function* () { 25 | const [, fieldContext] = yield Validator.ask(); 26 | const validation = yield validator; 27 | _log(fieldContext, `Validation ${isValid(validation) ? "succeded" : "failed"} for path ${fieldContext.fieldPath.join(".")}`); 28 | return validation; 29 | }); 30 | } 31 | 32 | function _filterFieldPath(validator) { 33 | return $do(function* () { 34 | const [, fieldContext] = yield Validator.ask(); 35 | return !fieldContext.fieldFilter(fieldContext) ? Success : yield validator; 36 | }); 37 | } 38 | 39 | function _getFieldContext(model, context, key) { 40 | return { ...context, fieldPath: [...context.fieldPath, key], parentModel: model, parentContext: context }; 41 | } 42 | 43 | function _log(context, message) { 44 | if (context.log) { 45 | context.logger.log(message); 46 | } 47 | } 48 | 49 | export default field; 50 | -------------------------------------------------------------------------------- /packages/zion/src/data/reader.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { tagged } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import { $do } from "../prelude"; 7 | import { concat } from "ramda"; 8 | 9 | const Reader = tagged("Reader", ["computation"]); 10 | Reader[fl.of] = x => Reader(_ => x); // Monad, Applicative 11 | Reader.ask = () => Reader(ctx => ctx); // Reader 12 | 13 | /* Reader */ { 14 | Reader.prototype.runReader = function runReader(ctx){ 15 | return this.computation(ctx); 16 | } 17 | 18 | Reader.prototype.local = function(f) { 19 | return Reader(ctx => this.computation(f(ctx))); 20 | }; 21 | 22 | Reader.prototype.toString = function() { 23 | return `Reader(${this.computation})`; 24 | }; 25 | } 26 | 27 | /* Functor Reader */ { 28 | Reader.prototype[fl.map] = function(f) { 29 | return Reader(ctx => f(this.computation(ctx))); 30 | }; 31 | } 32 | 33 | /* Apply Reader */ { 34 | Reader.prototype[fl.ap] = function(fn) { 35 | return Reader(ctx => fn.computation(ctx)(this.computation(ctx))); 36 | }; 37 | } 38 | 39 | /* Chain Reader */ { 40 | Reader.prototype[fl.chain] = function(f) { 41 | return Reader(ctx => f(this.computation(ctx)).computation(ctx)); 42 | }; 43 | } 44 | 45 | /* Contravariant Reader */ { 46 | Reader.prototype[fl.contramap] = function(f) { 47 | return Reader(ctx => this.computation(f(ctx))); 48 | }; 49 | } 50 | 51 | /* Semigroup a => Semigroup (Reader a) */ { 52 | Reader.prototype[fl.concat] = function(that) { 53 | const self = this; 54 | return $do(function*() { 55 | const x1 = yield self; 56 | const x2 = yield that; 57 | return concat(x1, x2); 58 | }); 59 | }; 60 | } 61 | 62 | export default Reader; 63 | -------------------------------------------------------------------------------- /packages/zion/src/prelude/__tests__/do.tests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { $do } from "../index"; 5 | import Maybe from "../../data/maybe"; 6 | import List from "../../data/list"; 7 | 8 | const { Just, Nothing } = Maybe; 9 | 10 | const monadSum = (monadX, monadY) => 11 | $do(function*() { 12 | const x = yield monadX; 13 | const y = yield monadY; 14 | return x + y; 15 | }); 16 | 17 | describe("do notation:", () => { 18 | it("should chain Maybes: ", () => { 19 | // Arrange 20 | 21 | // Act && Assert 22 | expect(monadSum(Just(5), Just(6))).toStrictEqual(Just(11)); 23 | expect(monadSum(Just(5), Nothing)).toStrictEqual(Nothing); 24 | }); 25 | 26 | it("should chain Lists: ", () => { 27 | // Arrange 28 | 29 | // Act && Assert 30 | expect(monadSum(List.fromArray([1, 2]), List.fromArray([3]))).toStrictEqual(List.fromArray([1 + 3, 2 + 3])); 31 | expect(monadSum(List.fromArray([1]), List.fromArray([2]))).toStrictEqual(List.fromArray([1 + 2])); 32 | expect(monadSum(List.fromArray([1, 2]), List.fromArray([3, 4]))).toStrictEqual(List.fromArray([1 + 3, 1 + 4, 2 + 3, 2 + 4])); 33 | }); 34 | 35 | it("should chain arrays: ", () => { 36 | // Arrange 37 | 38 | // Act && Assert 39 | expect(monadSum([1, 2], [3])).toStrictEqual([1 + 3, 2 + 3]); 40 | expect(monadSum([1], [2])).toStrictEqual([1 + 2]); 41 | expect(monadSum([1, 2], [3, 4])).toStrictEqual([1 + 3, 1 + 4, 2 + 3, 2 + 4]); 42 | }); 43 | 44 | it("should chain functions: ", () => { 45 | // Arrange 46 | 47 | // Act && Assert 48 | expect(monadSum(x => x + 1, x => x - 1)(7)).toBe(7 + 1 + 7 - 1); 49 | expect(monadSum(x => x * 2, x => x - 1)(5)).toBe(5 * 2 + 5 - 1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/useChangeTrackingLens.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useStateLens, rmap, over, reuseCache } from '@totalsoft/react-state-lens' 5 | import { useCallback, useState, useMemo, useRef } from 'react'; 6 | import { create, detectChanges, ensureArrayUIDsDeep } from '@totalsoft/change-tracking' 7 | 8 | export function useChangeTrackingLens(initialModel) { 9 | const [dirtyInfo, setDirtyInfo] = useState(create) 10 | const stateLens = useStateLens(() => ensureArrayUIDsDeep(initialModel)); 11 | const prevProxy = useRef(null); 12 | const changeTrackingLens = useMemo(() => { 13 | const proxy = stateLens |> rmap( 14 | (changedModel, prevModel) => { 15 | const newModel = ensureArrayUIDsDeep(changedModel) 16 | setDirtyInfo(dirtyInfo => detectChanges(newModel, prevModel, dirtyInfo)); 17 | return newModel; 18 | }) 19 | 20 | if (prevProxy.current) { 21 | reuseCache(prevProxy.current, proxy) 22 | } 23 | prevProxy.current = proxy; 24 | return proxy 25 | }, 26 | [stateLens]) 27 | 28 | return [ 29 | changeTrackingLens, 30 | dirtyInfo, 31 | 32 | // Reset 33 | useCallback((newModel = undefined) => { 34 | setDirtyInfo(create()) 35 | over(stateLens, (prevModel => { 36 | return newModel !== undefined 37 | ? typeof newModel == "function" 38 | ? ensureArrayUIDsDeep(newModel(prevModel)) 39 | : ensureArrayUIDsDeep(newModel) 40 | : prevModel; 41 | })); 42 | }, []) 43 | ] 44 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/primitiveRules/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { lift, curry, curryN } from "ramda"; 6 | import { propertiesChanged } from "../predicates"; 7 | import { when } from "../higherOrderRules"; 8 | 9 | export const ensureRuleParams = curry(function(N, func) { 10 | return curryN(N, function(...args) { 11 | const ruleArgs = args.map(ensureRule); 12 | return func(...ruleArgs); 13 | }); 14 | }); 15 | 16 | export const unchanged = Rule(model => model); 17 | 18 | export function constant(value) { 19 | return Rule.of(value); 20 | } 21 | 22 | export function computed(computation) { 23 | return Rule((prop, { document, prevDocument }) => computation(document, prevDocument, prop)); 24 | } 25 | 26 | export const min = lift(curry(Math.min)) |> ensureRuleParams(2); 27 | export const max = lift(curry(Math.max)) |> ensureRuleParams(2); 28 | export const sum = lift(a => b => a + b) |> ensureRuleParams(2); 29 | 30 | export const minimumValue = max(unchanged) |> ensureRuleParams(1); 31 | export const maximumValue = min(unchanged) |> ensureRuleParams(1); 32 | 33 | export function sprintf(format) { 34 | const params = format.match(/{{\s*[\w.]+\s*}}/g).map(x => x.match(/[\w.]+/)[0]); 35 | const makeRegex = key => new RegExp(`{{${key}}}`); 36 | return ( 37 | computed(document => params.reduce((str, key) => str.replace(makeRegex(key), document[key]), format)) 38 | |> when(propertiesChanged(document => params.map(key => document[key]))) 39 | ); 40 | } 41 | 42 | function ensureRule(selector) { 43 | if (Rule.is(selector)) { 44 | return selector; 45 | } 46 | if (typeof selector === "function") { 47 | return computed(selector); 48 | } 49 | 50 | return constant(selector); 51 | } 52 | -------------------------------------------------------------------------------- /packages/change-tracking/src/arrayUtils/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | /** 5 | * Ensures unique identifiers for object items in arrays. 6 | * It returns the same object hierarcy as the model but it attaches unique identifiers to array items. 7 | * Only items of type "object" will have identifiers added. 8 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#ensurearrayuidsdeep 9 | */ 10 | export function ensureArrayUIDsDeep(model: any): any; 11 | 12 | /** 13 | * Ensures unique identifiers for object items in the given array. 14 | * It returns the same array as the modeinputl but it attaches unique identifiers to items. 15 | * Only items of type "object" will have identifiers added. 16 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#ensurearrayuids 17 | */ 18 | export function ensureArrayUIDs(array: any[]): any[]; 19 | 20 | /** 21 | * Gets the item with the same uid if it has one or the item at the same index otherwise. 22 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#findmatchingitem 23 | */ 24 | export function findMatchingItem(currentItem: any, currentIndex: number, otherArray: any[]): any[]; 25 | 26 | /** 27 | * Transforms an array with object items that have uids to a map where uids are keys. 28 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#touniqueidmap 29 | */ 30 | export function toUniqueIdMap(array: any[]): {[uid: string]: {value: any, index: number}}; 31 | 32 | /** 33 | * Checks if the items in the provided arrays have the same order based on the uid or value. 34 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#hasSameItemOrder 35 | */ 36 | export function hasSameItemOrder(firstArray: any[], secondArray: any[]): boolean; 37 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/hooks/useRules.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useState, useCallback, useMemo } from 'react'; 5 | import { create, detectChanges, ensureArrayUIDsDeep, setInnerProp} from '@totalsoft/change-tracking' 6 | import { logTo, applyRule } from '@totalsoft/rules-algebra' 7 | 8 | export function useRules(rules, initialModel, { isLogEnabled = true, logger = console } = {}, deps = []) { 9 | const [dirtyInfo, setDirtyInfo] = useState(create) 10 | const [model, setModel] = useState(ensureArrayUIDsDeep(initialModel)) 11 | 12 | const rulesEngine = useMemo(() => { 13 | let newRules = rules; 14 | 15 | if (isLogEnabled) { 16 | newRules = logTo(logger)(newRules) 17 | } 18 | 19 | return newRules 20 | }, [rules, isLogEnabled, logger, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps 21 | 22 | return [ 23 | model, 24 | 25 | dirtyInfo, 26 | 27 | // Update model or property 28 | useCallback((value, propertyPath = undefined) => { 29 | const changedModel = propertyPath ? setInnerProp(model, propertyPath, value) : value 30 | if (changedModel === model) { 31 | return model; 32 | } 33 | 34 | const result = applyRule(rulesEngine, ensureArrayUIDsDeep(changedModel), model) 35 | setDirtyInfo(detectChanges(result, model, dirtyInfo)) 36 | setModel(result); 37 | return result; 38 | }, [model, dirtyInfo, rulesEngine]), 39 | 40 | // Reset 41 | useCallback((newModel = undefined) => { 42 | setDirtyInfo(create()) 43 | if (newModel !== undefined) { 44 | setModel(ensureArrayUIDsDeep(newModel)); 45 | } 46 | }, []) 47 | ] 48 | } 49 | 50 | -------------------------------------------------------------------------------- /packages/zion/src/prelude/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import * as fl from "fantasy-land"; 5 | import immutagen from "immutagen"; 6 | import { chain, reduce, concat, curry, always, identity } from "ramda"; 7 | 8 | function $do(gen) { 9 | const doNext = (next, typeRep) => input => { 10 | const { value, next: nextNext } = next(input); 11 | 12 | if (!nextNext) { 13 | return pure(typeRep)(value); 14 | } 15 | 16 | return chain(doNext(nextNext, value.constructor), value); 17 | }; 18 | return doNext(immutagen(gen))(); 19 | } 20 | 21 | // pure :: Applicative f => TypeRep f -> a -> f a 22 | const pure = function(A) { 23 | if (A[fl.of]) { 24 | return A[fl.of]; 25 | } 26 | if (A === Array) { 27 | return A.of; 28 | } 29 | if (A === Function) { 30 | return always; 31 | } 32 | throw Error(`TypeRep ${A} is not Applicative`); 33 | }; 34 | 35 | // contramap :: Contravariant f => (b -> a) -> f a -> f b 36 | const contramap = curry(function contramap(fn, contravariant) { 37 | return contravariant[fl.contramap](fn); 38 | }); 39 | 40 | // fold :: Monoid m, Foldable f => (a -> m a) -> f a -> m a 41 | const fold = curry(function(M, xs) { 42 | return xs |> reduce((acc, x) => concat(acc, M(x)), M[fl.empty]()); 43 | }); 44 | 45 | // promap :: Profunctor p => (a -> b) -> (c -> d) -> p b c -> p a d 46 | const promap = curry(function promap(fn1, fn2, profunctor) { 47 | return profunctor[fl.promap](fn1, fn2); 48 | }); 49 | 50 | // lmap :: Profunctor p => (a -> b) -> p b c -> p a c 51 | const lmap = curry(function lmap(fn, profunctor) { 52 | return profunctor[fl.promap](fn, identity); 53 | }); 54 | 55 | // rmap :: Profunctor p => (b -> c) -> p a b -> p a c 56 | const rmap = curry(function rmap(fn, profunctor) { 57 | return profunctor[fl.promap](identity, fn); 58 | }); 59 | 60 | export { $do, pure, contramap, fold, promap, lmap, rmap }; 61 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { DirtyInfo } from "@totalsoft/change-tracking"; 5 | import { Validator, Logger, FilterFunc, Success, Failure } from "@totalsoft/pure-validations"; 6 | import { ValidatorContext } from "packages/pure-validations/src/validator"; 7 | import { ValidationProxy } from "../validationProxy"; 8 | 9 | /** 10 | * React hook for model validation using the @totalsof/pure-validation library. 11 | * Returns a statefull validation result, a function that performs the validation and a function that resets the validation state. 12 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/pure-validations-react#usevalidation-hook 13 | */ 14 | export function useValidation( 15 | rules: Validator, 16 | options?: { isLogEnabled: boolean; logger: Logger; fieldFilterFunc: FilterFunc }, 17 | deps?: any[] 18 | ): [ 19 | // Validation result 20 | ValidationProxy, 21 | 22 | // Validate function 23 | (model: TModel, context?: ValidatorContext) => boolean, 24 | 25 | // Reset validation function 26 | () => void 27 | ]; 28 | 29 | /** 30 | * React hook that uses dirty fields info to validate only the fields that were modified. 31 | * Returns a statefull validation result, a function that performs the validation and a function that resets the validation state. 32 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/pure-validations-react#usedirtyfieldvalidation-hook 33 | */ 34 | export function useDirtyFieldValidation( 35 | rules: Validator, 36 | options?: { isLogEnabled: boolean; logger: Logger; fieldFilterFunc: FilterFunc }, 37 | deps?: any[] 38 | ): [ 39 | // Validation result 40 | ValidationProxy, 41 | 42 | // Validate function 43 | (model: TModel, dirtyinfo?: DirtyInfo) => boolean, 44 | 45 | // Reset validation function 46 | () => void 47 | ]; 48 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/hooks/useValidation.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useState, useCallback, useMemo, useEffect } from 'react'; 5 | import { Success, validate, logTo, filterFields } from '@totalsoft/pure-validations'; 6 | import { ValidationProxy, isValid } from '../validationProxy'; 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | 10 | export function useValidation(rules, { isLogEnabled = true, logger = console, fieldFilterFunc = undefined } = {}, deps = []) { 11 | const [validation, setValidation] = useState(ValidationProxy(Success)); 12 | const [, i18n] = useTranslation() 13 | const [state, setState] = useState({}) 14 | 15 | const validator = useMemo(() => { 16 | let newValidator = rules; 17 | 18 | if (isLogEnabled) { 19 | newValidator = logTo(logger)(newValidator) 20 | } 21 | 22 | if (fieldFilterFunc) { 23 | newValidator = filterFields(fieldFilterFunc)(newValidator) 24 | } 25 | 26 | return newValidator 27 | }, [rules, isLogEnabled, logger, fieldFilterFunc, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps 28 | 29 | useEffect(() => { 30 | if (isValid(validation)) { 31 | return 32 | } 33 | const validationProxy = ValidationProxy(validate(validator, state.model, state.context)); 34 | setValidation((validationProxy)); 35 | }, [i18n.language]) 36 | 37 | return [ 38 | validation, 39 | 40 | // Validate 41 | useCallback((model, context) => { 42 | const validation = ValidationProxy(validate(validator, model, context)); 43 | setState({ model, context }) 44 | setValidation((validation)); 45 | return isValid(validation); 46 | }, [validator]), 47 | 48 | // Reset 49 | useCallback(() => { 50 | setValidation(ValidationProxy(Success)) 51 | setState({}) 52 | }, []) 53 | ] 54 | } -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/hooks/useRulesLens.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { useStateLens, rmap, over, reuseCache } from '@totalsoft/react-state-lens' 5 | import { useMemo, useCallback, useState, useRef } from 'react'; 6 | import { applyRule, logTo } from '@totalsoft/rules-algebra'; 7 | import { create, detectChanges, ensureArrayUIDsDeep } from '@totalsoft/change-tracking' 8 | 9 | export function useRulesLens(rules, initialModel, { isLogEnabled = true, logger = console } = {}, deps = []) { 10 | const [dirtyInfo, setDirtyInfo] = useState(create) 11 | 12 | const rulesEngine = useMemo(() => { 13 | let newRules = rules; 14 | 15 | if (isLogEnabled) { 16 | newRules = logTo(logger)(newRules) 17 | } 18 | 19 | return newRules 20 | }, [rules, isLogEnabled, logger, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps 21 | 22 | const stateLens = useStateLens(() => ensureArrayUIDsDeep(initialModel)); 23 | const prevProxy = useRef(null); 24 | const rulesEngineLens = useMemo(() => { 25 | const proxy = stateLens |> rmap( 26 | (changedModel, prevModel) => { 27 | const result = applyRule(rulesEngine, ensureArrayUIDsDeep(changedModel), prevModel) 28 | setDirtyInfo(prevDirtyInfo => detectChanges(result, prevModel, prevDirtyInfo)) 29 | return result; 30 | }) 31 | if (prevProxy.current) { 32 | reuseCache(prevProxy.current, proxy) 33 | } 34 | prevProxy.current = proxy; 35 | return proxy 36 | }, [stateLens, rulesEngine]) 37 | 38 | return [ 39 | rulesEngineLens, 40 | dirtyInfo, 41 | 42 | // Reset 43 | useCallback((newModel = undefined) => { 44 | setDirtyInfo(create()) 45 | over(stateLens, (prevModel => { 46 | return newModel !== undefined ? ensureArrayUIDsDeep(newModel) : prevModel 47 | })); 48 | }, []) 49 | ] 50 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/__tests__/workshop.tests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { applyRule } from "../"; 5 | import { } from "../higherOrderRules"; // import { when, shape, logTo, scope, chainRules, items, root, parent, fromModel } from "../higherOrderRules"; 6 | import { unchanged } from "../primitiveRules"; // import { constant, computed, maximumValue, unchanged } from "../primitiveRules"; 7 | import { } from "../predicates" // import { propertyChanged, any, all, propertiesChanged } from "../predicates"; 8 | 9 | import { ensureArrayUIDsDeep } from "@totalsoft/change-tracking"; 10 | 11 | 12 | describe("workshop examples:", () => { 13 | it("workshop example: ", () => { 14 | // Arrange 15 | const rule = unchanged 16 | 17 | const originalLoan = { 18 | aquisitionPrice: 100, 19 | interestRate: 0.05, 20 | advance: 10, 21 | approved: true, 22 | persons: [ 23 | { name: "Doe", surname: "John" }, 24 | { name: "Klaus", surname: "John"} 25 | ] 26 | } |> ensureArrayUIDsDeep 27 | 28 | const changedLoan = { 29 | ...originalLoan, 30 | advance: 20, 31 | isCompanyLoan: false, 32 | persons: [ 33 | { ...originalLoan.persons[0], name: "Smith" }, 34 | { ...originalLoan.persons[1], surname: "Santa" }, 35 | ] 36 | } 37 | 38 | // Act 39 | const result = applyRule(rule, changedLoan, originalLoan) 40 | 41 | // Assert 42 | expect(result).toStrictEqual({ 43 | ...changedLoan, 44 | // advancePercent: 20, 45 | // approved: false, 46 | // persons: [ 47 | // { ...changedLoan.persons[0], fullName: "John Smith"}, //, isCompanyRep: false }, 48 | // { ...changedLoan.persons[1], fullName: "Santa Klaus"} //, isCompanyRep: false } 49 | // ] 50 | }) 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/__tests__/useDirtyInfo.tests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { renderHook, act } from "@testing-library/react-hooks"; 5 | import { useDirtyInfo } from "../"; 6 | 7 | jest.unmock("@totalsoft/change-tracking") 8 | 9 | describe("useDirtyInfo hook", () => { 10 | it("should set dirtyInfo property", () => { 11 | // Arrange 12 | 13 | // Act 14 | const { result } = renderHook(() => useDirtyInfo()); 15 | act(() => { 16 | const [, setProperty] = result.current; 17 | setProperty("a.b", "OK"); 18 | }); 19 | 20 | // Assert 21 | const [dirtyInfo] = result.current; 22 | expect(dirtyInfo.a.b).toBe(true); 23 | }); 24 | 25 | it("should enforce reference economy for dirty info", () => { 26 | // Arrange 27 | 28 | // Act 29 | const { result } = renderHook(() => useDirtyInfo()); 30 | act(() => { 31 | const [, setProperty] = result.current; 32 | setProperty("a.b", "OK"); 33 | }); 34 | const [dirtyInfo1] = result.current; 35 | 36 | act(() => { 37 | const [, setProperty] = result.current; 38 | setProperty("a.b", "OK"); 39 | }); 40 | 41 | // Assert 42 | const [dirtyInfo2] = result.current; 43 | expect(dirtyInfo1).toBe(dirtyInfo2); 44 | }); 45 | 46 | it("should reset dirtyInfo property", () => { 47 | // Arrange 48 | 49 | // Act 50 | const { result } = renderHook(() => useDirtyInfo()); 51 | act(() => { 52 | const [, setProperty] = result.current; 53 | setProperty("a.b", "OK"); 54 | }); 55 | act(() => { 56 | const [, , resetDirtyInfo] = result.current; 57 | resetDirtyInfo(); 58 | }); 59 | 60 | 61 | // Assert 62 | const [dirtyInfo] = result.current; 63 | expect(dirtyInfo.a ?.b).toBe(undefined); 64 | }); 65 | }); 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/zion/src/data/map.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { tagged } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import Maybe from "./maybe"; 7 | import List from "./list"; 8 | import KeyValuePair from "./keyValuePair"; 9 | import { equals, map, concat, reduce, identity, curry } from "ramda"; 10 | 11 | const { Just, Nothing } = Maybe; 12 | 13 | //data Map a b :: [a] * a -> Maybe 14 | const Map = tagged("Map", ["obj"]); 15 | 16 | Map.getValueAt = curry(function getValueAt(key, aMap) { 17 | const x = aMap.obj[key]; 18 | if (x == undefined) { 19 | return Nothing; 20 | } 21 | return Just(x); 22 | }) 23 | 24 | Map.fromObj = function fromObj(obj){ 25 | return Map(obj); 26 | } 27 | 28 | Map.fromList = function fromList(list) { 29 | const obj = list |> reduce((acc, current) => (acc[KeyValuePair.getKey(current)] = KeyValuePair.getValue(current)), {}); 30 | return Map(obj); 31 | }; 32 | 33 | Map.toList = function toList(aMap) { 34 | return List.fromArray(Object.keys(aMap)) |> map(key => KeyValuePair(key, aMap.obj[key])); 35 | }; 36 | 37 | /* Setoid a => Setoid (Map a) */ 38 | Map.prototype[fl.equals] = function(that) { 39 | return equals(Map.toList(that), Map.toList(this)); 40 | }; 41 | 42 | /* Functor ObjectTree */ 43 | Map.prototype[fl.map] = function(f) { 44 | return Map.fromList(Map.toList(this) |> map(f)); 45 | }; 46 | 47 | /* Foldable Map */ 48 | Map.prototype[fl.reduce] = function(f, acc) { 49 | return Map.toList(this) |> reduce(f, acc); 50 | }; 51 | 52 | /* Semigroup Map */ 53 | Map.prototype[fl.concat] = function(that) { 54 | const fields = [...new Set([...Object.keys(this.obj), ...Object.keys(that.obj)])]; 55 | var result = {}; 56 | for (let f of fields) { 57 | const thisValue = Map.getValueAt(f, this); 58 | const thatValue = Map.getValueAt(f, that); 59 | const concatValue = concat(thisValue, thatValue); 60 | result[f] = concatValue.cata({ 61 | Just: identity, 62 | Nothing: _ => undefined //??????????? 63 | }); 64 | } 65 | 66 | return Map(result); 67 | }; 68 | 69 | /* Monoid Map */ 70 | Map[fl.empty] = () => Map({}); 71 | 72 | export default Map; 73 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/src/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule, Logger } from "@totalsoft/rules-algebra"; 5 | import { DirtyInfo } from "@totalsoft/change-tracking"; 6 | import { LensProxy } from "@totalsoft/react-state-lens"; 7 | 8 | /** 9 | * React hook that applies the business rules and keeps track of user modified values (dirty field info). 10 | * Receives the rules, the initial model. Optional arguments are the settings and dependencies 11 | * Returns a stateful rule application result, a dirty info object, a function that sets a value for the given property path and a function that resets the rule engine state. 12 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra-react#useRules-hook 13 | */ 14 | export function useRules( 15 | rules: Rule, 16 | initialModel: any, 17 | options?: { isLogEnabled: boolean; logger: Logger; }, 18 | deps?: any[] 19 | ): [ 20 | // Validation result 21 | any, 22 | 23 | DirtyInfo, 24 | 25 | // Set a value at the given property path 26 | (propertyPath: string, value: any) => void, 27 | 28 | // Resets rule engine changes ands sets a new model 29 | (newModel: any) => void 30 | ]; 31 | 32 | /** 33 | * React hook that applies the business rules and keeps track of user modified values (dirty field info). 34 | * Receives the rules, the initial model. Optional arguments are the settings and dependencies 35 | * Returns a stateful profunctor lens with the rule application result, a dirty info object and a function that resets the rule engine state. 36 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra-react#useRulesLens-hook 37 | */ 38 | export function useRulesLens( 39 | rules: Rule, 40 | initialModel: any, 41 | options?: { isLogEnabled: boolean; logger: Logger; }, 42 | deps?: any[] 43 | ): [ 44 | // Model lens 45 | LensProxy, 46 | 47 | DirtyInfo, 48 | 49 | // Set a value at the given property path 50 | (propertyPath: string, value: any) => void, 51 | 52 | // Resets rule engine changes ands sets a new model 53 | (newModel: any) => void 54 | ]; 55 | -------------------------------------------------------------------------------- /packages/change-tracking-react/src/hooks/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { DirtyInfo } from "@totalsoft/change-tracking"; 5 | import { LensProxy } from "@totalsoft/react-state-lens"; 6 | 7 | /** 8 | * Keeps track of modified properties of an external model. 9 | * Returns a stateful dirty info object, a function that sets the property path as dirty and a function that resets the dirty info state. 10 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking-react#useDirtyInfo-hook 11 | */ 12 | export function useDirtyInfo(): [ 13 | // DirtyInfo 14 | DirtyInfo, 15 | 16 | // Set dirty info for the property path 17 | (propertyPath: string) => void, 18 | 19 | // Reset dirty info 20 | () => void 21 | ]; 22 | 23 | /** 24 | * Provides a stateful model with change tracking. 25 | * Returns a stateful model, stateful dirty info object, a function that sets the model or property value and a function that resets the change tracking. 26 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking-react#useChangeTrackingState-hook 27 | */ 28 | export function useChangeTrackingState(initialModel?: TModel): [ 29 | // Model 30 | TModel, 31 | 32 | // DirtyInfo 33 | DirtyInfo, 34 | 35 | // Set set model or property value 36 | (model: TModel, propertyPath?: string | string[]) => void, 37 | 38 | // Reset dirty info 39 | (newModel?: TModel) => void 40 | ]; 41 | 42 | /** 43 | * Provides a stateful model with change tracking using a profunctor lens. 44 | * Receives the initial model 45 | * Returns a stateful profunctor lens with the rule application result, a dirty info object and a function that resets the change tracking. 46 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking-react#useChangeTrackingLens-hook 47 | */ 48 | export function useChangeTrackingLens(initialModel: TModel): [ 49 | // Model lens 50 | LensProxy, 51 | 52 | // DirtyInfo object 53 | DirtyInfo, 54 | 55 | // Resets the change tracking and optionally sets a new model 56 | (newModel?: TModel | ((param?: TModel) => TModel)) => void 57 | ]; 58 | -------------------------------------------------------------------------------- /packages/pure-validations-react/src/validationProxy/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Success, isValid as pureIsValid, getErrors as pureGetErrors, getInner } from '@totalsoft/pure-validations'; 5 | 6 | const isValidPropSymbol = Symbol("isValid") 7 | const errorsPropSymbol = Symbol("errors") 8 | const errorSeparatorSymbol = Symbol("errorSeparator") 9 | const targetSymbol = Symbol("target") 10 | const defaultErrorSeparator = ", " 11 | 12 | const handler = { 13 | get: function (target, name) { 14 | if (name in target) { 15 | return target[name] 16 | } 17 | 18 | switch (name) { 19 | case targetSymbol: { 20 | return target 21 | } 22 | 23 | case isValidPropSymbol: { 24 | const valid = pureIsValid(target) 25 | target[isValidPropSymbol] = valid; // cache value 26 | return valid 27 | } 28 | case errorsPropSymbol: { 29 | const errors = pureGetErrors(target).join(target[errorSeparatorSymbol] || defaultErrorSeparator) 30 | target[errorsPropSymbol] = errors; // cache value 31 | return errors 32 | } 33 | default: { 34 | const proxy = ValidationProxy(getInner([name], target)) 35 | target[name] = proxy; // cache value 36 | return proxy 37 | } 38 | } 39 | } 40 | } 41 | 42 | const successProxy = initializeSuccessProxy() 43 | 44 | function initializeSuccessProxy() { 45 | const proxy = new Proxy(Success, handler) 46 | proxy[isValidPropSymbol] 47 | proxy[errorsPropSymbol] 48 | proxy[targetSymbol] 49 | return proxy; 50 | } 51 | 52 | export function eject(proxy) { 53 | return proxy[targetSymbol] 54 | } 55 | 56 | export function getErrors(proxy, separator) { 57 | proxy[errorSeparatorSymbol] = separator 58 | return proxy[errorsPropSymbol] 59 | } 60 | 61 | export function isValid(proxy) { 62 | return proxy[isValidPropSymbol] 63 | } 64 | 65 | export function ValidationProxy(validation) { 66 | return isValid(validation) ? successProxy : new Proxy(validation, handler) 67 | } -------------------------------------------------------------------------------- /packages/change-tracking/src/objectUtils/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | 6 | export const setInnerProp = curry(function setInnerProp(obj, path, value) { 7 | function inner(obj, searchKeyPath) { 8 | const [prop, ...rest] = searchKeyPath; 9 | if (prop == undefined) { 10 | return value 11 | } 12 | const newValue = rest.length > 0 ? inner(obj[prop], rest) : value 13 | return _immutableAssign(obj, prop, newValue); 14 | } 15 | 16 | if (typeof path === "string") { 17 | path = path.split(".") 18 | } 19 | 20 | return inner(obj, path); 21 | }); 22 | 23 | export const getInnerProp = curry(function getInnerProp(obj, searchKeyPath = []) { 24 | const [prop, ...rest] = searchKeyPath; 25 | return prop !== undefined ? getInnerProp(obj[prop], rest) : obj; 26 | }); 27 | 28 | 29 | function _immutableAssign(obj, prop, value) { 30 | if (obj[prop] === value) { 31 | return obj; 32 | } 33 | 34 | return Array.isArray(obj) 35 | ? Object.assign([], obj, { [prop]: value }) 36 | : { ...obj, [prop]: value } 37 | } 38 | 39 | export function defaultVal(type) { 40 | if (typeof type !== 'string') throw new TypeError('Type must be a string.'); 41 | 42 | // Handle simple types (primitives and plain function/object) 43 | switch (type) { 44 | // eslint-disable-next-line no-undef 45 | case 'bigint' : return BigInt(0); 46 | case 'boolean' : return false; 47 | case 'function' : return function () {}; 48 | case 'null' : return null; 49 | case 'number' : return 0; 50 | case 'object' : return {}; 51 | case 'string' : return ""; 52 | case 'symbol' : return Symbol(); 53 | case 'undefined' : return void 0; 54 | } 55 | 56 | try { 57 | // Look for constructor in this or current scope 58 | var ctor = typeof this[type] === 'function' 59 | ? this[type] 60 | : eval(type); 61 | 62 | return new ctor; 63 | 64 | // Constructor not found, return new object 65 | } catch (e) { return {}; } 66 | } -------------------------------------------------------------------------------- /packages/rules-algebra/src/rule.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { checkRules } from "./_utils" 5 | import { curry } from "ramda"; 6 | import * as fl from "fantasy-land"; 7 | import { tagged } from "daggy"; 8 | import { $do } from "@totalsoft/zion/prelude"; 9 | import { concat } from "ramda"; 10 | 11 | export const Rule = tagged("Rule", ["computation"]); 12 | Rule[fl.of] = x => Rule(_ => x); // Monad, Applicative 13 | Rule.ask = () => Rule((model, ctx) => [model, ctx]); // Reader 14 | 15 | /* Rule */ { 16 | Rule.prototype.runRule = function runRule(model, ctx){ 17 | return this.computation(model, ctx); 18 | } 19 | } 20 | 21 | /* Functor Rule */ { 22 | Rule.prototype[fl.map] = function(f) { 23 | return Rule((model, ctx) => f(this.computation(model, ctx))); 24 | }; 25 | } 26 | 27 | /* Apply Rule */ { 28 | Rule.prototype[fl.ap] = function(fn) { 29 | return Rule((model, ctx) => fn.computation(model, ctx)(this.computation(model, ctx))); 30 | }; 31 | } 32 | 33 | /* Chain Rule */ { 34 | Rule.prototype[fl.chain] = function(f) { 35 | return Rule((model, ctx) => f(this.computation(model, ctx)).computation(model, ctx)); 36 | }; 37 | } 38 | 39 | /* Contravariant Reader */ { 40 | Rule.prototype[fl.contramap] = function(f) { 41 | return Rule((model, ctx) => this.computation(...f(model, ctx))); 42 | }; 43 | } 44 | 45 | /* Show Rule */ { 46 | Rule.prototype.toString = function() { 47 | return `Rule(${this.computation})`; 48 | }; 49 | } 50 | 51 | /* Semigroup a => Semigroup (Rule a) */ { 52 | Rule.prototype[fl.concat] = function(that) { 53 | const self = this; 54 | return $do(function*() { 55 | const x1 = yield self; 56 | const x2 = yield that; 57 | return concat(x1, x2); 58 | }); 59 | }; 60 | } 61 | 62 | Rule.of = Rule[fl.of]; 63 | 64 | const emptyContext = { 65 | prevDocument: undefined, 66 | document: undefined, 67 | fieldPath: [], 68 | scopePath: [], 69 | log: false, 70 | logger: { log: () => { } }, 71 | }; 72 | 73 | export const applyRule = curry(function applyRule(rule, newModel, prevModel = undefined, ctx = undefined) { 74 | checkRules(rule) 75 | return rule.runRule(newModel, { ...emptyContext, ...ctx, prevModel, document: newModel, prevDocument: prevModel }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/pure-validations/src/validator.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { checkValidators } from "./higherOrderValidators/_utils"; 5 | import { curry } from "ramda"; 6 | import * as fl from "fantasy-land"; 7 | import { tagged } from "daggy"; 8 | import { $do } from "@totalsoft/zion/prelude"; 9 | import { concat } from "ramda"; 10 | 11 | 12 | const Validator = tagged("Validator", ["computation"]); 13 | Validator[fl.of] = x => Validator(_ => x); // Monad, Applicative 14 | Validator.ask = () => Validator((model, ctx) => [model, ctx]); // Reader 15 | 16 | /* Validator */ { 17 | Validator.prototype.runValidator = function runValidator(model, ctx) { 18 | return this.computation(model, ctx); 19 | } 20 | } 21 | 22 | /* Functor Validator */ { 23 | Validator.prototype[fl.map] = function (f) { 24 | return Validator((model, ctx) => f(this.computation(model, ctx))); 25 | }; 26 | } 27 | 28 | /* Apply Validator */ { 29 | Validator.prototype[fl.ap] = function (fn) { 30 | return Validator((model, ctx) => fn.computation(model, ctx)(this.computation(model, ctx))); 31 | }; 32 | } 33 | 34 | /* Chain Validator */ { 35 | Validator.prototype[fl.chain] = function (f) { 36 | return Validator((model, ctx) => f(this.computation(model, ctx)).computation(model, ctx)); 37 | }; 38 | } 39 | 40 | /* Contravariant Reader */ { 41 | Validator.prototype[fl.contramap] = function (f) { 42 | return Validator((model, ctx) => this.computation(...f(model, ctx))); 43 | }; 44 | } 45 | 46 | /* Show Validator */ { 47 | Validator.prototype.toString = function () { 48 | return `Validator(${this.computation})`; 49 | }; 50 | } 51 | 52 | /* Semigroup a => Semigroup (Validator a) */ { 53 | Validator.prototype[fl.concat] = function (that) { 54 | const self = this; 55 | return $do(function* () { 56 | const x1 = yield self; 57 | const x2 = yield that; 58 | return concat(x1, x2); 59 | }); 60 | }; 61 | } 62 | 63 | Validator.of = Validator[fl.of]; 64 | 65 | const emptyContext = { 66 | fieldPath: [], 67 | fieldFilter: _ => true, 68 | log: false, 69 | logger: { log: () => { } }, 70 | parentModel: null, 71 | parentContext: null 72 | }; 73 | 74 | const validate = curry(function validate(validator, model, ctx = undefined) { 75 | checkValidators(validator); 76 | return validator.runValidator(model, { ...emptyContext, ...ctx }); 77 | }); 78 | 79 | export { Validator, validate }; 80 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | 3 | api.cache.using(() => process.env.NODE_ENV) 4 | 5 | const defaultAlias = { 6 | "@totalsoft/zion": "@totalsoft/zion/src", 7 | "@totalsoft/pure-validations": "@totalsoft/pure-validations/src", 8 | "@totalsoft/rules-algebra": "@totalsoft/rules-algebra/src", 9 | "@totalsoft/react-state-lens": "@totalsoft/react-state-lens/src", 10 | "@totalsoft/change-tracking": "@totalsoft/change-tracking/src", 11 | "@totalsoft/change-tracking-react": "@totalsoft/change-tracking-react/src", 12 | "@totalsoft/change-tracking-react/lensProxy": "@totalsoft/change-tracking-react/src/lensProxy", 13 | }; 14 | 15 | 16 | // const presets = api.env("test") 17 | // ? [ 18 | // [ 19 | // "@babel/preset-env", 20 | // { 21 | // targets: { node: "current" } 22 | // } 23 | // ] 24 | // ] 25 | // : []; 26 | 27 | const defaultPlugins = [["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }], "@babel/plugin-proposal-optional-chaining"]; 28 | 29 | // const plugins = api.env("test") 30 | // ? [ 31 | // ...defaultPlugins, 32 | // [ 33 | // "babel-plugin-module-resolver", 34 | // { 35 | // root: ["./"], 36 | // alias: defaultAlias 37 | // } 38 | // ] 39 | // ] 40 | // : [...defaultPlugins, ["@babel/plugin-transform-modules-commonjs"]]; 41 | 42 | return { 43 | plugins: defaultPlugins, 44 | env: { 45 | cjs: { 46 | presets: [ 47 | [ 48 | "@babel/preset-env", 49 | { 50 | modules: "commonjs" 51 | } 52 | ] 53 | ], 54 | plugins: [["@babel/plugin-transform-runtime"]] 55 | }, 56 | esm: { 57 | plugins: [["@babel/plugin-transform-runtime", { useESModules: true }]] 58 | }, 59 | test: { 60 | presets: [ 61 | ["@babel/preset-react", { 62 | "runtime": "automatic" 63 | }], 64 | [ 65 | "@babel/preset-env", 66 | { 67 | targets: { node: "current" } 68 | } 69 | ] 70 | ], 71 | plugins: [ 72 | [ 73 | "babel-plugin-module-resolver", 74 | { 75 | root: ["./"], 76 | alias: defaultAlias 77 | } 78 | ] 79 | ] 80 | } 81 | } 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/predicates/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../index"; 5 | 6 | export type Selector = (document: TDocument) => TValue; 7 | 8 | export type MultiSelector = (document: TDocument) => any[]; 9 | 10 | export type Predicate = (document: TDocument, prevDocument?: TDocument, value?: TValue) => boolean; 11 | 12 | /** 13 | * Checks if the selected property in the current models differs from the same property in the previous model. 14 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#propertyChanged 15 | */ 16 | export function propertyChanged(selector: Selector): Predicate; 17 | 18 | /** 19 | * Checks if the selected properties in the current models differ from the same properties in the previous model. 20 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#propertiesChanged 21 | */ 22 | export function propertiesChanged(selector: MultiSelector): Predicate; 23 | 24 | /** 25 | * Checks if the selected values are equal. 26 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#equals 27 | */ 28 | export function equals(first: Selector, second: Selector): Predicate; 29 | 30 | /** 31 | * Checks if all the selected values are true. 32 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#all 33 | */ 34 | export function all(...predicates: Array | TValue>): Predicate; 35 | 36 | /** 37 | * Checks if any of the selected values are true. 38 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#any 39 | */ 40 | export function any(...predicates: Array | TValue>): Predicate; 41 | 42 | /** 43 | * Negates the selected value. 44 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#not 45 | */ 46 | export function not(predicate: Predicate | TValue): Predicate; 47 | 48 | /* Checks if the selected property is a number. 49 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#isNumber 50 | */ 51 | export function isNumber(selector: Selector): Predicate; 52 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/higherOrderRules/field.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import { curry } from "ramda"; 6 | import { contramap, $do } from "@totalsoft/zion"; 7 | import { checkRules } from "../_utils"; 8 | import { findMatchingItem } from "@totalsoft/change-tracking"; 9 | 10 | export const field = curry(function field(key, rule) { 11 | checkRules(rule); 12 | return ( 13 | rule 14 | |> _logFieldPath 15 | |> _filterCurrentProp 16 | |> contramap((model, ctx) => [model[key], _getFieldContext(model, ctx, key)]) 17 | |> mergeParent(key) 18 | ); 19 | }); 20 | 21 | const mergeParent = curry(function mergeParent(field, fieldRule) { 22 | return $do(function* () { 23 | const [model] = yield Rule.ask(); 24 | const fieldValue = yield fieldRule; 25 | 26 | if (model[field] === fieldValue) { 27 | return model; 28 | } 29 | 30 | return Array.isArray(model) 31 | ? Object.assign([], model, { [field]: fieldValue }) 32 | : { ...model, [field]: fieldValue } 33 | }); 34 | }); 35 | 36 | function _logFieldPath(rule) { 37 | return $do(function* () { 38 | const [, fieldContext] = yield Rule.ask(); 39 | const result = yield rule; 40 | if (result !== Object(result)) 41 | _log(fieldContext, `Rule result is ${result} for path ${[...fieldContext.scopePath, ...fieldContext.fieldPath].join(".")}`); 42 | return result; 43 | }); 44 | } 45 | 46 | function _filterCurrentProp(rule) { 47 | return $do(function* () { 48 | const [model, { prevModel }] = yield Rule.ask(); 49 | 50 | if (_isPrimitiveValue(model) && model !== prevModel) { 51 | return model; 52 | } 53 | 54 | return yield rule; 55 | }); 56 | } 57 | 58 | function _isPrimitiveValue(model) { 59 | return (typeof (model) !== "object" && !Array.isArray(model)) || model === null || model === undefined 60 | } 61 | 62 | function _getFieldContext(parentModel, parentContext, key) { 63 | const prevModel = Array.isArray(parentModel) 64 | ? findMatchingItem(parentModel[key], key, parentContext.prevModel) 65 | : parentContext.prevModel && parentContext.prevModel[key] 66 | 67 | return { ...parentContext, fieldPath: [...parentContext.fieldPath, key], prevModel, parentModel, parentContext }; 68 | } 69 | 70 | function _log(context, message) { 71 | if (context.log) { 72 | context.logger.log(message); 73 | } 74 | } 75 | 76 | export default field -------------------------------------------------------------------------------- /packages/change-tracking/src/dirtyInfo/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { F } from "ts-toolbelt"; 5 | /** 6 | * An object that mimics the structure of the tracked model and specifies the "dirty" status of the properties 7 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#dirty-info 8 | */ 9 | export type DirtyInfo = { 10 | [key: string]: boolean | DirtyInfo; 11 | }; 12 | 13 | /** 14 | * Creates a new DirtyInfo object initialized with the value of the "isDirty" parameter. 15 | * If the parameter is not provided it is initialized with "false" 16 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#create 17 | */ 18 | export function create(isDirty?: boolean): DirtyInfo; 19 | 20 | /** 21 | * Updates the given DirtyInfo object with the given value for the specified property path. 22 | * The path can be a dot delimited string or an array 23 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#update 24 | */ 25 | export function update(propertyPath: string | string[], propertyDirtyInfo: boolean, dirtyInfo: DirtyInfo): DirtyInfo; 26 | export function update(propertyPath: string | string[]): F.Curry<(propertyDirtyInfo: boolean, dirtyInfo: DirtyInfo) => DirtyInfo>; 27 | 28 | /** 29 | * Merges two DirtyInfo objects 30 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#merge 31 | */ 32 | export function merge(sourceDirtyInfo: DirtyInfo, targetDirtyInfo: DirtyInfo): DirtyInfo; 33 | export function merge(sourceDirtyInfo: DirtyInfo): (targetDirtyInfo: DirtyInfo) => DirtyInfo; 34 | 35 | /** 36 | * Checks if the specified property is dirty in the given DirtyInfo object 37 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#ispropertydirty 38 | */ 39 | export function isPropertyDirty(propertyPath: string, dirtyInfo: DirtyInfo): boolean; 40 | export function isPropertyDirty(propertyPath: string): (dirtyInfo: DirtyInfo) => boolean; 41 | 42 | /** 43 | * Returns the state of the DirtyInfo object (weather it is dirty or not) 44 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#isdirty 45 | */ 46 | export function isDirty(dirtyInfo: DirtyInfo): boolean; 47 | 48 | /** 49 | * Creates a new DirtyInfo object based on the changes between the model and the previous model. 50 | * It also takes into account the previous dirtyInfo object if specified. 51 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/change-tracking#detectchanges 52 | */ 53 | export function detectChanges(model: TModel, prevModel: TModel, prevDirtyInfo?: DirtyInfo): DirtyInfo; 54 | export function detectChanges(model: TModel): F.Curry<(prevModel: TModel, prevDirtyInfo?: DirtyInfo) => DirtyInfo>; 55 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/predicates/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../rule"; 5 | import * as fl from "fantasy-land"; 6 | import { curry, map, lift, reduce } from "ramda"; 7 | import { $do } from "@totalsoft/zion"; 8 | import { checkRules, variadicApply } from "../_utils"; 9 | 10 | export const Predicate = Rule; 11 | Predicate.of = Rule[fl.of] 12 | 13 | export const propertyChanged = curry(function propertyChanged(selector) { 14 | return $do(function* () { 15 | const [, ctx] = yield Rule.ask(); 16 | 17 | if (ctx.document === null || ctx.document === undefined || ctx.prevDocument === null || ctx.prevDocument === undefined) { 18 | return ctx.document !== ctx.prevDocument; 19 | } 20 | 21 | return selector(ctx.document) !== selector(ctx.prevDocument) 22 | }); 23 | }); 24 | 25 | export const propertiesChanged = curry(function propertyChanged(selector) { 26 | return $do(function* () { 27 | const [, ctx] = yield Rule.ask(); 28 | 29 | if (ctx.document === null || ctx.document === undefined || ctx.prevDocument === null || ctx.prevDocument === undefined) { 30 | return ctx.document !== ctx.prevDocument; 31 | } 32 | 33 | const properties = selector(ctx.document); 34 | const prevProperties = selector(ctx.prevDocument); 35 | 36 | return properties.some((value, index) => value !== prevProperties[index]) 37 | }); 38 | }); 39 | 40 | //export const equals = lift(x => y => x === y) |> ensurePredicateParams 41 | 42 | export const equals = curry(function equals(selector1, selector2) { 43 | return $do(function* () { 44 | const [, ctx] = yield Rule.ask(); 45 | return selector1(ctx.document) === selector2(ctx.document) 46 | }); 47 | }); 48 | 49 | const _and = lift(x => y => x && y); 50 | export const all = variadicApply(function all(...predicates) { 51 | return predicates |> map(ensurePredicate) |> reduce(_and, Predicate.of(true)); 52 | }); 53 | 54 | const _or = lift(x => y => x || y); 55 | export const any = variadicApply(function any(...predicates) { 56 | return predicates |> map(ensurePredicate) |> reduce(_or, Predicate.of(false)); 57 | }); 58 | 59 | export function not(predicate) { 60 | return predicate |> ensurePredicate |> map(x => !x) 61 | } 62 | 63 | export function isNumber(selector) { 64 | return selector |> ensurePredicate |> map(x => !isNaN(x)) 65 | } 66 | 67 | function computed(computation) { 68 | return Predicate((prop, { document, prevDocument }) => computation(document, prevDocument, prop)); 69 | } 70 | 71 | export function ensurePredicate(predicate) { 72 | if (typeof predicate === "boolean") { 73 | return Predicate.of(predicate); 74 | } 75 | if (typeof predicate === "function") { 76 | return computed(predicate); 77 | } 78 | 79 | checkRules(predicate); 80 | 81 | return predicate; 82 | } -------------------------------------------------------------------------------- /packages/change-tracking/src/arrayUtils/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import uniqid from "uniqid" 5 | import { find, reduce, addIndex } from "ramda"; 6 | 7 | const uniqueIdSymbol = Symbol("uid"); 8 | 9 | const reduceIndexed = addIndex(reduce) 10 | 11 | export function ensureArrayUIDsDeep(model) { 12 | if (typeof (model) !== "object" || model === null) 13 | return model; 14 | 15 | model = ensureArrayUIDs(model); 16 | 17 | let newModel = Object.entries(model).reduce( 18 | (acc, [k, v]) => { acc[k] = ensureArrayUIDsDeep(v); return acc; }, 19 | Array.isArray(model) ? Object.assign([], model) : { ...model } 20 | ); 21 | 22 | const hasSameProps = Object.entries(newModel).every(([k, v]) => v === model[k]) 23 | return hasSameProps ? model : newModel; 24 | } 25 | 26 | export function ensureArrayUIDs(array) { 27 | if (!Array.isArray(array)) { 28 | return array; 29 | } 30 | 31 | const newArray = array.map(_ensureUniqueId) 32 | if (uniqueIdSymbol in array) { 33 | newArray[uniqueIdSymbol] = array[uniqueIdSymbol] 34 | } 35 | 36 | const hasSameElements = newArray.every((value, index) => value === array[index]) 37 | return hasSameElements ? array : newArray 38 | } 39 | 40 | export function findMatchingItem(currentItem, currentIndex, otherArray) { 41 | if (!Array.isArray(otherArray)) { 42 | return undefined; 43 | } 44 | 45 | return typeof (currentItem) === "object" && uniqueIdSymbol in currentItem 46 | ? otherArray |> find(prevItem => prevItem[uniqueIdSymbol] === currentItem[uniqueIdSymbol]) 47 | : otherArray[currentIndex]; 48 | } 49 | 50 | export function hasSameItemOrder(firstArray, secondArray) { 51 | if (!Array.isArray(firstArray) || !Array.isArray(secondArray) || firstArray.length !== secondArray.length) { 52 | return false; 53 | } 54 | 55 | return firstArray |> reduceIndexed((acc, firstValue, index) => { 56 | const secondValue = secondArray[index]; 57 | const firstUidOrValue = (firstValue && firstValue[uniqueIdSymbol]) || firstValue; 58 | const secondUidOrValue = (secondValue && secondValue[uniqueIdSymbol]) || secondValue; 59 | return acc && firstUidOrValue === secondUidOrValue 60 | }, true) 61 | } 62 | 63 | export function toUniqueIdMap(array) { 64 | if (!Array.isArray(array) || array.length === 0) { 65 | return {}; 66 | } 67 | 68 | return array |> reduceIndexed((acc, value, index) => { 69 | acc[(value && value[uniqueIdSymbol]) || index] = { value, index } 70 | return acc; 71 | }, {}) 72 | } 73 | 74 | function _ensureUniqueId(item) { 75 | if (typeof (item) === "object" && item[uniqueIdSymbol] === undefined) { 76 | return Array.isArray(item) 77 | ? Object.assign([...item], { [uniqueIdSymbol]: uniqid() }) 78 | : {...item, [uniqueIdSymbol]: uniqid()} 79 | } 80 | return item; 81 | } -------------------------------------------------------------------------------- /packages/pure-validations-react/README.md: -------------------------------------------------------------------------------- 1 | # pure-validations-react 2 | React extensions for pure-validations. 3 | 4 | ## installation 5 | ```javascript 6 | npm install @totalsoft/pure-validations-react 7 | ``` 8 | 9 | ## info 10 | The library provides two hooks: 11 | - useValidation: usage without dirty fields info 12 | - useDirtyFieldValidation: usage with dirty fields info 13 | 14 | ## useValidation hook 15 | ```jsx 16 | const validator = shape({ 17 | name: required, 18 | details: concatFailure( 19 | atLeastOne, 20 | unique(x => x.detailId), 21 | items( 22 | shape({ 23 | detailId: required, 24 | formula: required, 25 | }), 26 | ), 27 | ), 28 | }); 29 | 30 | const SomeComponent = props => { 31 | const [model, setModel] = useState({}); 32 | const [validation, validate] = useValidation(validator); 33 | 34 | const handleSave = () => { 35 | if (!validate(model)) { 36 | return; 37 | } 38 | 39 | actions.save(model); 40 | }; 41 | 42 | return ( 43 | <> 44 | 49 | 50 | 51 | ); 52 | }; 53 | ``` 54 | 55 | 56 | ## useDirtyFieldValidation hook 57 | ```jsx 58 | const validator = shape({ 59 | name: required, 60 | surname: required, 61 | details: concatFailure( 62 | atLeastOne, 63 | unique(x => x.detailId), 64 | items( 65 | shape({ 66 | detailId: required, 67 | formula: required, 68 | }), 69 | ), 70 | ), 71 | }); 72 | 73 | const SomeComponent = props => { 74 | const [model, setModel] = useState({}); 75 | const [dirtyInfo, setDirtyInfo] = useState(di.create); 76 | const [validation, validate] = useDirtyFieldValidation(validator, dirtyInfo); 77 | 78 | const handleNameChange = useCallback( 79 | event => { 80 | setDirtyInfo(di.update('name', true, dirtyInfo)); 81 | setModel({ ...model, name: event.target.value }); 82 | }, 83 | [model, dirtyInfo], 84 | ); 85 | 86 | const handleSurnameChange = useCallback( 87 | event => { 88 | setDirtyInfo(di.update('surname', true, dirtyInfo)); 89 | setModel({ ...model, surname: event.target.value }); 90 | }, 91 | [model, dirtyInfo], 92 | ); 93 | 94 | useEffect(() => { 95 | validate(model, dirtyInfo); 96 | }, [model, dirtyInfo]); 97 | 98 | const handleSave = () => { 99 | if (!validate(model)) { 100 | return; 101 | } 102 | 103 | actions.save(model); 104 | }; 105 | 106 | return ( 107 | <> 108 | 113 | 118 | 119 | 120 | ); 121 | }; 122 | ``` 123 | -------------------------------------------------------------------------------- /scripts/copy-files.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | /* eslint-disable no-console */ 5 | const path = require('path'); 6 | const fse = require('fs-extra'); 7 | const glob = require('glob'); 8 | 9 | const packagePath = process.cwd(); 10 | const buildPath = path.join(packagePath, './build'); 11 | const srcPath = path.join(packagePath, './src'); 12 | 13 | /** 14 | * Puts a package.json into every immediate child directory of rootDir. 15 | * That package.json contains information about esm for bundlers so that imports 16 | * like import Typography from '@material-ui/core/Typography' are tree-shakeable. 17 | * 18 | * It also tests that an this import can be used in typescript by checking 19 | * if an index.d.ts is present at that path. 20 | * 21 | * @param {string} rootDir 22 | */ 23 | async function createModulePackages({ from, to }) { 24 | const directoryPackages = glob.sync('*/index.js', { cwd: from }).map(path.dirname); 25 | 26 | await Promise.all( 27 | directoryPackages.map(async directoryPackage => { 28 | const packageJson = { 29 | sideEffects: false, 30 | module: path.join('../esm', directoryPackage, 'index.js'), 31 | typings: './index.d.ts', 32 | }; 33 | const packageJsonPath = path.join(to, directoryPackage, 'package.json'); 34 | 35 | /*const [typingsExist] =*/ await Promise.all([ 36 | //fse.exists(path.join(to, directoryPackage, 'index.d.ts')), 37 | fse.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)), 38 | ]); 39 | 40 | // if (!typingsExist) { 41 | // throw new Error(`index.d.ts for ${directoryPackage} is missing`); 42 | // } 43 | 44 | return packageJsonPath; 45 | }), 46 | ); 47 | } 48 | 49 | async function typescriptCopy({ from, to }) { 50 | if (!(await fse.exists(to))) { 51 | console.warn(`path ${to} does not exists`); 52 | return []; 53 | } 54 | 55 | const files = glob.sync('**/*.d.ts', { cwd: from }); 56 | const cmds = files.map(file => fse.copy(path.resolve(from, file), path.resolve(to, file))); 57 | return Promise.all(cmds); 58 | } 59 | 60 | async function createPackageFile() { 61 | const packageData = await fse.readFile(path.resolve(packagePath, './package.json'), 'utf8'); 62 | // eslint-disable-next-line no-unused-vars 63 | const { nyc, scripts, devDependencies, workspaces, ...packageDataOther } = JSON.parse( 64 | packageData, 65 | ); 66 | const newPackageData = { 67 | ...packageDataOther, 68 | private: false, 69 | main: './index.js', 70 | module: './esm/index.js', 71 | typings: './index.d.ts' 72 | }; 73 | const targetPath = path.resolve(buildPath, './package.json'); 74 | 75 | await fse.writeFile(targetPath, JSON.stringify(newPackageData, null, 2), 'utf8'); 76 | console.log(`Created package.json in ${targetPath}`); 77 | 78 | return newPackageData; 79 | } 80 | 81 | async function run() { 82 | try { 83 | await createPackageFile(); 84 | 85 | // TypeScript 86 | await typescriptCopy({ from: srcPath, to: buildPath }); 87 | 88 | await createModulePackages({ from: srcPath, to: buildPath }); 89 | 90 | } catch (err) { 91 | console.error(err); 92 | process.exit(1); 93 | } 94 | } 95 | 96 | run(); 97 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/stateLens.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { tagged } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import * as R from "ramda"; 7 | import * as Z from "@totalsoft/zion" 8 | import { identity } from "ramda" 9 | 10 | export const StateLens = tagged("StateLens", ["state", "setState"]); 11 | 12 | /* Profunctor StateLens */ { 13 | StateLens.prototype[fl.promap] = function (get, set) { 14 | const setState = this.setState; 15 | const innerSetState = function (newInnerStateOrUpdate) { 16 | setState(prevState => { 17 | const innerState = get(prevState) 18 | const newInnerState = 19 | typeof newInnerStateOrUpdate === 'function' 20 | ? newInnerStateOrUpdate(innerState) 21 | : newInnerStateOrUpdate 22 | 23 | if (newInnerState == innerState) { 24 | return prevState 25 | } 26 | 27 | return set(newInnerState, prevState) 28 | }) 29 | } 30 | 31 | const innerState = get(this.state) 32 | return StateLens(innerState, innerSetState) 33 | } 34 | } 35 | 36 | /* Functor StateLens */ { 37 | StateLens.prototype[fl.map] = function (f) { 38 | return this[fl.promap](identity, f) 39 | } 40 | } 41 | 42 | // Manual curry workaround 43 | export function set(stateLens, newValue = undefined) { 44 | if (newValue !== undefined) { 45 | return stateLens.setState(newValue) 46 | } else { 47 | return stateLens.setState 48 | } 49 | } 50 | 51 | export function get(stateLens) { 52 | return stateLens.state 53 | } 54 | 55 | export const over = R.curry(function over(proxy, func) { 56 | return set(proxy, prev => func(prev)) 57 | }) 58 | 59 | export const promap = Z.promap; 60 | 61 | export const lmap = Z.lmap; 62 | 63 | export const rmap = Z.rmap; 64 | 65 | export const pipe = R.curry(function pipe(stateLens, otherLens) { 66 | if (typeof (otherLens) !== 'function') { 67 | throw Error("Parameter 'otherLens' is not a Ramda lens") 68 | } 69 | 70 | return stateLens |> Z.promap(R.view(otherLens), R.set(otherLens)); 71 | }) 72 | 73 | export function sequence(stateLens) { 74 | const xs = (stateLens |> get) || [] 75 | if (!Array.isArray(xs)) { 76 | throw new Error(`Cannot sequence lens with value ${xs.toString()}`); 77 | } 78 | const result = xs.map((_, index) => getInnerLens(stateLens, index)) 79 | return result 80 | } 81 | 82 | export function getInnerLens(stateLens, fieldName) { 83 | return stateLens |> Z.promap( 84 | model => model && model[fieldName], 85 | (fieldValue, model) => _immutableAssign(model, fieldName, fieldValue) 86 | ) 87 | } 88 | 89 | function _immutableAssign(obj, prop, value) { 90 | if (obj == null || obj == undefined) { 91 | if (Number.isInteger(Number(prop))) { 92 | obj = []; 93 | } 94 | else { 95 | obj = {}; 96 | } 97 | } 98 | else if (obj[prop] === value) { 99 | return obj; 100 | } 101 | 102 | return Array.isArray(obj) 103 | ? Object.assign([], obj, { [prop]: value }) 104 | : { ...obj, [prop]: value } 105 | } 106 | 107 | export default StateLens 108 | -------------------------------------------------------------------------------- /packages/rules-algebra-react/README.md: -------------------------------------------------------------------------------- 1 | # rules-algebra-react 2 | React extensions for rules-algebra. 3 | 4 | 5 | ## installation 6 | ```javascript 7 | npm install @totalsoft/rules-algebra-react 8 | ``` 9 | 10 | ## info 11 | The library provides three hooks: 12 | - **useRules** - applies the rule engine after updating a value at the given property path 13 | - **useRulesLens** - applies the rule engine after updating the model through a profunctor lens 14 | 15 | ## useRules hook 16 | React hook that applies the business rules and keeps track of user modified values (dirty field info). 17 | * Arguments: 18 | 1. The rules object (see rules-algebra library) 19 | 2. The initial value of the model 20 | 3. Settings (optional) 21 | 4. Dependencies (optional) 22 | * Return values: 23 | 1. A stateful rule application result (changed model) 24 | 2. A dirty info object to track the model changes 25 | 3. A function that sets a value for the given property path 26 | 4. A function that resets the rule engine state. 27 | 28 | Usage example: 29 | 30 | ```jsx 31 | import { useRules } from "@totalsoft/rules-algebra-react"; 32 | 33 | const rules = shape({ 34 | fullName: computed(doc => doc.firstName + doc.lastName) 35 | }); 36 | 37 | const SomeComponent = props => { 38 | const [person, dirtyInfo, updateProperty, reset] = useRules(rules, {}); 39 | 40 | const onPropertyChange = propPath => value => { 41 | updateProperty(propPath, value) 42 | }; 43 | 44 | return ( 45 | <> 46 | Full name: {person.fullName} 47 | 51 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | ``` 61 | Note: The `reset` function clears the dirty info object. If an object is passed as parameter, the model is set to that object, otherwise the current model is kept unchanged. 62 | 63 | 64 | ## useRulesLens hook 65 | React hook that applies the business rules and keeps track of user modified values (dirty field info). 66 | * Arguments: 67 | 1. The rules object (see rules-algebra library) 68 | 2. The initial value of the model 69 | 3. Settings (optional) 70 | 4. Dependencies (optional) 71 | * Return values: 72 | 1. A stateful profunctor lens with the rule application result 73 | 2. A dirty info object 74 | 3. A function that resets the rule engine state. It receives an optional new model. 75 | 76 | ```jsx 77 | import { useRulesLens, set, get } from "@totalsoft/rules-algebra-react"; 78 | 79 | const rules = shape({ 80 | fullName: computed(doc => doc.firstName + doc.lastName) 81 | }); 82 | 83 | const SomeComponent = props => { 84 | const [personLens, dirtyInfo, reset] = useRulesLens(rules, {}); 85 | return ( 86 | <> 87 | Full name: {personLens.fullName |> get} 88 | get} 90 | onChange={personLens.firstName |> set |> onTextBoxChange} 91 | /> 92 | get} 94 | onChange={personLens.lastName |> set |> onTextBoxChange} 95 | /> 96 | 97 | 98 | ); 99 | }; 100 | ``` 101 | 102 | Note: The `reset` function clears the dirty info object. If an object is passed as parameter, the model is set to that object, otherwise the current model is kept unchanged. 103 | 104 | [Read more about lens operations](../react-state-lens/src/lensProxy/README.md) -------------------------------------------------------------------------------- /packages/rules-algebra/src/primitiveRules/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Rule } from "../index"; 5 | 6 | export type Computation = (document: TDocument, prevDocument?: TDocument, value?: TValue) => TValue; 7 | 8 | /** 9 | * A rule that does not change the value. Used for composition with other rules. 10 | */ 11 | export const unchanged: Rule; 12 | 13 | /** 14 | * A rule that returns the spciffied value regardless of the model. 15 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#constant 16 | */ 17 | export function constant(value: TValue): Rule; 18 | 19 | /** 20 | * A rule that returns a value computed based on the "document" in scope and its previous value. 21 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#computed 22 | */ 23 | export function computed(computation: Computation): Rule; 24 | 25 | /** 26 | * A rule that returns the minimum of two properties or values. 27 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#min 28 | */ 29 | export function min(first: TValue | Computation, second: TValue | Computation): Rule; 30 | 31 | /** 32 | * A rule that returns the minimum of two properties or values. 33 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#min 34 | */ 35 | export function min(first: TValue | Computation): (second: TValue | Computation) => Rule; 36 | 37 | /** 38 | * A rule that returns the maximum of two properties or values. 39 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#max 40 | */ 41 | export function max(first: TValue | Computation, second: TValue | Computation): Rule; 42 | 43 | /** 44 | * A rule that returns the maximum of two properties or values. 45 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#max 46 | */ 47 | export function max(first: TValue | Computation): (second: TValue | Computation) => Rule; 48 | 49 | /** 50 | * A rule that returns the sum between two prperties or values. 51 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#sum 52 | */ 53 | export function sum(first: TValue | Computation, second: TValue | Computation): Rule; 54 | 55 | /** 56 | * A rule that returns the sum between two prperties or values. 57 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#sum 58 | */ 59 | export function sum(first: TValue | Computation): (second: TValue | Computation) => Rule; 60 | 61 | /** 62 | * A rule that returns the minimum between the current value and the argument. 63 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#minimumValue 64 | */ 65 | export function minimumValue(other: TValue | Computation): Rule; 66 | 67 | /** 68 | * A rule that returns the maximum between the current value and the argument. 69 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#maximumValue 70 | */ 71 | export function maximumValue(other: TValue | Computation): Rule; 72 | 73 | /** 74 | * A rule that returns a string produced according to the provided format. 75 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/rules-algebra#sprintf 76 | */ 77 | export function sprintf(format: string): Rule; 78 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/lensProxy/index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | export type StateLens = { state: TState, setState: (prevState: TState) => TState }; 5 | 6 | /** 7 | * Provides access to the inner profunctor state 8 | */ 9 | export function eject(lens: LensProxy): StateLens; 10 | 11 | /** 12 | * Sets a new value in the profunctor state 13 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#set 14 | */ 15 | export function set(lens: LensProxy, newValue: any): void; 16 | 17 | /** 18 | * Sets a new value in the profunctor state 19 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#set 20 | */ 21 | export function set(lens: LensProxy): (newValue: any) => void; 22 | 23 | /** 24 | * Read the value from the profunctor state 25 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#get 26 | */ 27 | export function get(lens: LensProxy): TValue; 28 | 29 | /** 30 | * Sets the value from the profunctor state using an updater fn 31 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#over 32 | */ 33 | export function over(lens: LensProxy, func: (value: TValue) => any): void; 34 | 35 | /** 36 | * Sets the value from the profunctor state using an updater fn 37 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#over 38 | */ 39 | export function over(lens: LensProxy): (func: (value: TValue) => any) => void; 40 | 41 | /** 42 | * Map both the getter and the setter to retrieve another lens 43 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#promap 44 | */ 45 | export function promap( 46 | get: (value: TValue) => TResult, 47 | set: (newValue: TResult, oldState: TValue) => void, 48 | lens: LensProxy 49 | ): LensProxy; 50 | 51 | /** 52 | * Maps only the getter and returns another lens 53 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#lmap 54 | */ 55 | export function lmap(get: (value: TValue) => TResult, lens: LensProxy): LensProxy; 56 | 57 | /** 58 | * Maps only the setter and returns another lens 59 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#rmap 60 | */ 61 | export function rmap(set: (newValue: TResult, oldState: TValue) => void, lens: LensProxy): LensProxy; 62 | 63 | /** 64 | * Transforms a lens of array into an array of lenses. 65 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#sequence 66 | */ 67 | export function sequence(lens: LensProxy): Array>; 68 | 69 | /** 70 | * Pipes a lens to a Ramda lens. Both the getters and setters are piped. 71 | * @see https://github.com/osstotalsoft/jsbb/tree/master/packages/react-state-lens/src/lensProxy/README.md#pipe 72 | */ 73 | export function pipe(lens: LensProxy, otherLens: any): LensProxy; 74 | 75 | /** 76 | * Creates a LensProxy over an existing lens 77 | */ 78 | export function LensProxy(lens: StateLens): LensProxy; 79 | 80 | /** 81 | * Creates a StateLens and a proxy over it 82 | */ 83 | export function StateLensProxy(state: TState, setState: (prevState: TState) => TState): LensProxy; 84 | 85 | export type Proxy = { 86 | [k in keyof T]: T[k]; 87 | }; 88 | 89 | export type LensProxy = Proxy>; 90 | -------------------------------------------------------------------------------- /packages/change-tracking/README.md: -------------------------------------------------------------------------------- 1 | # change-tracking 2 | Lightweight change tracking library for models including objects and arrays. 3 | 4 | ## Installation 5 | ```javascript 6 | npm install @totalsoft/change-tracking 7 | ``` 8 | 9 | ## Usage 10 | ```javascript 11 | import { create, update, isPropertyDirty } from '@totalsoft/change-tracking'; 12 | 13 | let dirtyInfo = create() 14 | dirtyInfo = update("person.name", true, dirtyInfo) 15 | let isDirtyName = isPropertyDirty("person.name") 16 | ``` 17 | 18 | ## Concepts 19 | 20 | ### Dirty Info 21 | It is an object that mimics the structure of the tracked model and specifies the "dirty" status of the properties 22 | 23 | The following operations are available to manipulate the DirtyInfo data: 24 | 25 | #### create 26 | Creates a new DirtyInfo object initialized with the value of the "isDirty" parameter. 27 | 28 | If the parameter is not provided it is initialized with "false" 29 | ```javascript 30 | let dirtyInfo1 = create() 31 | let dirtyInfo2 = create(true) 32 | ``` 33 | #### update 34 | Updates the given DirtyInfo object with the given value for the specified property path. 35 | 36 | The path can be a dot delimited string or an array 37 | ```javascript 38 | let dirtyInfo1 = update("person.name", true, dirtyInfo) 39 | let dirtyInfo2 = update(["person","name"], false, dirtyInfo) 40 | ``` 41 | #### merge 42 | Merges two DirtyInfo objects 43 | ```javascript 44 | let dirtyInfo = merge(dirtyInfo1, dirtyInfo2) 45 | ``` 46 | #### isPropertyDirty 47 | Checks if the specified property is dirty in the given DirtyInfo object 48 | ```javascript 49 | let isDirtyName = isPropertyDirty("person.name", dirtyInfo) 50 | ``` 51 | 52 | #### isDirty 53 | Returns the state of the DirtyInfo object (weather it is dirty or not) 54 | ```javascript 55 | let isDirtyPerson = isDirty(dirtyInfo) 56 | ``` 57 | 58 | #### detectChanges 59 | Creates a new DirtyInfo object based on the changes between the model and the previous model. 60 | 61 | It also takes into account the previous dirtyInfo object if specified. 62 | ```javascript 63 | let newDirtyInfo = detectChanges(model, prevModel, prevDirtyInfo) 64 | ``` 65 | 66 | ### Array items unique ids 67 | Unique identifiers for object items in arrays are useful to keep track of array items in the following operations: 68 | - Inserts 69 | - Deletions 70 | - Reordering 71 | 72 | Change tracking of array items cannot be performed based on index alone. A unique identifier is essentintial 73 | to determine if an item was changed or just moved in the array. 74 | 75 | The following methods are available: 76 | #### ensureArrayUIDs 77 | Ensures unique identifiers for object items in the given array. 78 | 79 | It returns the same array as the modeinputl but it attaches unique identifiers to items. 80 | Only items of type "object" will have identifiers added. 81 | ```javascript 82 | import { ensureArrayUIDs } from '@totalsoft/change-tracking' 83 | 84 | let persons = [{name: "John", surname:"Doe"}, {name: "Bob", surname:"Smith"}] 85 | let newPersons = ensureArrayUIDs(persons) 86 | ``` 87 | 88 | #### ensureArrayUIDsDeep 89 | Ensures unique identifiers for object items in arrays. 90 | 91 | The received model can be an object that contains arrays in the nesting hierarchy 92 | 93 | It returns the same object hierarcy as the model but it attaches unique identifiers to array items. 94 | Only items of type "object" will have identifiers added. 95 | ```javascript 96 | import { ensureArrayUIDsDeep } from '@totalsoft/change-tracking' 97 | 98 | let model = { persons: [{name: "John", surname:"Doe"}, {name: "Bob", surname:"Smith"}] } 99 | let newModel = ensureArrayUIDsDeep(model) 100 | ``` 101 | 102 | #### toUniqueIdMap 103 | Transforms an array with object items that have uids to a map where uids are keys. 104 | 105 | #### findMatchingItem 106 | Gets the item with the same uid if it has one or the item at the same index otherwise. 107 | 108 | #### hasSameItemOrder 109 | Checks if the items of the provided arrays have the same order based on the uid or value. -------------------------------------------------------------------------------- /packages/zion/src/data/list.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { taggedSum } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | import { equals, ap, lift, concat } from "ramda"; 7 | 8 | //- List is basically the same as `Array`, though much easier for our 9 | //- examples. If one of the methods in Prototype/Array looks 10 | //- frightening, take a look at the same method here: all the `Array` 11 | //- methods act in the exact same way, but `List` is much clearer! 12 | 13 | const List = taggedSum("List", { 14 | Cons: ["head", "tail"], 15 | Nil: [] 16 | }); 17 | 18 | const { Cons, Nil } = List; 19 | 20 | //- Stack-safe List-to-Array natural transformation. 21 | //+ toArray :: List a ~> Array a 22 | List.toArray = function(list) { 23 | const result = []; 24 | let start = list; 25 | 26 | while (Cons.is(start)) { 27 | result.push(start.head); 28 | start = start.tail; 29 | } 30 | 31 | return result; 32 | }; 33 | 34 | //- NOT!!!! Stack-safe List-to-Array natural transformation. 35 | //+ toArray :: List a ~> Array a 36 | List.fromArray = function(arr) { 37 | if (arr.length == 0) { 38 | return Nil; 39 | } 40 | const [head, ...tail] = arr; 41 | return Cons(head, List.fromArray(tail)); 42 | }; 43 | 44 | /* Setoid a => Setoid (List a) */ { 45 | List.prototype[fl.equals] = function(that) { 46 | return this.cata({ 47 | Cons: (head, tail) => { 48 | return equals(head, that.head) && tail[fl.equals](that.tail); 49 | }, 50 | 51 | Nil: () => { 52 | return that.is(Nil); 53 | } 54 | }); 55 | }; 56 | } 57 | 58 | /* Ord a => Ord (List a) */ { 59 | List.prototype[fl.lte] = function(that) { 60 | return this.cata({ 61 | Cons: (head, tail) => head[fl.lte](that.head) || tail[fl.lte](that.tail), 62 | 63 | Nil: () => true 64 | }); 65 | }; 66 | } 67 | 68 | /* Semigroup List */ { 69 | List.prototype[fl.concat] = function(that) { 70 | return this.cata({ 71 | Cons: (head, tail) => Cons(head, tail[fl.concat](that)), 72 | Nil: () => that 73 | }); 74 | }; 75 | } 76 | 77 | /* Monoid List */ { 78 | List[fl.empty] = () => Nil; 79 | } 80 | 81 | /* Functor List */ { 82 | List.prototype[fl.map] = function(f) { 83 | return this.cata({ 84 | Cons: (head, tail) => Cons(f(head), tail[fl.map](f)), 85 | 86 | Nil: () => Nil 87 | }); 88 | }; 89 | } 90 | 91 | /* Apply List */ { 92 | List.prototype[fl.ap] = function(fn) { 93 | return this.cata({ 94 | Cons: (head, tail) => concat(head |> ap(fn), tail[fl.ap](fn)), 95 | 96 | Nil: () => Nil 97 | }); 98 | }; 99 | } 100 | 101 | /* Applicative List */ { 102 | List[fl.of] = x => Cons(x, Nil); 103 | } 104 | 105 | /* Alt List */ { 106 | List.prototype[fl.alt] = List.prototype[fl.concat]; 107 | } 108 | 109 | /* Plus List */ { 110 | List.prototype[fl.zero] = List.prototype[fl.empty]; 111 | } 112 | 113 | // /* Alternative List */ { 114 | // } 115 | 116 | /* Foldable List */ { 117 | List.prototype[fl.reduce] = function(f, acc) { 118 | return this.cata({ 119 | Cons: (head, tail) => tail[fl.reduce](f, f(acc, head)), 120 | Nil: () => acc 121 | }); 122 | }; 123 | } 124 | 125 | /* Traversable List */ { 126 | List.prototype[fl.traverse] = function(T, f) { 127 | return this.cata({ 128 | Cons: (head, tail) => lift(x => y => Cons(x, y), f(head), tail[fl.traverse](T, f)), 129 | 130 | Nil: () => T.of(Nil) 131 | }); 132 | }; 133 | } 134 | 135 | /* Chain List */ { 136 | List.prototype[fl.chain] = function(f) { 137 | return this[fl.map](f)[fl.reduce](concat, Nil); 138 | }; 139 | } 140 | 141 | /* Trainwreck List */ { 142 | List.trainwreck = function(f, init) { 143 | return this.toArray() 144 | .trainwreck(f, init) 145 | .toList(); 146 | }; 147 | } 148 | 149 | // /* ChainRec List */ { 150 | // List[fl.chainRec] = function(f, init) { 151 | // return this.toArray()[fl.chainRec](f, init) 152 | // .toList(); 153 | // }; 154 | // } 155 | 156 | /* Extend List */ { 157 | List.prototype[fl.extend] = function(f) { 158 | return this.cata({ 159 | Just: (head, tail) => Cons(f(this), tail[fl.extend](f)), 160 | Nothing: () => Nil 161 | }); 162 | }; 163 | } 164 | 165 | export default List; 166 | -------------------------------------------------------------------------------- /packages/rules-algebra/src/predicates/__tests__/predicates.tests.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { not, equals, isNumber, all, Predicate, propertiesChanged, propertyChanged } from ".."; 5 | import { Rule } from "../../rule"; 6 | 7 | describe("predicates:", () => { 8 | it("not with reader predicate", () => { 9 | // Arrange 10 | const model = 6; 11 | const predicate = not(Rule(_ => true)); 12 | 13 | // Act 14 | const newModel = predicate.runRule(model, {}); 15 | 16 | // Assert 17 | expect(newModel).toBe(false); 18 | }); 19 | 20 | it("not with predicate", () => { 21 | // Arrange 22 | const model = 6; 23 | const predicate = not(() => true); 24 | 25 | // Act 26 | const newModel = predicate.runRule(model, {}); 27 | 28 | // Assert 29 | expect(newModel).toBe(false); 30 | }); 31 | 32 | it("not with constant value", () => { 33 | // Arrange 34 | const model = 6; 35 | const predicate = not(true); 36 | 37 | // Act 38 | const newModel = predicate.runRule(model, {}); 39 | 40 | // Assert 41 | expect(newModel).toBe(false); 42 | }); 43 | 44 | it("equals", () => { 45 | // Arrange 46 | const model = -1; 47 | const predicate = equals(doc => doc.a + 1, _ => 7); 48 | 49 | // Act 50 | const newModel = predicate.runRule(model, { document: { a: 6 } }); 51 | 52 | // Assert 53 | expect(newModel).toBe(true); 54 | }); 55 | 56 | it("isNumber", () => { 57 | // Arrange 58 | const model = -1; 59 | const predicate = isNumber(doc => doc.a); 60 | 61 | // Act 62 | const newModel = predicate.runRule(model, { document: { a: 6 } }); 63 | 64 | // Assert 65 | expect(newModel).toBe(true); 66 | }); 67 | 68 | it("all with reader predicates should return true", () => { 69 | // Arrange 70 | const model = 6; 71 | const predicate = all([Predicate(_ => true), Predicate(_ => true)]); 72 | 73 | // Act 74 | const newModel = predicate.runRule(model, {}); 75 | 76 | // Assert 77 | expect(newModel).toBe(true); 78 | }); 79 | 80 | it("all with predicates should return true", () => { 81 | // Arrange 82 | const model = 6; 83 | const predicate = all([_ => true, _ => true]); 84 | 85 | // Act 86 | const newModel = predicate.runRule(model, {}); 87 | 88 | // Assert 89 | expect(newModel).toBe(true); 90 | }); 91 | 92 | it("all with predicates should return false", () => { 93 | // Arrange 94 | const model = 6; 95 | const predicate = all([_ => true, _ => false]); 96 | 97 | // Act 98 | const newModel = predicate.runRule(model, {}); 99 | 100 | // Assert 101 | expect(newModel).toBe(false); 102 | }); 103 | 104 | it("propertyChanged should return true", () => { 105 | // Arrange 106 | const model = 6; 107 | const predicate = propertyChanged(doc => doc.property); 108 | 109 | // Act 110 | const newModel = predicate.runRule(model, { document: { property: 1 }, prevDocument: { property: 2 } }); 111 | 112 | // Assert 113 | expect(newModel).toBe(true); 114 | }); 115 | 116 | it("propertyChanged should return false", () => { 117 | // Arrange 118 | const model = 6; 119 | const predicate = propertyChanged(doc => doc.property); 120 | 121 | // Act 122 | const newModel = predicate.runRule(model, { document: { property: 1 }, prevDocument: { property: 1 } }); 123 | 124 | // Assert 125 | expect(newModel).toBe(false); 126 | }); 127 | 128 | it("propertiesChanged should return true", () => { 129 | // Arrange 130 | const model = 6; 131 | const predicate = propertiesChanged(doc => [doc.property1, doc.property2]); 132 | 133 | // Act 134 | const newModel = predicate.runRule(model, { document: { property1: 1, property2: 1 }, prevDocument: { property: 2, property2: 1 } }); 135 | 136 | // Assert 137 | expect(newModel).toBe(true); 138 | }); 139 | 140 | it("propertiesChanged should return false", () => { 141 | // Arrange 142 | const model = 6; 143 | const predicate = propertiesChanged(doc => [doc.property1, doc.property2]); 144 | 145 | // Act 146 | const newModel = predicate.runRule(model, { document: { property1: 1, property2: 1 }, prevDocument: { property: 1, property2: 1 } }); 147 | 148 | // Assert 149 | expect(newModel).toBe(true); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/lensProxy/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { curry } from "ramda"; 5 | import * as L from "../stateLens" 6 | import * as fl from "fantasy-land"; 7 | import * as Z from "@totalsoft/zion" 8 | 9 | const cacheSymbol = Symbol("cache") 10 | const ignoredPrefixes = ["@@", "$$"] 11 | 12 | const handler = { 13 | ownKeys(target) { 14 | return [...Reflect.ownKeys(target), "__target"] 15 | }, 16 | getOwnPropertyDescriptor(target, prop) { 17 | if (prop === "__target") { 18 | return { configurable: true, enumerable: true }; 19 | } 20 | 21 | return Reflect.getOwnPropertyDescriptor(target, prop) 22 | }, 23 | get: function (target, prop) { 24 | switch (prop) { 25 | case "__target": { 26 | return target; 27 | } 28 | case "toJSON": { 29 | return function () { return target; } 30 | } 31 | case fl.promap: { 32 | return function (get, set) { 33 | return target[fl.promap](get, set) |> LensProxy 34 | } 35 | } 36 | case fl.map: { 37 | return function (func) { return target[fl.map](func) |> LensProxy } 38 | } 39 | default: { 40 | if (isIgnoredProp(prop)) { 41 | return target[prop]; 42 | } 43 | if (!target[cacheSymbol]) { 44 | target[cacheSymbol] = {}; 45 | } 46 | if (prop in target[cacheSymbol]) { 47 | return target[cacheSymbol][prop]; 48 | } 49 | const propLensProxy = L.getInnerLens(target, prop) |> LensProxy; 50 | 51 | target[cacheSymbol][prop] = propLensProxy; // cache value 52 | return propLensProxy; 53 | } 54 | } 55 | } 56 | } 57 | 58 | function isIgnoredProp(name) { 59 | if (typeof name === "string" && ignoredPrefixes.some(prefix => name.startsWith(prefix))) { 60 | return true; 61 | } 62 | if (typeof name === "symbol") { 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | export function eject(proxy) { 70 | return proxy["__target"] 71 | } 72 | 73 | export function reuseCache(sourceProxy, targetProxy) { 74 | const source = eject(sourceProxy) 75 | const target = eject(targetProxy) 76 | 77 | for (const prop in source.state) { 78 | if (Object.hasOwn(source.state, prop) && cacheSymbol in source && prop in source[cacheSymbol] && target.state && Object.hasOwn(target.state, prop)) { 79 | if (source.state[prop] === target.state[prop]) { 80 | if (!target[cacheSymbol]) { 81 | target[cacheSymbol] = {}; 82 | } 83 | target[cacheSymbol][prop] = source[cacheSymbol][prop] 84 | } 85 | else { 86 | reuseCache(sourceProxy[prop], targetProxy[prop]) 87 | } 88 | } 89 | } 90 | } 91 | 92 | // Manual curry workaround 93 | export function set(proxy, newValue = undefined) { 94 | return L.set(eject(proxy), newValue) 95 | } 96 | 97 | export function get(proxy) { 98 | return L.get(eject(proxy)) 99 | } 100 | 101 | export const over = curry(function over(proxy, func) { 102 | return L.over(eject(proxy), func) 103 | }) 104 | 105 | export const promap = Z.promap; 106 | 107 | export const lmap = Z.lmap; 108 | 109 | export const rmap = Z.rmap; 110 | 111 | export function sequence(proxy) { 112 | const xs = (proxy |> get) || [] 113 | if (!Array.isArray(xs)) { 114 | throw new Error(`Cannot sequence lens with value ${xs.toString()}`); 115 | } 116 | 117 | return xs.map((_, index) => proxy[index]); 118 | } 119 | 120 | export const pipe = curry(function (proxy, otherLens) { 121 | const lens = eject(proxy) 122 | const newLens = L.pipe(lens, otherLens) 123 | return (newLens === lens) ? proxy : LensProxy(newLens) 124 | }) 125 | 126 | export function LensProxy(stateLens) { 127 | return new Proxy(stateLens, handler) 128 | } 129 | 130 | export function StateLensProxy(state, setState) { 131 | return new L.StateLens(state, setState) |> LensProxy 132 | } 133 | -------------------------------------------------------------------------------- /packages/zion/src/data/maybe.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { taggedSum } from "daggy"; 5 | import * as fl from "fantasy-land"; 6 | 7 | import { Loop, Done } from "./step"; 8 | import { equals, lte, map, concat } from "ramda"; 9 | 10 | //- The `Maybe` type is used to model "nullable" values in a type- 11 | //- safe way. Instead of returning a value OR null, return a Just 12 | //- value OR Nothing. Why? Because these two values have **the same 13 | //- interface**, which means that you can treat them in the same way. 14 | //- When you want to get a value _out_, you must also deal with the 15 | //- Nothing case, which prevents you from forgetting about it. 16 | 17 | const Maybe = taggedSum("Maybe", { Just: [Symbol("value")], Nothing: [] }); 18 | 19 | const { Just, Nothing } = Maybe; 20 | 21 | /* Setoid a => Setoid (Maybe a) */ { 22 | Maybe.prototype[fl.equals] = function(that) { 23 | return this.cata({ 24 | Just: x => 25 | that.cata({ 26 | Just: equals(x), 27 | Nothing: () => false 28 | }), 29 | 30 | Nothing: () => Nothing.is(that) 31 | }); 32 | }; 33 | } 34 | 35 | /* Ord a => Ord (Maybe a) */ { 36 | Maybe.prototype[fl.lte] = function(that) { 37 | return this.cata({ 38 | Just: x => 39 | that.cata({ 40 | Just: lte(x), 41 | Nothing: () => false 42 | }), 43 | 44 | Nothing: () => true 45 | }); 46 | }; 47 | } 48 | 49 | /* Semigroup a => Semigroup (Maybe a) */ { 50 | Maybe.prototype[fl.concat] = function(that) { 51 | // return this.cata({ 52 | // Just: x => that |> map(concat(x)), 53 | // Nothing: () => that 54 | // }) 55 | return this.cata({ 56 | Just: x => 57 | that.cata({ 58 | Just: y => { 59 | const concatValue = concat(x, y); 60 | return concatValue !== x ? Just(concatValue) : this; 61 | }, 62 | Nothing: () => this 63 | }), 64 | Nothing: () => that 65 | }); 66 | }; 67 | } 68 | 69 | /* Monoid a => Monoid (Maybe a) */ { 70 | Maybe[fl.empty] = () => Nothing; 71 | } 72 | 73 | /* Functor Maybe */ { 74 | Maybe.prototype[fl.map] = function(f) { 75 | return this.cata({ 76 | Just: x => Just(f(x)), 77 | Nothing: () => this 78 | }); 79 | }; 80 | } 81 | 82 | /* Apply Maybe */ { 83 | Maybe.prototype[fl.ap] = function(fn) { 84 | return this.cata({ 85 | Just: x => fn |> map(f => f(x)), 86 | Nothing: () => this 87 | }); 88 | }; 89 | } 90 | 91 | /* Applicative Maybe */ { 92 | Maybe[fl.of] = Just; 93 | } 94 | 95 | /* Alt Maybe */ { 96 | Maybe.prototype[fl.alt] = function(that) { 97 | return this.cata({ 98 | Just: _ => this, 99 | Nothing: () => that 100 | }); 101 | }; 102 | } 103 | 104 | /* Plus Maybe */ { 105 | Maybe[fl.zero] = Nothing; 106 | } 107 | 108 | /* Alternative Maybe */ 109 | 110 | /* Foldable Maybe */ { 111 | Maybe.prototype[fl.reduce] = function(f, acc) { 112 | return this.cata({ 113 | Just: x => f(acc, x), 114 | Nothing: acc 115 | }); 116 | }; 117 | } 118 | 119 | /* Traversable Maybe */ { 120 | Maybe.prototype[fl.traverse] = function(T, f) { 121 | return this.cata({ 122 | Just: x => f(x) |> map(Just), 123 | Nothing: () => T[fl.of](this) 124 | }); 125 | }; 126 | } 127 | 128 | /* Chain Maybe */ { 129 | Maybe.prototype[fl.chain] = function(f) { 130 | return this.cata({ 131 | Just: f, 132 | Nothing: () => this 133 | }); 134 | }; 135 | } 136 | 137 | /* Trainwreck Maybe */ { 138 | Maybe.trainwreck = function(f, init) { 139 | let acc = Loop(Just(init)); 140 | 141 | do { 142 | acc.loop instanceof Nothing ? (acc = Done(Nothing)) : (acc = f(acc.loop.value)); 143 | } while (acc instanceof Loop); 144 | 145 | return acc.result; 146 | }; 147 | } 148 | 149 | /* ChainRec Maybe */ { 150 | Maybe[fl.chainRec] = function(f, init) { 151 | let acc = Loop(Just(init)); 152 | 153 | do { 154 | acc.loop instanceof Nothing ? (acc = Done(acc.loop)) : (acc = f(Done, Loop, acc.loop.value)); 155 | } while (acc instanceof Loop); 156 | 157 | return acc.result; 158 | }; 159 | } 160 | 161 | // eslint-disable-next-line no-empty 162 | /* Monad Maybe */ { 163 | } 164 | 165 | /* Extend Maybe */ { 166 | Maybe[fl.extend] = function(f) { 167 | return this.cata({ 168 | Just: _ => Just(f(this)), 169 | Nothing: () => Nothing 170 | }); 171 | }; 172 | } 173 | 174 | export default Maybe; 175 | -------------------------------------------------------------------------------- /packages/zion/README.md: -------------------------------------------------------------------------------- 1 | # Zion 2 | ##### Fantasy Land compliant algebraic data types 3 | 4 | 5 | ## General 6 | Zion is a functional programming library inspired by [Haskell][] and [Fantasy Land][]. 7 | 8 | It provides [Haskell][]-like algebraic data-types and polymorphic functions against [Fantasy Land][] algebraic structures. 9 | 10 | ## Instalation & usage 11 | ```javascript 12 | npm install @totalsoft/zion 13 | ``` 14 | 15 | ```javascript 16 | import List from "@totalsoft/zion/data/list"; 17 | import { $do } from "@totalsoft/zion"; 18 | ``` 19 | 20 | ## Algebraic data types 21 | ### Maybe 22 | ```haskell 23 | data Maybe a = Nothing | Just a 24 | ``` 25 | Instance of: 26 | - [Setoid][] 27 | - [Ord][] 28 | - [Semigroup][] 29 | - [Monoid][] 30 | - [Functor][] 31 | - [Apply][] 32 | - [Applicative][] 33 | - [Alt][] 34 | - [Plus][] 35 | - [Foldable][] 36 | - [Traversable][] 37 | - [Chain][] 38 | - [ChainRec][] 39 | 40 | ### List 41 | ```haskell 42 | data List a = Cons a (List a) | Nil 43 | ``` 44 | Instance of: 45 | - [Setoid][] 46 | - [Ord][] 47 | - [Semigroup][] 48 | - [Monoid][] 49 | - [Functor][] 50 | - [Apply][] 51 | - [Applicative][] 52 | - [Alt][] 53 | - [Plus][] 54 | - [Foldable][] 55 | - [Traversable][] 56 | - [Chain][] 57 | - [Extend][] 58 | 59 | 60 | ### Map 61 | ```haskell 62 | data Map k a 63 | ``` 64 | Instance of: 65 | - [Setoid][] 66 | - [Semigroup][] 67 | - [Monoid][] 68 | - [Functor][] 69 | - [Foldable][] 70 | 71 | 72 | ### KeyValuePair 73 | ```haskell 74 | data KeyValuePair k a 75 | ``` 76 | Instance of: 77 | - [Functor][] 78 | 79 | 80 | ### Reader 81 | ```haskell 82 | data Reader r a 83 | ``` 84 | Instance of: 85 | - [Functor][] 86 | - [Apply][] 87 | - [Chain][] 88 | - [Contravariant][] 89 | - [Semigroup][] 90 | 91 | 92 | ### Step 93 | ```haskell 94 | data Step b a 95 | ``` 96 | Instance of: 97 | - [Setoid][] 98 | - [Ord][] 99 | - [Functor][] 100 | - [Bifunctor][] 101 | 102 | 103 | ## Polymorphic functions 104 | - pure :: Applicative f => TypeRep f -> a -> f a 105 | - contramap :: Contravariant f => (b -> a) -> f a -> f b 106 | - fold :: Monoid m, Foldable f => (a -> m a) -> f a -> m a 107 | 108 | ## Do notation 109 | ```javascript 110 | const monadSum = (monadX, monadY) => 111 | $do(function*() { 112 | const x = yield monadX; 113 | const y = yield monadY; 114 | return x + y; 115 | }) 116 | 117 | expect(monadSum(Just(5), Just(6))).toStrictEqual(Just(11)) 118 | expect(monadSum(Just(5), Nothing)).toStrictEqual(Nothing) 119 | 120 | expect(monadSum(List.fromArray([1]), List.fromArray([2]))).toStrictEqual(List.fromArray([1+2])) 121 | expect(monadSum(List.fromArray([1, 2]), List.fromArray([3]))).toStrictEqual(List.fromArray([1 + 3, 2 + 3])) 122 | 123 | expect(monadSum([1, 2], [3])).toStrictEqual([1 + 3, 2 + 3]) 124 | expect(monadSum([1], [2])).toStrictEqual([1 + 2]) 125 | 126 | expect(monadSum(x => x + 1, x => x - 1)(7)).toBe(7 + 1 + 7 - 1) 127 | expect(monadSum(x => x * 2, x => x - 1)(5)).toBe(5 * 2 + 5 - 1) 128 | ``` 129 | 130 | 131 | 132 | 133 | [Haskell]: https://www.haskell.org/ 134 | [Ramda]: http://ramdajs.com/ 135 | [Sanctuary]: https://github.com/sanctuary-js/ 136 | [Fantasy Land]: https://github.com/fantasyland/fantasy-land 137 | [Setoid]: https://github.com/fantasyland/fantasy-land#setoid 138 | [Ord]: https://github.com/fantasyland/fantasy-land#ord 139 | [Semigroup]: https://github.com/fantasyland/fantasy-land#semigroup 140 | [Monoid]: https://github.com/fantasyland/fantasy-land#monoid 141 | [Functor]: https://github.com/fantasyland/fantasy-land#functor 142 | [Bifunctor]: https://github.com/fantasyland/fantasy-land#bifunctor 143 | [Apply]: https://github.com/fantasyland/fantasy-land#apply 144 | [Applicative]: https://github.com/fantasyland/fantasy-land#applicative 145 | [Alt]: https://github.com/fantasyland/fantasy-land#alt 146 | [Plus]: https://github.com/fantasyland/fantasy-land#plus 147 | [Foldable]: https://github.com/fantasyland/fantasy-land#foldable 148 | [Traversable]: https://github.com/fantasyland/fantasy-land#traversable 149 | [Chain]: https://github.com/fantasyland/fantasy-land#chain 150 | [ChainRec]: https://github.com/fantasyland/fantasy-land#chainRec 151 | [Extend]: https://github.com/fantasyland/fantasy-land#extend 152 | [Contravariant]: https://github.com/fantasyland/fantasy-land#contravariant 153 | -------------------------------------------------------------------------------- /packages/react-state-lens/src/lensProxy/README.md: -------------------------------------------------------------------------------- 1 | # lens-proxy 2 | Yet another solution for react state management based on profunctor algebra. 3 | 4 | ### Profunctors and lenses 5 | 6 | In profunctor optics a lens is like a focus within a data-structure that you can view or set, abstracting away the data-structure details. The key benefit of lens is composition. Lens compose easily. 7 | Another way to think about lens is just a pair of getter + setter. 8 | 9 | ```js 10 | const SomeComponent = props => { 11 | const [model, setModel] = useState(initialModel); 12 | } 13 | ``` 14 | Our profunctor (lens-ish) is a wrapper around the result of useState hook: [model:TModel, setModel: TModel -> unit] which obeys the profunctor algebraic structure. 15 | 16 | The main advantage of representing the state as a profunctor is that you can easily move the focus on fields and items by promap-ing. 17 | 18 | ### Fantasy land Profunctor 19 | 20 | A value that implements the Profunctor specification must also implement the Functor specification. 21 | 22 | 1. `p['fantasy-land/promap'](a => a, b => b)` is equivalent to `p` (identity) 23 | 2. `p['fantasy-land/promap'](a => f(g(a)), b => h(i(b)))` is equivalent to `p['fantasy-land/promap'](f, i)['fantasy-land/promap'](g, h)` (composition) 24 | 25 | 26 | 27 | #### `fantasy-land/promap` method 28 | 29 | ```hs 30 | fantasy-land/promap :: Profunctor p => p b c ~> (a -> b, c -> d) -> p a d 31 | ``` 32 | 33 | ### React state profunctor (lens-ish) 34 | ```js 35 | const SomeComponent = props => { 36 | const [modelLens] = useChangeTrackingState({a:1, b:2}) 37 | } 38 | ``` 39 | 40 | #### get 41 | get is used to read the value from lens. 42 | ```js 43 | const SomeComponent = props => { 44 | const lens = useChangeTrackingState(21) 45 | const value = lens |> get // 21 46 | } 47 | ``` 48 | 49 | #### set 50 | set is used to set the value of a lens. 51 | ```js 52 | const SomeComponent = props => { 53 | const lens = useChangeTrackingState(21) 54 | (lens |> set)(22) // sets the model to 22 55 | } 56 | ``` 57 | 58 | #### over 59 | over - sets the state of a lens using some updater fn. 60 | ```js 61 | const SomeComponent = props => { 62 | const lens = useChangeTrackingState('John') 63 | over(lens)(x => x + ' Smith') // sets the model to 'John Smith' 64 | } 65 | ``` 66 | 67 | #### promap 68 | Maps both the getter and the setter of a lens. 69 | ```js 70 | const SomeComponent = props => { 71 | const lens = useChangeTrackingState({a:1, b:2}) 72 | const aLens = lens |> promap(R.prop('a'), R.assoc('a')) 73 | console.log(aLens |> get) //1 74 | (aLens |> set)(0) // sets the model to {a:0, b:2} 75 | } 76 | ``` 77 | 78 | #### lmap 79 | Maps only the getter of a lens. 80 | ```js 81 | const SomeComponent = props => { 82 | const lens = useStateLens(null) 83 | const lensOrDefault = lens |> lmap(x=> x || "default") 84 | const value = lensOrDefault |> get //"default" 85 | } 86 | ``` 87 | 88 | #### rmap 89 | Maps only the setter of a lens. 90 | ```js 91 | const SomeComponent = props => { 92 | const lens = useStateLens(1) 93 | const lensOrDefault = lens |> rmap(x=> x || "default") 94 | set(lensOrDefault)(null) //sets the model to "default" 95 | } 96 | ``` 97 | 98 | #### sequence 99 | Sequence transforms a lens of array into an array of lenses. 100 | ```js 101 | const SomeComponent = props => { 102 | const lens = useStateLens([1,2,3]) 103 | const lenses = lens |> sequence 104 | const firstItem = lenses[0] |> get //1 105 | } 106 | ``` 107 | 108 | #### pipe 109 | Pipes a lens to a Ramda lens. Both the getters and setters are piped. 110 | ```js 111 | const SomeComponent = props => { 112 | const lens = useStateLens({a:1, b:2}) 113 | const aLens = pipe(lens, R.lens(R.prop('a'), R.assoc('a'))) 114 | console.log(aLens |> get) //1 115 | (aLens |> set)(0) // sets the model to {a:0, b:2} 116 | } 117 | ``` 118 | #### sintax sugar 119 | 120 | By using es6 proxy we were able to provide field and array indexer access just like with pojos. 121 | ```js 122 | const SomeComponent = props => { 123 | const lens = useStateLens({name: 'John'}) 124 | const nameLens = lens.name 125 | const name = nameLens |> get // John 126 | } 127 | ``` 128 | 129 | ```js 130 | const SomeComponent = props => { 131 | const lens = useStateLens([1,2,3]) 132 | const firstItemLens = lens[0] 133 | const firstItem = firstItemLens |> get // 1 134 | } 135 | ``` 136 | 137 | #### example usage with controlled components 138 | ```jsx 139 | const [personlens] = useStateLens({name:'John', age:23}) 140 | <...> 141 | get} 143 | onChange={personLens.name |> set |> onTextBoxChange} 144 | /> 145 | ``` 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /packages/change-tracking-react/README.md: -------------------------------------------------------------------------------- 1 | # change-tracking-react 2 | React extensions for the "change-tracking" library. 3 | 4 | 5 | ## installation 6 | ```javascript 7 | npm install @totalsoft/change-tracking-react 8 | ``` 9 | 10 | ## info 11 | The library provides three hooks: 12 | - **useChangeTrackingState** - provides a stateful model with change tracking 13 | - **useChangeTrackingLens** - provides a stateful model with change tracking using a profunctor lens 14 | - **useDirtyInfo** - keeps track of modified properties of an external model 15 | 16 | 17 | ## useChangeTrackingState hook 18 | React hook for change tracking a model. 19 | 20 | It Returns a stateful model, stateful dirty info object, a function that sets the model or property value and a function that resets the change tracking. 21 | 22 | Usage example: 23 | 24 | ```jsx 25 | import { useChangeTrackingState } from "@totalsoft/change-tracking-react"; 26 | import { isPropertyDirty } from "@totalsoft/change-tracking" 27 | 28 | const SomeComponent = props => { 29 | const [person, dirtyInfo, setPerson] = useChangeTrackingState({}); 30 | 31 | const handleChange = useCallback( 32 | propPath => event => { 33 | let newPerson = setInnerProp(person, propPath, event.target.value) 34 | setPerson(newPerson); 35 | }, [person] 36 | ); 37 | 38 | return ( 39 | <> 40 | FirstName dirty: {isPropertyDirty("firstName", dirtyInfo)} 41 | 45 | LastName dirty: {isPropertyDirty("lastName", dirtyInfo)} 46 | 50 | 51 | 52 | ); 53 | }; 54 | ``` 55 | ## useChangeTrackingLens hook 56 | Provides a stateful model with change tracking using a profunctor lens. 57 | 58 | It returns a stateful model, stateful dirty info object, and a function that resets the change tracking. 59 | 60 | Usage example: 61 | 62 | ```jsx 63 | import { useChangeTrackingLens, get, set } from "@totalsoft/change-tracking-react"; 64 | import { isPropertyDirty } from "@totalsoft/change-tracking" 65 | 66 | const onTextBoxChange = onPropertyChange => event => onPropertyChange(event.target.value) 67 | 68 | const SomeComponent = props => { 69 | const [personLens, dirtyInfo, reset] = useChangeTrackingLens({}); 70 | 71 | return ( 72 | <> 73 | FirstName dirty: {isPropertyDirty("firstName", dirtyInfo)} 74 | get} 76 | onChange={personLens.firstName |> set |> onTextBoxChange} 77 | /> 78 | LastName dirty: {isPropertyDirty("lastName", dirtyInfo)} 79 | get} 81 | onChange={personLens.lastName |> set |> onTextBoxChange} 82 | /> 83 | 84 | 85 | ); 86 | }; 87 | ``` 88 | 89 | Note: The `reset` function clears the dirty info object. If an object is passed as parameter, the model is set to that object, otherwise the current model is kept unchanged. It could also receive an updater function which takes the pending state and compute the next state from it. 90 | 91 | [Read more about lens operations](../react-state-lens/src/lensProxy/README.md) 92 | 93 | ## useDirtyInfo hook 94 | 95 | A react hook for field change tracking. 96 | 97 | It returns a stateful dirty info object, a function that sets the property path as dirty and a function that resets the dirty info state. 98 | 99 | Usage example: 100 | 101 | ```jsx 102 | import { useDirtyInfo } from "@totalsoft/change-tracking-react"; 103 | import { isPropertyDirty } from "@totalsoft/change-tracking" 104 | 105 | const SomeComponent = props => { 106 | const [person, setPerson] = useState({}); 107 | const [dirtyInfo, setDirtyInfoPath] = useDirtyInfo(); 108 | 109 | const handleChange = useCallback( 110 | propPath => event => { 111 | setDirtyInfoPath(propPath); 112 | setModel({ ...person, [propPath]: event.target.value }); 113 | }, [person] 114 | ); 115 | 116 | return ( 117 | <> 118 | FirstName dirty: {isPropertyDirty("firstName", dirtyInfo)} 119 | 123 | LastName dirty: {isPropertyDirty("lastName", dirtyInfo)} 124 | 128 | 129 | 130 | ); 131 | }; 132 | ``` 133 | 134 | Note: The `reset` function clears the dirty info object. If an object is passed as parameter, the model is set to that object, otherwise the current model is kept unchanged. 135 | -------------------------------------------------------------------------------- /packages/pure-validations/src/primitiveValidators/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) TotalSoft. 2 | // This source code is licensed under the MIT license. 3 | 4 | import { Validator } from "../validator"; 5 | import { Success, Failure } from "../validation"; 6 | import ValidationError from "../validationError"; 7 | 8 | import i18next from "i18next"; 9 | 10 | const defaultMessages = { 11 | "Validations.Generic.Mandatory": "The value is mandatory", 12 | "Validations.Generic.AtLeastOne": "There should be at least one item.", 13 | "Validations.Generic.Regex": "The value has an invalid format", 14 | "Validations.Generic.Email": "The value is not a valid email address", 15 | "Validations.Generic.OutOfRange": "The value must be between {{min}} and {{max}}", 16 | "Validations.Generic.Greater": "The value must be greater than {{min}}", 17 | "Validations.Generic.Less": "The value must be less than {{max}}", 18 | "Validations.Generic.MaxCharacters": "The length must be less than {{max}}", 19 | "Validations.Generic.MinCharacters": "The length must be greater than {{min}}", 20 | "Validations.Generic.Unique": "The value of {{selector}} must be unique", 21 | "Validations.Generic.Integer": "The value must be an integer number", 22 | "Validations.Generic.Number": "The value must be a number", 23 | }; 24 | 25 | function translate(key, args) { 26 | return i18next.t(key, { defaultValue: defaultMessages[key], ...args }) || defaultMessages[key]; 27 | } 28 | 29 | export const required = Validator(function required(x) { 30 | return x !== null && x !== undefined && (typeof x === "string" ? x !== "" : true) 31 | ? Success 32 | : Failure(ValidationError(translate("Validations.Generic.Mandatory"))); 33 | }); 34 | 35 | export const atLeastOne = Validator(function atLeastOne(x) { 36 | return Array.isArray(x) && x.length ? Success : Failure(ValidationError(translate("Validations.Generic.AtLeastOne"))); 37 | }); 38 | 39 | export const email = Validator(function email(x) { 40 | const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)+)|([^<>()[\]\\.,;:\s@"]{2,})|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]{2,}\.)+[a-zA-Z]{2,}))$/; 41 | return re.test(String(x).toLowerCase()) ? Success : Failure(ValidationError(translate("Validations.Generic.Email"))); 42 | }); 43 | 44 | export function matches(regex) { 45 | return Validator(function matches(x) { 46 | return regex.test(String(x)) ? Success : Failure(ValidationError(translate("Validations.Generic.Regex"))); 47 | }); 48 | } 49 | 50 | export function between(min, max) { 51 | return Validator(function between(value) { 52 | return value === null || value === undefined || (value >= min && value <= max) 53 | ? Success 54 | : Failure(ValidationError(translate("Validations.Generic.OutOfRange", { min, max }))); 55 | }); 56 | } 57 | 58 | export function greaterThan(min) { 59 | return Validator(function greaterThan(value) { 60 | return value === null || value === undefined || value > min 61 | ? Success 62 | : Failure(ValidationError(translate("Validations.Generic.Greater", { min }))); 63 | }); 64 | } 65 | 66 | export function lessThan(max) { 67 | return Validator(function lessThan(value) { 68 | return value === null || value === undefined || value < max ? Success : Failure(ValidationError(translate("Validations.Generic.Less", { max }))); 69 | }); 70 | } 71 | 72 | export function minLength(min) { 73 | return Validator(function minLength(value) { 74 | return value === null || value === undefined || value.length >= min 75 | ? Success 76 | : Failure(ValidationError(translate("Validations.Generic.MinCharacters", { min }))); 77 | }); 78 | } 79 | 80 | export function maxLength(max) { 81 | return Validator(function maxLength(value) { 82 | return value === null || value === undefined || value.length <= max 83 | ? Success 84 | : Failure(ValidationError(translate("Validations.Generic.MaxCharacters", { max }))); 85 | }); 86 | } 87 | 88 | export const isInteger = Validator(function isInteger(value) { 89 | return Number.isInteger(value) ? Success : Failure(ValidationError(translate("Validations.Generic.Integer"))); 90 | }); 91 | 92 | export const isNumber = Validator(function isNumber(value) { 93 | return typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value) 94 | ? Success 95 | : Failure(ValidationError(translate("Validations.Generic.Number"))); 96 | }); 97 | 98 | export const valid = Validator.of(Success); 99 | 100 | export function unique(selector, displayName = null) { 101 | return Validator(function unique(list) { 102 | if (list === null || list === undefined) { 103 | return Success; 104 | } 105 | 106 | let selectorFn; 107 | 108 | function buildSelectorFn(propArray) { 109 | return (x) => propArray.reduce((acc, prop) => acc[prop], x); 110 | } 111 | 112 | if (!selector) { 113 | selectorFn = (x) => x; 114 | } else if (typeof selector === "string") { 115 | selectorFn = buildSelectorFn(selector.split(".")); 116 | } else if (selector instanceof Array) { 117 | selectorFn = buildSelectorFn(selector); 118 | } else if (typeof selector === "function") { 119 | selectorFn = selector; 120 | } else { 121 | throw "Invalid selector"; // TBD 122 | } 123 | 124 | const fieldName = displayName ? i18next.t(displayName) || displayName : selector ? selector.toString() : ""; 125 | 126 | return [...new Set(list.map(selectorFn))].length === list.length 127 | ? Success 128 | : Failure(ValidationError(translate("Validations.Generic.Unique", { selector: fieldName }))); 129 | }); 130 | } 131 | --------------------------------------------------------------------------------